@headless-lms/core 0.4.0 → 0.6.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 +15 -9
- package/dist/assets/file-urls.d.ts +14 -7
- package/dist/assets/file-urls.js +15 -4
- package/dist/assets/file-urls.js.map +1 -1
- package/dist/assets/index.d.ts +4 -4
- package/dist/assets/index.js +3 -3
- package/dist/assets/ports.d.ts +34 -15
- package/dist/assets/service.d.ts +30 -14
- package/dist/assets/service.js +233 -54
- package/dist/assets/service.js.map +1 -1
- package/dist/automations/actions.js.map +1 -1
- package/dist/automations/catalog.js +68 -3
- package/dist/automations/catalog.js.map +1 -1
- package/dist/automations/community-derivations.js +16 -16
- package/dist/automations/community-derivations.js.map +1 -1
- package/dist/automations/community-mail.d.ts +34 -4
- package/dist/automations/community-mail.js +32 -4
- package/dist/automations/community-mail.js.map +1 -1
- package/dist/automations/event-permissions.d.ts +12 -0
- package/dist/automations/event-permissions.js +38 -0
- package/dist/automations/event-permissions.js.map +1 -0
- package/dist/automations/index.d.ts +2 -1
- package/dist/automations/index.js +2 -1
- package/dist/automations/notifications/applicable.d.ts +8 -0
- package/dist/automations/notifications/applicable.js +12 -0
- package/dist/automations/notifications/applicable.js.map +1 -0
- package/dist/automations/notifications/debounce.js +1 -1
- package/dist/automations/notifications/debounce.js.map +1 -1
- package/dist/automations/notifications/defaults.js +33 -3
- package/dist/automations/notifications/defaults.js.map +1 -1
- package/dist/automations/notifications/processor.js +1 -1
- package/dist/automations/notifications/processor.js.map +1 -1
- package/dist/automations/notifications/rules/content.js +60 -3
- package/dist/automations/notifications/rules/content.js.map +1 -1
- package/dist/automations/notifications/rules/discussion.js +29 -29
- package/dist/automations/notifications/rules/discussion.js.map +1 -1
- package/dist/automations/notifications/rules/entitlements.js +36 -9
- package/dist/automations/notifications/rules/entitlements.js.map +1 -1
- package/dist/automations/notifications/rules/events.d.ts +9 -0
- package/dist/automations/notifications/rules/events.js +27 -0
- package/dist/automations/notifications/rules/events.js.map +1 -0
- package/dist/automations/notifications/rules/organizations.js +17 -10
- package/dist/automations/notifications/rules/organizations.js.map +1 -1
- package/dist/automations/notifications/rules/recipients.js +0 -1
- package/dist/automations/notifications/rules/recipients.js.map +1 -1
- package/dist/automations/notifications/service.d.ts +8 -6
- package/dist/automations/notifications/service.js +12 -12
- package/dist/automations/notifications/service.js.map +1 -1
- package/dist/automations/notifications/types.js +94 -7
- package/dist/automations/notifications/types.js.map +1 -1
- package/dist/automations/ports.d.ts +30 -31
- package/dist/automations/service.d.ts +20 -17
- package/dist/automations/service.js +90 -61
- package/dist/automations/service.js.map +1 -1
- package/dist/content/event-sight.d.ts +11 -0
- package/dist/content/event-sight.js +11 -0
- package/dist/content/event-sight.js.map +1 -0
- package/dist/content/events.d.ts +465 -88
- package/dist/content/events.js +34 -1
- 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/jobs.d.ts +16 -0
- package/dist/content/jobs.js +37 -0
- package/dist/content/jobs.js.map +1 -0
- package/dist/content/model.d.ts +2 -2
- package/dist/content/notification-categories.d.ts +5 -4
- package/dist/content/notification-categories.js +15 -6
- package/dist/content/notification-categories.js.map +1 -1
- package/dist/content/ports.d.ts +113 -68
- package/dist/content/service.d.ts +103 -67
- package/dist/content/service.js +739 -152
- package/dist/content/service.js.map +1 -1
- package/dist/content/titles.d.ts +6 -3
- package/dist/content/titles.js +37 -24
- package/dist/content/titles.js.map +1 -1
- package/dist/content/types.d.ts +2 -2
- package/dist/discussion/authors.d.ts +4 -2
- package/dist/discussion/authors.js +9 -5
- package/dist/discussion/authors.js.map +1 -1
- package/dist/discussion/chats.d.ts +56 -37
- package/dist/discussion/chats.js +392 -160
- package/dist/discussion/chats.js.map +1 -1
- package/dist/discussion/events.d.ts +85 -47
- package/dist/discussion/events.js +41 -3
- package/dist/discussion/events.js.map +1 -1
- package/dist/discussion/index.d.ts +8 -5
- package/dist/discussion/index.js +5 -3
- package/dist/discussion/live.d.ts +106 -0
- package/dist/discussion/live.js +642 -0
- package/dist/discussion/live.js.map +1 -0
- package/dist/discussion/mentions.js +2 -2
- package/dist/discussion/mentions.js.map +1 -1
- package/dist/discussion/ports.d.ts +182 -100
- package/dist/discussion/ports.js.map +1 -1
- package/dist/discussion/posts.d.ts +45 -39
- package/dist/discussion/posts.js +266 -152
- package/dist/discussion/posts.js.map +1 -1
- package/dist/discussion/rules.d.ts +18 -0
- package/dist/discussion/rules.js +22 -0
- package/dist/discussion/rules.js.map +1 -0
- package/dist/discussion/service.d.ts +37 -31
- package/dist/discussion/service.js +250 -186
- package/dist/discussion/service.js.map +1 -1
- package/dist/discussion/subscriptions.d.ts +8 -8
- package/dist/discussion/subscriptions.js +28 -12
- package/dist/discussion/subscriptions.js.map +1 -1
- package/dist/discussion/titles.d.ts +4 -2
- package/dist/discussion/titles.js +27 -13
- package/dist/discussion/titles.js.map +1 -1
- package/dist/entitlements/events.d.ts +58 -58
- package/dist/entitlements/index.d.ts +4 -4
- package/dist/entitlements/index.js +2 -2
- package/dist/entitlements/model.d.ts +2 -2
- package/dist/entitlements/ports.d.ts +65 -39
- package/dist/entitlements/rules.d.ts +3 -6
- package/dist/entitlements/rules.js +5 -10
- package/dist/entitlements/rules.js.map +1 -1
- package/dist/entitlements/service.d.ts +40 -28
- package/dist/entitlements/service.js +369 -162
- package/dist/entitlements/service.js.map +1 -1
- package/dist/identity/devices.d.ts +3 -3
- package/dist/identity/devices.js +6 -4
- package/dist/identity/devices.js.map +1 -1
- package/dist/identity/index.d.ts +2 -2
- package/dist/identity/index.js +2 -2
- package/dist/identity/notification-settings.d.ts +1 -1
- package/dist/identity/notification-settings.js +2 -1
- package/dist/identity/notification-settings.js.map +1 -1
- package/dist/identity/ports.d.ts +7 -7
- package/dist/identity/service.d.ts +8 -7
- package/dist/identity/service.js +16 -12
- package/dist/identity/service.js.map +1 -1
- package/dist/integrations/events.d.ts +3 -3
- package/dist/integrations/ports.d.ts +9 -8
- package/dist/integrations/service.d.ts +9 -8
- package/dist/integrations/service.js +31 -14
- package/dist/integrations/service.js.map +1 -1
- package/dist/organizations/default-roles.d.ts +22 -0
- package/dist/organizations/default-roles.js +89 -0
- package/dist/organizations/default-roles.js.map +1 -0
- package/dist/organizations/effective-permissions.d.ts +47 -0
- package/dist/organizations/effective-permissions.js +118 -0
- package/dist/organizations/effective-permissions.js.map +1 -0
- package/dist/organizations/events.d.ts +326 -181
- package/dist/organizations/events.js +90 -18
- package/dist/organizations/events.js.map +1 -1
- package/dist/organizations/index.d.ts +14 -10
- package/dist/organizations/index.js +7 -3
- package/dist/organizations/members.d.ts +66 -8
- package/dist/organizations/members.js.map +1 -1
- package/dist/organizations/model.d.ts +2 -2
- package/dist/organizations/notification-catalog.d.ts +31 -0
- package/dist/organizations/notification-catalog.js +32 -0
- package/dist/organizations/notification-catalog.js.map +1 -0
- package/dist/organizations/notification-categories.d.ts +6 -4
- package/dist/organizations/notification-categories.js +25 -10
- package/dist/organizations/notification-categories.js.map +1 -1
- package/dist/organizations/org-roles.d.ts +33 -0
- package/dist/organizations/org-roles.js +49 -0
- package/dist/organizations/org-roles.js.map +1 -0
- package/dist/organizations/permissions.d.ts +311 -0
- package/dist/organizations/permissions.js +327 -0
- package/dist/organizations/permissions.js.map +1 -0
- package/dist/organizations/ports.d.ts +187 -65
- package/dist/organizations/service.d.ts +102 -35
- package/dist/organizations/service.js +1254 -327
- package/dist/organizations/service.js.map +1 -1
- package/dist/organizations/testing.d.ts +18 -0
- package/dist/organizations/testing.js +37 -0
- package/dist/organizations/testing.js.map +1 -0
- package/dist/organizations/types.d.ts +5 -16
- package/dist/progress/events.d.ts +14 -14
- package/dist/progress/index.d.ts +2 -2
- package/dist/progress/ports.d.ts +11 -3
- package/dist/progress/service.d.ts +6 -5
- package/dist/progress/service.js +21 -6
- package/dist/progress/service.js.map +1 -1
- package/dist/reporting/admin/index.d.ts +6 -4
- package/dist/reporting/admin/model.d.ts +36 -3
- package/dist/reporting/admin/ports.d.ts +39 -14
- package/dist/reporting/admin/service.d.ts +32 -11
- package/dist/reporting/admin/service.js +102 -18
- package/dist/reporting/admin/service.js.map +1 -1
- package/dist/reporting/events/index.d.ts +27 -0
- package/dist/reporting/events/index.js +20 -0
- package/dist/reporting/events/index.js.map +1 -0
- package/dist/reporting/member/calendar.d.ts +11 -0
- package/dist/reporting/member/calendar.js +75 -0
- package/dist/reporting/member/calendar.js.map +1 -0
- package/dist/reporting/member/index.d.ts +3 -2
- package/dist/reporting/member/model.d.ts +29 -5
- package/dist/reporting/member/ports.d.ts +49 -29
- package/dist/reporting/member/service.d.ts +57 -29
- package/dist/reporting/member/service.js +345 -114
- package/dist/reporting/member/service.js.map +1 -1
- package/dist/shared/actor.d.ts +23 -3
- package/dist/shared/errors.d.ts +2 -1
- package/dist/shared/errors.js +3 -1
- package/dist/shared/errors.js.map +1 -1
- package/dist/shared/id.d.ts +4 -0
- package/dist/shared/id.js +4 -0
- package/dist/shared/id.js.map +1 -1
- package/dist/shared/logger.d.ts +2 -1
- package/dist/shared/logger.js +10 -1
- package/dist/shared/logger.js.map +1 -1
- package/dist/shared/ports.d.ts +25 -5
- package/dist/types/content.d.ts +2 -2
- package/dist/types/discussion.d.ts +4 -5
- package/dist/types/email-templates.d.ts +22 -3
- package/dist/types/entitlements.d.ts +2 -2
- package/dist/types/identity.d.ts +13 -16
- package/dist/types/index.d.ts +9 -9
- package/dist/types/index.js +2 -1
- package/dist/types/notifications.d.ts +4 -4
- package/dist/types/organizations.d.ts +3 -2
- package/dist/types/ports.d.ts +8 -1
- package/dist/types/ports.js +7 -1
- package/dist/types/ports.js.map +1 -0
- package/dist/types/schemas/assets.d.ts +5 -5
- package/dist/types/schemas/assets.js +4 -3
- package/dist/types/schemas/assets.js.map +1 -1
- package/dist/types/schemas/content.d.ts +599 -13
- package/dist/types/schemas/content.js +219 -8
- package/dist/types/schemas/content.js.map +1 -1
- package/dist/types/schemas/discussion.d.ts +280 -57
- package/dist/types/schemas/discussion.js +77 -3
- package/dist/types/schemas/discussion.js.map +1 -1
- package/dist/types/schemas/entitlements.d.ts +21 -8
- package/dist/types/schemas/entitlements.js +11 -4
- package/dist/types/schemas/entitlements.js.map +1 -1
- package/dist/types/schemas/index.d.ts +5 -5
- package/dist/types/schemas/index.js +5 -5
- package/dist/types/schemas/notifications.d.ts +11 -10
- package/dist/types/schemas/notifications.js +3 -2
- package/dist/types/schemas/notifications.js.map +1 -1
- package/dist/types/schemas/organizations.d.ts +99 -60
- package/dist/types/schemas/organizations.js +80 -27
- package/dist/types/schemas/organizations.js.map +1 -1
- package/dist/types/schemas/progress.d.ts +3 -4
- package/dist/types/schemas/progress.js +0 -1
- package/dist/types/schemas/progress.js.map +1 -1
- package/package.json +1 -1
- package/dist/organizations/access-scopes.d.ts +0 -19
- package/dist/organizations/access-scopes.js +0 -89
- package/dist/organizations/access-scopes.js.map +0 -1
- package/dist/organizations/roles.d.ts +0 -19
- package/dist/organizations/roles.js +0 -82
- package/dist/organizations/roles.js.map +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"service.js","names":[],"sources":["../../src/identity/service.ts"],"sourcesContent":["import type { IdentityRepository, IdentityService, IdentityUnitOfWork } from './ports.js';\nimport type { User, UserProfile, UserProfileView } from './model.js';\nimport type { UpdateProfileInput } from './ports.js';\nimport type { CreateUserInput, UpdateUserInput } from './types.js';\nimport type { Logger } from '../shared/ports.js';\nimport { noopLogger } from '../shared/logger.js';\nimport { genId } from '../shared/id.js';\nimport { ConflictError, ForbiddenError, NotFoundError } from '../shared/errors.js';\nimport type { Mailer } from '../shared/mailer.js';\nimport type { FileUrls, MemberImageReader } from '../assets/index.js';\nimport { identityEvents } from './events.js';\n\nconst UNNAMED = 'A member';\n\nexport class IdentityServiceImpl implements IdentityService {\n private readonly repo: IdentityRepository;\n private readonly logger: Logger;\n private readonly uow: IdentityUnitOfWork;\n private readonly mailer: Pick<Mailer, 'send'>;\n private readonly images: MemberImageReader;\n private readonly fileUrls: FileUrls;\n\n constructor(input: {\n repo: IdentityRepository;\n logger?: Logger;\n mailer: Mailer;\n uow: IdentityUnitOfWork;\n images: MemberImageReader;\n fileUrls: FileUrls;\n }) {\n this.repo = input.repo;\n this.mailer = input.mailer;\n this.logger = input.logger ?? noopLogger;\n this.uow = input.uow;\n this.images = input.images;\n this.fileUrls = input.fileUrls;\n }\n\n async createUser(input: CreateUserInput): Promise<User> {\n const created = await this.uow.run(async ({ identity, outbox }) => {\n const user = await identity.insertUser(input);\n await outbox.append([identityEvents.userCreated.make({ orgId: '-', data: user })]);\n return user;\n });\n this.logger.info('user created', { userId: created.id });\n return created;\n }\n\n async linkOrCreateUser(input: CreateUserInput): Promise<User> {\n const existing = await this.repo.findUserByEmail(input.email);\n if (!existing) {\n const id = input.id ?? genId('user');\n return this.createUser({ ...input, id, externalId: id });\n }\n if (existing.externalId !== null) {\n throw new ConflictError('That email already has an account');\n }\n // Existing user without external link, so invited. update the user with the\n // id.\n const linked = await this.uow.run(async ({ identity, outbox }) => {\n const user = await identity.updateUser(existing.id, {\n externalId: existing.id,\n ...(input.firstName !== undefined && { firstName: input.firstName }),\n ...(input.lastName !== undefined && { lastName: input.lastName }),\n });\n await outbox.append([identityEvents.userUpdated.make({ orgId: '-', data: user! })]);\n return user!;\n });\n this.logger.info('user linked to auth account', { userId: linked.id });\n return linked;\n }\n\n async updateUser(id: string, input: UpdateUserInput): Promise<User> {\n const person = await this.repo.findUserById(id);\n if (!person) {\n throw new NotFoundError('User', id);\n }\n\n const updated = await this.uow.run(async ({ identity, outbox }) => {\n const user = await identity.updateUser(id, input);\n await outbox.append([identityEvents.userUpdated.make({ orgId: '-', data: user! })]);\n return user!;\n });\n this.logger.info('user updated', { userId: updated.id });\n return updated;\n }\n\n async getUserByExternalId(externalId: string): Promise<User | null> {\n return this.repo.findUserByExternalId(externalId);\n }\n\n async getUserByEmail(email: string): Promise<User | null> {\n return this.repo.findUserByEmail(email);\n }\n\n async getUserById(id: string): Promise<User | null> {\n return this.repo.findUserById(id);\n }\n\n async sendPasswordReset(input: { email: string; url: string }): Promise<void> {\n await this.mailer.send(input.email, 'passwordReset', { resetUrl: input.url });\n }\n\n async sendPasswordChanged(input: { email: string }): Promise<void> {\n await this.mailer.send(input.email, 'passwordChanged', {});\n }\n\n async sendMagicLink(input: { email: string; url: string }): Promise<void> {\n await this.mailer.send(input.email, 'magicLink', { url: input.url });\n }\n\n async profileOf(orgId: string, userId: string): Promise<UserProfileView> {\n const stored = await this.repo.findProfile(userId);\n if (stored) {\n return this.view(orgId, stored);\n }\n const never = new Date(0);\n return {\n userId,\n displayName: null,\n headline: null,\n bio: null,\n imageAssetOrgId: null,\n imageAssetId: null,\n imageUrl: null,\n extra: {},\n createdAt: never,\n updatedAt: never,\n };\n }\n\n private view(orgId: string, profile: UserProfile): UserProfileView {\n const imageAssetId = profile.imageAssetOrgId === orgId ? profile.imageAssetId : null;\n return { ...profile, imageUrl: imageAssetId ? this.fileUrls.memberUpload(imageAssetId) : null };\n }\n\n async displayName(userId: string | null): Promise<string> {\n if (userId === null) {\n return UNNAMED;\n }\n const [profile, user] = await Promise.all([\n this.repo.findProfile(userId),\n this.repo.findUserById(userId),\n ]);\n const named = profile?.displayName?.trim();\n if (named) {\n return named;\n }\n const composed = [user?.firstName, user?.lastName].filter(Boolean).join(' ').trim();\n return composed || UNNAMED;\n }\n\n async updateProfile(\n orgId: string,\n userId: string,\n actorUserId: string,\n orgUserId: string,\n patch: UpdateProfileInput,\n ): Promise<UserProfileView> {\n if (actorUserId !== userId) {\n throw new ForbiddenError('You can only update your own profile');\n }\n const user = await this.repo.findUserById(userId);\n if (!user) {\n throw new NotFoundError('User', userId);\n }\n if (patch.imageAssetId != null) {\n const owned = await this.images.ownsImage(orgId, patch.imageAssetId, orgUserId);\n if (!owned) {\n this.logger.debug('profile photo refused', {\n userId,\n orgUserId,\n assetId: patch.imageAssetId,\n });\n throw new NotFoundError('Asset', patch.imageAssetId);\n }\n }\n const imageAssetOrgId =\n patch.imageAssetId === undefined ? undefined : patch.imageAssetId === null ? null : orgId;\n const profile = await this.repo.upsertProfile(userId, { ...patch, imageAssetOrgId });\n this.logger.info('profile updated', { userId });\n return this.view(orgId, profile);\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"service.js","names":[],"sources":["../../src/identity/service.ts"],"sourcesContent":["import type { IdentityRepository, IdentityService, IdentityUnitOfWork } from './ports.js';\nimport type { User, UserProfile, UserProfileView } from './model.js';\nimport type { MemberActor, UpdateProfileInput } from './ports.js';\nimport type { CreateUserInput, UpdateUserInput } from './types.js';\nimport type { Logger } from '../shared/ports.js';\nimport { noopLogger } from '../shared/logger.js';\nimport { genId } from '../shared/id.js';\nimport { ConflictError, NotFoundError } from '../shared/errors.js';\nimport type { Mailer } from '../shared/mailer.js';\nimport type { AssetFiles, FileUrls, MemberImageReader } from '../assets/index.js';\nimport { identityEvents } from './events.js';\n\nexport const UNNAMED_MEMBER = 'A member';\n\nexport class IdentityServiceImpl implements IdentityService {\n private readonly repo: IdentityRepository;\n private readonly logger: Logger;\n private readonly uow: IdentityUnitOfWork;\n private readonly mailer: Pick<Mailer, 'send'>;\n private readonly images: MemberImageReader & Pick<AssetFiles, 'syncPublicImages'>;\n private readonly fileUrls: FileUrls;\n\n constructor(input: {\n repo: IdentityRepository;\n logger?: Logger;\n mailer: Mailer;\n uow: IdentityUnitOfWork;\n images: MemberImageReader & Pick<AssetFiles, 'syncPublicImages'>;\n fileUrls: FileUrls;\n }) {\n this.repo = input.repo;\n this.mailer = input.mailer;\n this.logger = input.logger ?? noopLogger;\n this.uow = input.uow;\n this.images = input.images;\n this.fileUrls = input.fileUrls;\n }\n\n async createUser(input: CreateUserInput): Promise<User> {\n const created = await this.uow.run(async ({ identity, outbox }) => {\n const user = await identity.insertUser(input);\n await outbox.append([identityEvents.userCreated.make({ orgId: '-', data: user })]);\n return user;\n });\n this.logger.info('user created', { userId: created.id });\n return created;\n }\n\n async linkOrCreateUser(input: CreateUserInput): Promise<User> {\n const existing = await this.repo.findUserByEmail(input.email);\n if (!existing) {\n const id = input.id ?? genId('user');\n return this.createUser({ ...input, id, externalId: id });\n }\n if (existing.externalId !== null) {\n throw new ConflictError('That email already has an account');\n }\n // Existing user without external link, so invited. update the user with the\n // id.\n const linked = await this.uow.run(async ({ identity, outbox }) => {\n const user = await identity.updateUser(existing.id, {\n externalId: existing.id,\n ...(input.firstName !== undefined && { firstName: input.firstName }),\n ...(input.lastName !== undefined && { lastName: input.lastName }),\n });\n await outbox.append([identityEvents.userUpdated.make({ orgId: '-', data: user! })]);\n return user!;\n });\n this.logger.info('user linked to auth account', { userId: linked.id });\n return linked;\n }\n\n async updateUser(id: string, input: UpdateUserInput): Promise<User> {\n const person = await this.repo.findUserById(id);\n if (!person) {\n throw new NotFoundError('User', id);\n }\n\n const updated = await this.uow.run(async ({ identity, outbox }) => {\n const user = await identity.updateUser(id, input);\n await outbox.append([identityEvents.userUpdated.make({ orgId: '-', data: user! })]);\n return user!;\n });\n this.logger.info('user updated', { userId: updated.id });\n return updated;\n }\n\n async getUserByExternalId(externalId: string): Promise<User | null> {\n return this.repo.findUserByExternalId(externalId);\n }\n\n async getUserByEmail(email: string): Promise<User | null> {\n return this.repo.findUserByEmail(email);\n }\n\n async getUserById(id: string): Promise<User | null> {\n return this.repo.findUserById(id);\n }\n\n async sendPasswordReset(input: { email: string; url: string }): Promise<void> {\n await this.mailer.send(input.email, 'passwordReset', { resetUrl: input.url });\n }\n\n async sendPasswordChanged(input: { email: string }): Promise<void> {\n await this.mailer.send(input.email, 'passwordChanged', {});\n }\n\n async sendMagicLink(input: { email: string; url: string }): Promise<void> {\n await this.mailer.send(input.email, 'magicLink', { url: input.url });\n }\n\n async profileOf(actor: MemberActor): Promise<UserProfileView> {\n const userId = actor.userId;\n const stored = await this.repo.findProfile(userId);\n if (stored) {\n return this.view(actor.orgId, actor.id, stored);\n }\n const never = new Date(0);\n return {\n userId,\n displayName: null,\n headline: null,\n bio: null,\n imageAssetOrgId: null,\n imageAssetId: null,\n imageUrl: null,\n extra: {},\n createdAt: never,\n updatedAt: never,\n };\n }\n\n private view(orgId: string, orgUserId: string, profile: UserProfile): UserProfileView {\n const imageAssetId = profile.imageAssetOrgId === orgId ? profile.imageAssetId : null;\n return {\n ...profile,\n imageUrl: imageAssetId ? this.fileUrls.publicImage(orgId, orgUserId, imageAssetId) : null,\n };\n }\n\n async displayName(userId: string | null): Promise<string> {\n if (userId === null) {\n return UNNAMED_MEMBER;\n }\n const [profile, user] = await Promise.all([\n this.repo.findProfile(userId),\n this.repo.findUserById(userId),\n ]);\n const named = profile?.displayName?.trim();\n if (named) {\n return named;\n }\n const composed = [user?.firstName, user?.lastName].filter(Boolean).join(' ').trim();\n return composed || UNNAMED_MEMBER;\n }\n\n async updateProfile(patch: UpdateProfileInput, actor: MemberActor): Promise<UserProfileView> {\n const orgId = actor.orgId;\n const userId = actor.userId;\n const orgUserId = actor.id;\n const user = await this.repo.findUserById(userId);\n if (!user) {\n throw new NotFoundError('User', userId);\n }\n if (patch.imageAssetId != null) {\n const owned = await this.images.ownsImage(orgId, patch.imageAssetId, orgUserId);\n if (!owned) {\n this.logger.debug('profile photo refused', {\n userId,\n orgUserId,\n assetId: patch.imageAssetId,\n });\n throw new NotFoundError('Asset', patch.imageAssetId);\n }\n }\n const imageAssetOrgId =\n patch.imageAssetId === undefined ? undefined : patch.imageAssetId === null ? null : orgId;\n const profile = await this.repo.upsertProfile(userId, { ...patch, imageAssetOrgId });\n this.logger.info('profile updated', { userId });\n if (patch.imageAssetId !== undefined) {\n await this.images.syncPublicImages(\n orgId,\n orgUserId,\n profile.imageAssetOrgId === orgId && profile.imageAssetId ? [profile.imageAssetId] : [],\n );\n }\n return this.view(orgId, orgUserId, profile);\n }\n}\n"],"mappings":"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"}
|
|
@@ -22,8 +22,8 @@ declare const integrationEvents: {
|
|
|
22
22
|
createdAt: unknown;
|
|
23
23
|
updatedAt: unknown;
|
|
24
24
|
}, import("zod").ZodObject<{
|
|
25
|
-
id: import("zod").ZodString;
|
|
26
25
|
orgId: import("zod").ZodString;
|
|
26
|
+
id: import("zod").ZodString;
|
|
27
27
|
createdAt: import("zod").ZodCoercedDate<unknown>;
|
|
28
28
|
updatedAt: import("zod").ZodCoercedDate<unknown>;
|
|
29
29
|
active: import("zod").ZodBoolean;
|
|
@@ -49,8 +49,8 @@ declare const integrationEvents: {
|
|
|
49
49
|
createdAt: unknown;
|
|
50
50
|
updatedAt: unknown;
|
|
51
51
|
}, import("zod").ZodObject<{
|
|
52
|
-
id: import("zod").ZodString;
|
|
53
52
|
orgId: import("zod").ZodString;
|
|
53
|
+
id: import("zod").ZodString;
|
|
54
54
|
createdAt: import("zod").ZodCoercedDate<unknown>;
|
|
55
55
|
updatedAt: import("zod").ZodCoercedDate<unknown>;
|
|
56
56
|
active: import("zod").ZodBoolean;
|
|
@@ -76,8 +76,8 @@ declare const integrationEvents: {
|
|
|
76
76
|
createdAt: unknown;
|
|
77
77
|
updatedAt: unknown;
|
|
78
78
|
}, import("zod").ZodObject<{
|
|
79
|
-
id: import("zod").ZodString;
|
|
80
79
|
orgId: import("zod").ZodString;
|
|
80
|
+
id: import("zod").ZodString;
|
|
81
81
|
createdAt: import("zod").ZodCoercedDate<unknown>;
|
|
82
82
|
updatedAt: import("zod").ZodCoercedDate<unknown>;
|
|
83
83
|
active: import("zod").ZodBoolean;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { ConfigureInput, ConnectInput, Connection, Validation } from "../types/schemas/integrations.js";
|
|
2
2
|
import { Action, ActionContext, Integration } from "../types/integrations.js";
|
|
3
3
|
import "../types/index.js";
|
|
4
|
+
import { Actor } from "../shared/actor.js";
|
|
4
5
|
import { CredentialStore, OutboxAppender, UnitOfWork } from "../shared/ports.js";
|
|
5
6
|
import "./model.js";
|
|
6
7
|
//#region src/integrations/ports.d.ts
|
|
@@ -23,22 +24,22 @@ interface AvailableIntegration {
|
|
|
23
24
|
}
|
|
24
25
|
interface IntegrationsService {
|
|
25
26
|
/** The integrations declared in this deployment, with their config schemas. */
|
|
26
|
-
available(): AvailableIntegration[];
|
|
27
|
+
available(actor: Actor): AvailableIntegration[];
|
|
27
28
|
/** Establish a connection for an org; stores its credential and configuration. */
|
|
28
|
-
connect(
|
|
29
|
+
connect(input: ConnectInput, actor: Actor): Promise<Connection>;
|
|
29
30
|
/** Replace a connection's stored secrets (re-auth / token refresh). */
|
|
30
|
-
reconnect(
|
|
31
|
+
reconnect(id: string, secrets: Record<string, unknown>, actor: Actor): Promise<Connection>;
|
|
31
32
|
/** Set or change a connection's configuration and/or active flag. */
|
|
32
|
-
configure(
|
|
33
|
+
configure(id: string, input: ConfigureInput, actor: Actor): Promise<Connection>;
|
|
33
34
|
/** Remove a connection and destroy its credential. */
|
|
34
|
-
disconnect(
|
|
35
|
-
list(
|
|
36
|
-
get(
|
|
35
|
+
disconnect(id: string, actor: Actor): Promise<void>;
|
|
36
|
+
list(actor: Actor): Promise<Connection[]>;
|
|
37
|
+
get(id: string, actor: Actor): Promise<Connection>;
|
|
37
38
|
/** The connection consumers (billing, automations) resolve an integration by id. */
|
|
38
39
|
getByIntegration(orgId: string, integrationId: string): Promise<Connection | null>;
|
|
39
40
|
/** Invoke one of a connection's actions with its credential revealed at point
|
|
40
41
|
* of use (e.g. a listing action that feeds a client-side picker). */
|
|
41
|
-
invoke(
|
|
42
|
+
invoke(connectionId: string, actionId: string, input: Record<string, unknown>, actor: Actor): Promise<unknown>;
|
|
42
43
|
}
|
|
43
44
|
interface ConnectionsRepository {
|
|
44
45
|
insert(orgId: string, connection: Connection): Promise<Connection>;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Logger } from "../types/ports.js";
|
|
2
2
|
import { ConfigureInput, ConnectInput, Connection } from "../types/schemas/integrations.js";
|
|
3
|
+
import { Actor } from "../shared/actor.js";
|
|
3
4
|
import { CredentialStore } from "../shared/ports.js";
|
|
4
5
|
import "./model.js";
|
|
5
6
|
import { ConnectionsRepository, IntegrationsRegistry, IntegrationsService, IntegrationsUnitOfWork } from "./ports.js";
|
|
@@ -21,7 +22,7 @@ declare class IntegrationsServiceImpl implements IntegrationsService {
|
|
|
21
22
|
private readonly credentials;
|
|
22
23
|
private readonly logger;
|
|
23
24
|
constructor(params: IntegrationsServiceParams);
|
|
24
|
-
available(): {
|
|
25
|
+
available(actor: Actor): {
|
|
25
26
|
id: string;
|
|
26
27
|
configSchema: Record<string, unknown>;
|
|
27
28
|
secretsSchema: Record<string, unknown>;
|
|
@@ -33,14 +34,14 @@ declare class IntegrationsServiceImpl implements IntegrationsService {
|
|
|
33
34
|
}[];
|
|
34
35
|
}[];
|
|
35
36
|
private validate;
|
|
36
|
-
connect(
|
|
37
|
-
reconnect(
|
|
38
|
-
configure(
|
|
39
|
-
disconnect(
|
|
40
|
-
list(
|
|
41
|
-
get(
|
|
37
|
+
connect(input: ConnectInput, actor: Actor): Promise<Connection>;
|
|
38
|
+
reconnect(id: string, secrets: Record<string, unknown>, actor: Actor): Promise<Connection>;
|
|
39
|
+
configure(id: string, input: ConfigureInput, actor: Actor): Promise<Connection>;
|
|
40
|
+
disconnect(id: string, actor: Actor): Promise<void>;
|
|
41
|
+
list(actor: Actor): Promise<Connection[]>;
|
|
42
|
+
get(id: string, actor: Actor): Promise<Connection>;
|
|
42
43
|
getByIntegration(orgId: string, integrationId: string): Promise<Connection | null>;
|
|
43
|
-
invoke(
|
|
44
|
+
invoke(connectionId: string, actionId: string, input: Record<string, unknown>, actor: Actor): Promise<unknown>;
|
|
44
45
|
}
|
|
45
46
|
//#endregion
|
|
46
47
|
export { IntegrationsServiceImpl, IntegrationsServiceParams };
|
|
@@ -17,7 +17,8 @@ var IntegrationsServiceImpl = class {
|
|
|
17
17
|
this.credentials = params.credentials;
|
|
18
18
|
this.logger = params.logger ?? noopLogger;
|
|
19
19
|
}
|
|
20
|
-
available() {
|
|
20
|
+
available(actor) {
|
|
21
|
+
actor.permissions.require("integrations:read");
|
|
21
22
|
return this.registry.list().map((integration) => ({
|
|
22
23
|
id: integration.id,
|
|
23
24
|
configSchema: integration.configSchema(),
|
|
@@ -45,7 +46,9 @@ var IntegrationsServiceImpl = class {
|
|
|
45
46
|
throw new InvalidConfigError(integrationId, result.errors);
|
|
46
47
|
}
|
|
47
48
|
}
|
|
48
|
-
async connect(
|
|
49
|
+
async connect(input, actor) {
|
|
50
|
+
actor.permissions.require("integrations:write");
|
|
51
|
+
const orgId = actor.orgId;
|
|
49
52
|
const config = input.config ?? {};
|
|
50
53
|
this.validate(input.integrationId, config);
|
|
51
54
|
if (await this.repo.findByIntegration(orgId, input.integrationId)) {
|
|
@@ -81,9 +84,11 @@ var IntegrationsServiceImpl = class {
|
|
|
81
84
|
});
|
|
82
85
|
return connection;
|
|
83
86
|
}
|
|
84
|
-
async reconnect(
|
|
87
|
+
async reconnect(id, secrets, actor) {
|
|
88
|
+
actor.permissions.require("integrations:write");
|
|
89
|
+
const orgId = actor.orgId;
|
|
85
90
|
const connection = await this.repo.findById(orgId, id);
|
|
86
|
-
if (!connection)
|
|
91
|
+
if (!connection) throw new NotFoundError("Connection", id);
|
|
87
92
|
const updated = await this.uow.run(async ({ connections, credentials, outbox }) => {
|
|
88
93
|
await credentials.update(orgId, connection.credentialRef, secrets);
|
|
89
94
|
const result = await connections.update(orgId, id, { updatedAt: /* @__PURE__ */ new Date() });
|
|
@@ -98,11 +103,14 @@ var IntegrationsServiceImpl = class {
|
|
|
98
103
|
connectionId: id,
|
|
99
104
|
integrationId: connection.integrationId
|
|
100
105
|
});
|
|
106
|
+
if (!updated) throw new NotFoundError("Connection", id);
|
|
101
107
|
return updated;
|
|
102
108
|
}
|
|
103
|
-
async configure(
|
|
109
|
+
async configure(id, input, actor) {
|
|
110
|
+
actor.permissions.require("integrations:write");
|
|
111
|
+
const orgId = actor.orgId;
|
|
104
112
|
const connection = await this.repo.findById(orgId, id);
|
|
105
|
-
if (!connection)
|
|
113
|
+
if (!connection) throw new NotFoundError("Connection", id);
|
|
106
114
|
if (input.config !== void 0) this.validate(connection.integrationId, input.config);
|
|
107
115
|
const updated = await this.uow.run(async ({ connections, outbox }) => {
|
|
108
116
|
const result = await connections.update(orgId, id, {
|
|
@@ -121,11 +129,14 @@ var IntegrationsServiceImpl = class {
|
|
|
121
129
|
connectionId: id,
|
|
122
130
|
integrationId: connection.integrationId
|
|
123
131
|
});
|
|
132
|
+
if (!updated) throw new NotFoundError("Connection", id);
|
|
124
133
|
return updated;
|
|
125
134
|
}
|
|
126
|
-
async disconnect(
|
|
135
|
+
async disconnect(id, actor) {
|
|
136
|
+
actor.permissions.require("integrations:delete");
|
|
137
|
+
const orgId = actor.orgId;
|
|
127
138
|
const connection = await this.repo.findById(orgId, id);
|
|
128
|
-
if (!connection)
|
|
139
|
+
if (!connection) throw new NotFoundError("Connection", id);
|
|
129
140
|
const deleted = await this.uow.run(async ({ connections, credentials, outbox }) => {
|
|
130
141
|
const ok = await connections.delete(orgId, id);
|
|
131
142
|
await credentials.destroy(orgId, connection.credentialRef);
|
|
@@ -140,18 +151,24 @@ var IntegrationsServiceImpl = class {
|
|
|
140
151
|
connectionId: id,
|
|
141
152
|
integrationId: connection.integrationId
|
|
142
153
|
});
|
|
143
|
-
|
|
154
|
+
if (!deleted) throw new NotFoundError("Connection", id);
|
|
144
155
|
}
|
|
145
|
-
list(
|
|
146
|
-
|
|
156
|
+
async list(actor) {
|
|
157
|
+
actor.permissions.require("integrations:read");
|
|
158
|
+
return this.repo.list(actor.orgId);
|
|
147
159
|
}
|
|
148
|
-
get(
|
|
149
|
-
|
|
160
|
+
async get(id, actor) {
|
|
161
|
+
actor.permissions.require("integrations:read");
|
|
162
|
+
const connection = await this.repo.findById(actor.orgId, id);
|
|
163
|
+
if (!connection) throw new NotFoundError("Connection", id);
|
|
164
|
+
return connection;
|
|
150
165
|
}
|
|
151
166
|
getByIntegration(orgId, integrationId) {
|
|
152
167
|
return this.repo.findByIntegration(orgId, integrationId);
|
|
153
168
|
}
|
|
154
|
-
async invoke(
|
|
169
|
+
async invoke(connectionId, actionId, input, actor) {
|
|
170
|
+
actor.permissions.require("integrations:write");
|
|
171
|
+
const orgId = actor.orgId;
|
|
155
172
|
const connection = await this.repo.findById(orgId, connectionId);
|
|
156
173
|
if (!connection) throw new NotFoundError("Connection", connectionId);
|
|
157
174
|
if (!connection.active) throw new ConflictError(`connection "${connectionId}" is inactive`);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"service.js","names":[],"sources":["../../src/integrations/service.ts"],"sourcesContent":["// integrations context — service implementation (inbound port). Owns the\n// connection lifecycle; credentials live in the shared secure credential store\n// (this service holds only the ref). The integrations the system supports are\n// declared at startup via the IntegrationsRegistry: connect/configure reject\n// unknown integration ids and validate config with the integration's own\n// validator. The domain never calls the external service — consumers take the\n// connection, reveal the credential at point of use, and build their own adapter.\n//\n// Mutations run inside the context's UnitOfWork: credential writes, connection\n// writes, and the outbox append commit in ONE transaction (transactional\n// outbox; this also closed the historical orphan-credential window). The\n// outbox relay — not this service — publishes to the EventBus.\nimport { genId } from '../shared/id.js';\nimport {\n ActionInvocationError,\n AlreadyConnectedError,\n InvalidConfigError,\n UnknownIntegrationError,\n type ConfigureInput,\n type ConnectInput,\n type Connection,\n} from './model.js';\nimport type {\n ConnectionsRepository,\n IntegrationsRegistry,\n IntegrationsService,\n IntegrationsUnitOfWork,\n} from './ports.js';\nimport type { CredentialStore, Logger } from '../shared/ports.js';\nimport { ConflictError, NotFoundError } from '../shared/errors.js';\nimport { noopLogger } from '../shared/logger.js';\nimport { integrationEvents } from './events.js';\n\nexport type IntegrationsServiceParams = {\n registry: IntegrationsRegistry;\n /** Read-only access (find/list) — runs outside any transaction. */\n repo: ConnectionsRepository;\n /** Atomic write scope: tx-bound connections repo + credential store + outbox. */\n uow: IntegrationsUnitOfWork;\n /** Point-of-use credential reveal for action invocation — reads only. */\n credentials: CredentialStore;\n logger?: Logger;\n};\n\nexport class IntegrationsServiceImpl implements IntegrationsService {\n private readonly registry: IntegrationsRegistry;\n private readonly repo: ConnectionsRepository;\n private readonly uow: IntegrationsUnitOfWork;\n private readonly credentials: CredentialStore;\n private readonly logger: Logger;\n\n constructor(params: IntegrationsServiceParams) {\n this.registry = params.registry;\n this.repo = params.repo;\n this.uow = params.uow;\n this.credentials = params.credentials;\n this.logger = params.logger ?? noopLogger;\n }\n\n available() {\n return this.registry.list().map((integration) => ({\n id: integration.id,\n configSchema: integration.configSchema(),\n secretsSchema: integration.secretsSchema(),\n actions: integration.actions.map((action) => ({\n id: action.id,\n description: action.description,\n inputSchema: action.inputSchema(),\n outputSchema: action.outputSchema(),\n })),\n }));\n }\n\n private validate(integrationId: string, config: Record<string, unknown>): void {\n const integration = this.registry.get(integrationId);\n if (!integration) {\n this.logger.warn('unknown integration rejected', { integrationId });\n throw new UnknownIntegrationError(integrationId);\n }\n const result = integration.validateConfig(config);\n if (!result.ok) {\n this.logger.warn('invalid integration config rejected', {\n integrationId,\n errors: result.errors,\n });\n throw new InvalidConfigError(integrationId, result.errors);\n }\n }\n\n async connect(orgId: string, input: ConnectInput): Promise<Connection> {\n const config = input.config ?? {};\n this.validate(input.integrationId, config);\n const existing = await this.repo.findByIntegration(orgId, input.integrationId);\n if (existing) {\n this.logger.warn('duplicate connection rejected', {\n orgId,\n integrationId: input.integrationId,\n });\n throw new AlreadyConnectedError(input.integrationId);\n }\n const connection = await this.uow.run(async ({ connections, credentials, outbox }) => {\n const credentialRef = await credentials.store(orgId, input.secrets);\n const at = new Date();\n const created = await connections.insert(orgId, {\n orgId,\n id: genId('connection'),\n integrationId: input.integrationId,\n config,\n active: true,\n credentialRef,\n createdAt: at,\n updatedAt: at,\n });\n await outbox.append([\n integrationEvents.connectionCreated.make({\n orgId,\n data: created,\n }),\n ]);\n return created;\n });\n this.logger.info('integration connected', {\n orgId,\n integrationId: connection.integrationId,\n connectionId: connection.id,\n });\n return connection;\n }\n\n async reconnect(\n orgId: string,\n id: string,\n secrets: Record<string, unknown>,\n ): Promise<Connection | null> {\n const connection = await this.repo.findById(orgId, id);\n if (!connection) {\n return null;\n }\n const updated = await this.uow.run(async ({ connections, credentials, outbox }) => {\n await credentials.update(orgId, connection.credentialRef, secrets);\n const result = await connections.update(orgId, id, { updatedAt: new Date() });\n if (result) {\n await outbox.append([integrationEvents.connectionUpdated.make({ orgId, data: result })]);\n }\n return result;\n });\n this.logger.info('integration credentials rotated', {\n orgId,\n connectionId: id,\n integrationId: connection.integrationId,\n });\n return updated;\n }\n\n async configure(orgId: string, id: string, input: ConfigureInput): Promise<Connection | null> {\n const connection = await this.repo.findById(orgId, id);\n if (!connection) {\n return null;\n }\n if (input.config !== undefined) {\n this.validate(connection.integrationId, input.config);\n }\n const updated = await this.uow.run(async ({ connections, outbox }) => {\n const result = await connections.update(orgId, id, {\n ...(input.config !== undefined ? { config: input.config } : {}),\n ...(input.active !== undefined ? { active: input.active } : {}),\n updatedAt: new Date(),\n });\n if (result) {\n await outbox.append([integrationEvents.connectionUpdated.make({ orgId, data: result })]);\n }\n return result;\n });\n this.logger.info('integration configured', {\n orgId,\n connectionId: id,\n integrationId: connection.integrationId,\n });\n return updated;\n }\n\n async disconnect(orgId: string, id: string): Promise<boolean> {\n const connection = await this.repo.findById(orgId, id);\n if (!connection) {\n return false;\n }\n const deleted = await this.uow.run(async ({ connections, credentials, outbox }) => {\n // Connection first: it holds the FK onto the credential row.\n const ok = await connections.delete(orgId, id);\n await credentials.destroy(orgId, connection.credentialRef);\n await outbox.append([integrationEvents.connectionRemoved.make({ orgId, data: connection })]);\n return ok;\n });\n this.logger.info('integration disconnected', {\n orgId,\n connectionId: id,\n integrationId: connection.integrationId,\n });\n return deleted;\n }\n\n list(orgId: string): Promise<Connection[]> {\n return this.repo.list(orgId);\n }\n\n get(orgId: string, id: string): Promise<Connection | null> {\n return this.repo.findById(orgId, id);\n }\n\n getByIntegration(orgId: string, integrationId: string): Promise<Connection | null> {\n return this.repo.findByIntegration(orgId, integrationId);\n }\n\n async invoke(\n orgId: string,\n connectionId: string,\n actionId: string,\n input: Record<string, unknown>,\n ): Promise<unknown> {\n const connection = await this.repo.findById(orgId, connectionId);\n if (!connection) {\n throw new NotFoundError('Connection', connectionId);\n }\n if (!connection.active) {\n throw new ConflictError(`connection \"${connectionId}\" is inactive`);\n }\n const integration = this.registry.get(connection.integrationId);\n if (!integration) {\n throw new UnknownIntegrationError(connection.integrationId);\n }\n const action = integration.actions.find((a) => a.id === actionId);\n if (!action) {\n throw new NotFoundError('Action', actionId);\n }\n const secrets = await this.credentials.reveal(orgId, connection.credentialRef);\n if (!secrets) {\n throw new NotFoundError('Credential', connectionId);\n }\n try {\n return await action.invoke({ secrets, config: connection.config }, input);\n } catch (err) {\n this.logger.warn('integration action failed', {\n orgId,\n connectionId,\n integrationId: connection.integrationId,\n actionId,\n err,\n });\n throw new ActionInvocationError(connection.integrationId, actionId, err);\n }\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"service.js","names":[],"sources":["../../src/integrations/service.ts"],"sourcesContent":["// integrations context — service implementation (inbound port). Owns the\n// connection lifecycle; credentials live in the shared secure credential store\n// (this service holds only the ref). The integrations the system supports are\n// declared at startup via the IntegrationsRegistry: connect/configure reject\n// unknown integration ids and validate config with the integration's own\n// validator. The domain never calls the external service — consumers take the\n// connection, reveal the credential at point of use, and build their own adapter.\n//\n// Mutations run inside the context's UnitOfWork: credential writes, connection\n// writes, and the outbox append commit in ONE transaction (transactional\n// outbox; this also closed the historical orphan-credential window). The\n// outbox relay — not this service — publishes to the EventBus.\nimport { genId } from '../shared/id.js';\nimport {\n ActionInvocationError,\n AlreadyConnectedError,\n InvalidConfigError,\n UnknownIntegrationError,\n type ConfigureInput,\n type ConnectInput,\n type Connection,\n} from './model.js';\nimport type {\n ConnectionsRepository,\n IntegrationsRegistry,\n IntegrationsService,\n IntegrationsUnitOfWork,\n} from './ports.js';\nimport type { CredentialStore, Logger } from '../shared/ports.js';\nimport type { Actor } from '../shared/actor.js';\nimport { ConflictError, NotFoundError } from '../shared/errors.js';\nimport { noopLogger } from '../shared/logger.js';\nimport { integrationEvents } from './events.js';\n\nexport type IntegrationsServiceParams = {\n registry: IntegrationsRegistry;\n /** Read-only access (find/list) — runs outside any transaction. */\n repo: ConnectionsRepository;\n /** Atomic write scope: tx-bound connections repo + credential store + outbox. */\n uow: IntegrationsUnitOfWork;\n /** Point-of-use credential reveal for action invocation — reads only. */\n credentials: CredentialStore;\n logger?: Logger;\n};\n\nexport class IntegrationsServiceImpl implements IntegrationsService {\n private readonly registry: IntegrationsRegistry;\n private readonly repo: ConnectionsRepository;\n private readonly uow: IntegrationsUnitOfWork;\n private readonly credentials: CredentialStore;\n private readonly logger: Logger;\n\n constructor(params: IntegrationsServiceParams) {\n this.registry = params.registry;\n this.repo = params.repo;\n this.uow = params.uow;\n this.credentials = params.credentials;\n this.logger = params.logger ?? noopLogger;\n }\n\n available(actor: Actor) {\n actor.permissions.require('integrations:read');\n return this.registry.list().map((integration) => ({\n id: integration.id,\n configSchema: integration.configSchema(),\n secretsSchema: integration.secretsSchema(),\n actions: integration.actions.map((action) => ({\n id: action.id,\n description: action.description,\n inputSchema: action.inputSchema(),\n outputSchema: action.outputSchema(),\n })),\n }));\n }\n\n private validate(integrationId: string, config: Record<string, unknown>): void {\n const integration = this.registry.get(integrationId);\n if (!integration) {\n this.logger.warn('unknown integration rejected', { integrationId });\n throw new UnknownIntegrationError(integrationId);\n }\n const result = integration.validateConfig(config);\n if (!result.ok) {\n this.logger.warn('invalid integration config rejected', {\n integrationId,\n errors: result.errors,\n });\n throw new InvalidConfigError(integrationId, result.errors);\n }\n }\n\n async connect(input: ConnectInput, actor: Actor): Promise<Connection> {\n actor.permissions.require('integrations:write');\n const orgId = actor.orgId;\n const config = input.config ?? {};\n this.validate(input.integrationId, config);\n const existing = await this.repo.findByIntegration(orgId, input.integrationId);\n if (existing) {\n this.logger.warn('duplicate connection rejected', {\n orgId,\n integrationId: input.integrationId,\n });\n throw new AlreadyConnectedError(input.integrationId);\n }\n const connection = await this.uow.run(async ({ connections, credentials, outbox }) => {\n const credentialRef = await credentials.store(orgId, input.secrets);\n const at = new Date();\n const created = await connections.insert(orgId, {\n orgId,\n id: genId('connection'),\n integrationId: input.integrationId,\n config,\n active: true,\n credentialRef,\n createdAt: at,\n updatedAt: at,\n });\n await outbox.append([\n integrationEvents.connectionCreated.make({\n orgId,\n data: created,\n }),\n ]);\n return created;\n });\n this.logger.info('integration connected', {\n orgId,\n integrationId: connection.integrationId,\n connectionId: connection.id,\n });\n return connection;\n }\n\n async reconnect(id: string, secrets: Record<string, unknown>, actor: Actor): Promise<Connection> {\n actor.permissions.require('integrations:write');\n const orgId = actor.orgId;\n const connection = await this.repo.findById(orgId, id);\n if (!connection) {\n throw new NotFoundError('Connection', id);\n }\n const updated = await this.uow.run(async ({ connections, credentials, outbox }) => {\n await credentials.update(orgId, connection.credentialRef, secrets);\n const result = await connections.update(orgId, id, { updatedAt: new Date() });\n if (result) {\n await outbox.append([integrationEvents.connectionUpdated.make({ orgId, data: result })]);\n }\n return result;\n });\n this.logger.info('integration credentials rotated', {\n orgId,\n connectionId: id,\n integrationId: connection.integrationId,\n });\n if (!updated) {\n throw new NotFoundError('Connection', id);\n }\n return updated;\n }\n\n async configure(id: string, input: ConfigureInput, actor: Actor): Promise<Connection> {\n actor.permissions.require('integrations:write');\n const orgId = actor.orgId;\n const connection = await this.repo.findById(orgId, id);\n if (!connection) {\n throw new NotFoundError('Connection', id);\n }\n if (input.config !== undefined) {\n this.validate(connection.integrationId, input.config);\n }\n const updated = await this.uow.run(async ({ connections, outbox }) => {\n const result = await connections.update(orgId, id, {\n ...(input.config !== undefined ? { config: input.config } : {}),\n ...(input.active !== undefined ? { active: input.active } : {}),\n updatedAt: new Date(),\n });\n if (result) {\n await outbox.append([integrationEvents.connectionUpdated.make({ orgId, data: result })]);\n }\n return result;\n });\n this.logger.info('integration configured', {\n orgId,\n connectionId: id,\n integrationId: connection.integrationId,\n });\n if (!updated) {\n throw new NotFoundError('Connection', id);\n }\n return updated;\n }\n\n async disconnect(id: string, actor: Actor): Promise<void> {\n actor.permissions.require('integrations:delete');\n const orgId = actor.orgId;\n const connection = await this.repo.findById(orgId, id);\n if (!connection) {\n throw new NotFoundError('Connection', id);\n }\n const deleted = await this.uow.run(async ({ connections, credentials, outbox }) => {\n // Connection first: it holds the FK onto the credential row.\n const ok = await connections.delete(orgId, id);\n await credentials.destroy(orgId, connection.credentialRef);\n await outbox.append([integrationEvents.connectionRemoved.make({ orgId, data: connection })]);\n return ok;\n });\n this.logger.info('integration disconnected', {\n orgId,\n connectionId: id,\n integrationId: connection.integrationId,\n });\n if (!deleted) {\n throw new NotFoundError('Connection', id);\n }\n }\n\n async list(actor: Actor): Promise<Connection[]> {\n actor.permissions.require('integrations:read');\n return this.repo.list(actor.orgId);\n }\n\n async get(id: string, actor: Actor): Promise<Connection> {\n actor.permissions.require('integrations:read');\n const connection = await this.repo.findById(actor.orgId, id);\n if (!connection) {\n throw new NotFoundError('Connection', id);\n }\n return connection;\n }\n\n getByIntegration(orgId: string, integrationId: string): Promise<Connection | null> {\n return this.repo.findByIntegration(orgId, integrationId);\n }\n\n async invoke(\n connectionId: string,\n actionId: string,\n input: Record<string, unknown>,\n actor: Actor,\n ): Promise<unknown> {\n actor.permissions.require('integrations:write');\n const orgId = actor.orgId;\n const connection = await this.repo.findById(orgId, connectionId);\n if (!connection) {\n throw new NotFoundError('Connection', connectionId);\n }\n if (!connection.active) {\n throw new ConflictError(`connection \"${connectionId}\" is inactive`);\n }\n const integration = this.registry.get(connection.integrationId);\n if (!integration) {\n throw new UnknownIntegrationError(connection.integrationId);\n }\n const action = integration.actions.find((a) => a.id === actionId);\n if (!action) {\n throw new NotFoundError('Action', actionId);\n }\n const secrets = await this.credentials.reveal(orgId, connection.credentialRef);\n if (!secrets) {\n throw new NotFoundError('Credential', connectionId);\n }\n try {\n return await action.invoke({ secrets, config: connection.config }, input);\n } catch (err) {\n this.logger.warn('integration action failed', {\n orgId,\n connectionId,\n integrationId: connection.integrationId,\n actionId,\n err,\n });\n throw new ActionInvocationError(connection.integrationId, actionId, err);\n }\n }\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { Level, Permission } from "../types/schemas/organizations.js";
|
|
2
|
+
import "../types/index.js";
|
|
3
|
+
//#region src/organizations/default-roles.d.ts
|
|
4
|
+
interface RoleDefinition {
|
|
5
|
+
key: string;
|
|
6
|
+
name: string;
|
|
7
|
+
description: string;
|
|
8
|
+
levels: Level[];
|
|
9
|
+
team: boolean;
|
|
10
|
+
permissions: Permission[];
|
|
11
|
+
}
|
|
12
|
+
declare const OWNER_ROLE_KEY = "owner";
|
|
13
|
+
declare const MEMBER_ROLE_KEY = "member";
|
|
14
|
+
declare const DEFAULT_ROLES: readonly RoleDefinition[];
|
|
15
|
+
declare class RoleSetError extends Error {
|
|
16
|
+
constructor(message: string);
|
|
17
|
+
}
|
|
18
|
+
declare function validateRoleSet(roles: readonly RoleDefinition[]): void;
|
|
19
|
+
declare function defaultHolders(permission: Permission): string[];
|
|
20
|
+
//#endregion
|
|
21
|
+
export { DEFAULT_ROLES, MEMBER_ROLE_KEY, OWNER_ROLE_KEY, RoleDefinition, RoleSetError, defaultHolders, validateRoleSet };
|
|
22
|
+
//# sourceMappingURL=default-roles.d.ts.map
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { levelSchema } from "../types/schemas/organizations.js";
|
|
2
|
+
import { ALL_PERMISSIONS, fitsLevels, isPermission, withImplied } from "./permissions.js";
|
|
3
|
+
//#region src/organizations/default-roles.ts
|
|
4
|
+
const OWNER_ROLE_KEY = "owner";
|
|
5
|
+
const MEMBER_ROLE_KEY = "member";
|
|
6
|
+
const ADMIN_PERMISSIONS = [...ALL_PERMISSIONS];
|
|
7
|
+
const DEFAULT_ROLES = [
|
|
8
|
+
{
|
|
9
|
+
key: OWNER_ROLE_KEY,
|
|
10
|
+
name: "Owner",
|
|
11
|
+
description: "Owns the organization and holds every permission.",
|
|
12
|
+
levels: ["org"],
|
|
13
|
+
team: true,
|
|
14
|
+
permissions: [...ALL_PERMISSIONS]
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
key: "admin",
|
|
18
|
+
name: "Admin",
|
|
19
|
+
description: "Runs the organization: content, people, access, automations and settings.",
|
|
20
|
+
levels: ["org"],
|
|
21
|
+
team: true,
|
|
22
|
+
permissions: ADMIN_PERMISSIONS
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
key: "instructor",
|
|
26
|
+
name: "Instructor",
|
|
27
|
+
description: "Teaches courses: edits and publishes them, moderates lesson comments and sees learners.",
|
|
28
|
+
levels: ["org", "course"],
|
|
29
|
+
team: true,
|
|
30
|
+
permissions: [
|
|
31
|
+
"courses:read",
|
|
32
|
+
"courses:write",
|
|
33
|
+
"courses:moderate",
|
|
34
|
+
"progress:read",
|
|
35
|
+
"api_keys:read",
|
|
36
|
+
"api_keys:write",
|
|
37
|
+
"api_keys:delete"
|
|
38
|
+
]
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
key: "moderator",
|
|
42
|
+
name: "Moderator",
|
|
43
|
+
description: "Moderates communities: posts, comments, chats, reports and space members.",
|
|
44
|
+
levels: ["org", "community"],
|
|
45
|
+
team: true,
|
|
46
|
+
permissions: ["communities:moderate"]
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
key: MEMBER_ROLE_KEY,
|
|
50
|
+
name: "Member",
|
|
51
|
+
description: "Everyone who was not given another role.",
|
|
52
|
+
levels: ["org"],
|
|
53
|
+
team: false,
|
|
54
|
+
permissions: []
|
|
55
|
+
}
|
|
56
|
+
];
|
|
57
|
+
var RoleSetError = class extends Error {
|
|
58
|
+
constructor(message) {
|
|
59
|
+
super(message);
|
|
60
|
+
this.name = "RoleSetError";
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
function validateRoleSet(roles) {
|
|
64
|
+
const keys = /* @__PURE__ */ new Set();
|
|
65
|
+
for (const role of roles) {
|
|
66
|
+
if (keys.has(role.key)) throw new RoleSetError(`role key ${role.key} is repeated`);
|
|
67
|
+
keys.add(role.key);
|
|
68
|
+
if (role.levels.length === 0) throw new RoleSetError(`role ${role.key} has no level`);
|
|
69
|
+
for (const level of role.levels) if (!levelSchema.safeParse(level).success) throw new RoleSetError(`role ${role.key} names an unknown level ${String(level)}`);
|
|
70
|
+
for (const permission of role.permissions) {
|
|
71
|
+
if (!isPermission(permission)) throw new RoleSetError(`role ${role.key} names an unknown permission ${String(permission)}`);
|
|
72
|
+
if (!fitsLevels(permission, role.levels)) throw new RoleSetError(`role ${role.key} holds ${permission}, which doesn't fit its levels ${role.levels.join(", ")}`);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
const owner = roles.find((role) => role.key === OWNER_ROLE_KEY);
|
|
76
|
+
const member = roles.find((role) => role.key === MEMBER_ROLE_KEY);
|
|
77
|
+
if (!owner) throw new RoleSetError(`the role set lacks ${OWNER_ROLE_KEY}`);
|
|
78
|
+
if (!member) throw new RoleSetError(`the role set lacks ${MEMBER_ROLE_KEY}`);
|
|
79
|
+
if (owner.levels.length !== 1 || owner.levels[0] !== "org") throw new RoleSetError(`${OWNER_ROLE_KEY} must list the org only`);
|
|
80
|
+
if (owner.permissions.length !== ALL_PERMISSIONS.length) throw new RoleSetError(`${OWNER_ROLE_KEY} must hold every permission`);
|
|
81
|
+
if (member.levels.length !== 1 || member.levels[0] !== "org" || member.permissions.length > 0) throw new RoleSetError(`${MEMBER_ROLE_KEY} must list the org only and hold nothing`);
|
|
82
|
+
}
|
|
83
|
+
function defaultHolders(permission) {
|
|
84
|
+
return DEFAULT_ROLES.filter((role) => withImplied(role.permissions).has(permission)).map((role) => role.name);
|
|
85
|
+
}
|
|
86
|
+
//#endregion
|
|
87
|
+
export { DEFAULT_ROLES, MEMBER_ROLE_KEY, OWNER_ROLE_KEY, RoleSetError, defaultHolders, validateRoleSet };
|
|
88
|
+
|
|
89
|
+
//# sourceMappingURL=default-roles.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"default-roles.js","names":[],"sources":["../../src/organizations/default-roles.ts"],"sourcesContent":["import { levelSchema } from '../types/schemas/organizations.js';\nimport type { Level, Permission } from '../types/index.js';\nimport { ALL_PERMISSIONS, fitsLevels, isPermission, withImplied } from './permissions.js';\n\nexport interface RoleDefinition {\n key: string;\n name: string;\n description: string;\n levels: Level[];\n team: boolean;\n permissions: Permission[];\n}\n\nexport const OWNER_ROLE_KEY = 'owner';\nexport const MEMBER_ROLE_KEY = 'member';\n\nconst ADMIN_PERMISSIONS: Permission[] = [...ALL_PERMISSIONS];\n\nexport const DEFAULT_ROLES: readonly RoleDefinition[] = [\n {\n key: OWNER_ROLE_KEY,\n name: 'Owner',\n description: 'Owns the organization and holds every permission.',\n levels: ['org'],\n team: true,\n permissions: [...ALL_PERMISSIONS],\n },\n {\n key: 'admin',\n name: 'Admin',\n description: 'Runs the organization: content, people, access, automations and settings.',\n levels: ['org'],\n team: true,\n permissions: ADMIN_PERMISSIONS,\n },\n {\n key: 'instructor',\n name: 'Instructor',\n description:\n 'Teaches courses: edits and publishes them, moderates lesson comments and sees learners.',\n levels: ['org', 'course'],\n team: true,\n permissions: [\n 'courses:read',\n 'courses:write',\n 'courses:moderate',\n 'progress:read',\n 'api_keys:read',\n 'api_keys:write',\n 'api_keys:delete',\n ],\n },\n {\n key: 'moderator',\n name: 'Moderator',\n description: 'Moderates communities: posts, comments, chats, reports and space members.',\n levels: ['org', 'community'],\n team: true,\n permissions: ['communities:moderate'],\n },\n {\n key: MEMBER_ROLE_KEY,\n name: 'Member',\n description: 'Everyone who was not given another role.',\n levels: ['org'],\n team: false,\n permissions: [],\n },\n];\n\nexport class RoleSetError extends Error {\n constructor(message: string) {\n super(message);\n this.name = 'RoleSetError';\n }\n}\n\nexport function validateRoleSet(roles: readonly RoleDefinition[]): void {\n const keys = new Set<string>();\n for (const role of roles) {\n if (keys.has(role.key)) {\n throw new RoleSetError(`role key ${role.key} is repeated`);\n }\n keys.add(role.key);\n if (role.levels.length === 0) {\n throw new RoleSetError(`role ${role.key} has no level`);\n }\n for (const level of role.levels) {\n if (!levelSchema.safeParse(level).success) {\n throw new RoleSetError(`role ${role.key} names an unknown level ${String(level)}`);\n }\n }\n for (const permission of role.permissions) {\n if (!isPermission(permission)) {\n throw new RoleSetError(\n `role ${role.key} names an unknown permission ${String(permission)}`,\n );\n }\n if (!fitsLevels(permission, role.levels)) {\n throw new RoleSetError(\n `role ${role.key} holds ${permission}, which doesn't fit its levels ${role.levels.join(', ')}`,\n );\n }\n }\n }\n const owner = roles.find((role) => role.key === OWNER_ROLE_KEY);\n const member = roles.find((role) => role.key === MEMBER_ROLE_KEY);\n if (!owner) {\n throw new RoleSetError(`the role set lacks ${OWNER_ROLE_KEY}`);\n }\n if (!member) {\n throw new RoleSetError(`the role set lacks ${MEMBER_ROLE_KEY}`);\n }\n if (owner.levels.length !== 1 || owner.levels[0] !== 'org') {\n throw new RoleSetError(`${OWNER_ROLE_KEY} must list the org only`);\n }\n if (owner.permissions.length !== ALL_PERMISSIONS.length) {\n throw new RoleSetError(`${OWNER_ROLE_KEY} must hold every permission`);\n }\n if (member.levels.length !== 1 || member.levels[0] !== 'org' || member.permissions.length > 0) {\n throw new RoleSetError(`${MEMBER_ROLE_KEY} must list the org only and hold nothing`);\n }\n}\n\nexport function defaultHolders(permission: Permission): string[] {\n return DEFAULT_ROLES.filter((role) => withImplied(role.permissions).has(permission)).map(\n (role) => role.name,\n );\n}\n"],"mappings":";;;AAaA,MAAa,iBAAiB;AAC9B,MAAa,kBAAkB;AAE/B,MAAM,oBAAkC,CAAC,GAAG,eAAe;AAE3D,MAAa,gBAA2C;CACtD;EACE,KAAK;EACL,MAAM;EACN,aAAa;EACb,QAAQ,CAAC,KAAK;EACd,MAAM;EACN,aAAa,CAAC,GAAG,eAAe;CAClC;CACA;EACE,KAAK;EACL,MAAM;EACN,aAAa;EACb,QAAQ,CAAC,KAAK;EACd,MAAM;EACN,aAAa;CACf;CACA;EACE,KAAK;EACL,MAAM;EACN,aACE;EACF,QAAQ,CAAC,OAAO,QAAQ;EACxB,MAAM;EACN,aAAa;GACX;GACA;GACA;GACA;GACA;GACA;GACA;EACF;CACF;CACA;EACE,KAAK;EACL,MAAM;EACN,aAAa;EACb,QAAQ,CAAC,OAAO,WAAW;EAC3B,MAAM;EACN,aAAa,CAAC,sBAAsB;CACtC;CACA;EACE,KAAK;EACL,MAAM;EACN,aAAa;EACb,QAAQ,CAAC,KAAK;EACd,MAAM;EACN,aAAa,CAAC;CAChB;AACF;AAEA,IAAa,eAAb,cAAkC,MAAM;CACtC,YAAY,SAAiB;EAC3B,MAAM,OAAO;EACb,KAAK,OAAO;CACd;AACF;AAEA,SAAgB,gBAAgB,OAAwC;CACtE,MAAM,uBAAO,IAAI,IAAY;CAC7B,KAAK,MAAM,QAAQ,OAAO;EACxB,IAAI,KAAK,IAAI,KAAK,GAAG,GACnB,MAAM,IAAI,aAAa,YAAY,KAAK,IAAI,aAAa;EAE3D,KAAK,IAAI,KAAK,GAAG;EACjB,IAAI,KAAK,OAAO,WAAW,GACzB,MAAM,IAAI,aAAa,QAAQ,KAAK,IAAI,cAAc;EAExD,KAAK,MAAM,SAAS,KAAK,QACvB,IAAI,CAAC,YAAY,UAAU,KAAK,CAAC,CAAC,SAChC,MAAM,IAAI,aAAa,QAAQ,KAAK,IAAI,0BAA0B,OAAO,KAAK,GAAG;EAGrF,KAAK,MAAM,cAAc,KAAK,aAAa;GACzC,IAAI,CAAC,aAAa,UAAU,GAC1B,MAAM,IAAI,aACR,QAAQ,KAAK,IAAI,+BAA+B,OAAO,UAAU,GACnE;GAEF,IAAI,CAAC,WAAW,YAAY,KAAK,MAAM,GACrC,MAAM,IAAI,aACR,QAAQ,KAAK,IAAI,SAAS,WAAW,iCAAiC,KAAK,OAAO,KAAK,IAAI,GAC7F;EAEJ;CACF;CACA,MAAM,QAAQ,MAAM,MAAM,SAAS,KAAK,QAAQ,cAAc;CAC9D,MAAM,SAAS,MAAM,MAAM,SAAS,KAAK,QAAQ,eAAe;CAChE,IAAI,CAAC,OACH,MAAM,IAAI,aAAa,sBAAsB,gBAAgB;CAE/D,IAAI,CAAC,QACH,MAAM,IAAI,aAAa,sBAAsB,iBAAiB;CAEhE,IAAI,MAAM,OAAO,WAAW,KAAK,MAAM,OAAO,OAAO,OACnD,MAAM,IAAI,aAAa,GAAG,eAAe,wBAAwB;CAEnE,IAAI,MAAM,YAAY,WAAW,gBAAgB,QAC/C,MAAM,IAAI,aAAa,GAAG,eAAe,4BAA4B;CAEvE,IAAI,OAAO,OAAO,WAAW,KAAK,OAAO,OAAO,OAAO,SAAS,OAAO,YAAY,SAAS,GAC1F,MAAM,IAAI,aAAa,GAAG,gBAAgB,yCAAyC;AAEvF;AAEA,SAAgB,eAAe,YAAkC;CAC/D,OAAO,cAAc,QAAQ,SAAS,YAAY,KAAK,WAAW,CAAC,CAAC,IAAI,UAAU,CAAC,CAAC,CAAC,KAClF,SAAS,KAAK,IACjB;AACF"}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { ContentType } from "../types/schemas/content.js";
|
|
2
|
+
import { Permission } from "../types/schemas/organizations.js";
|
|
3
|
+
import { ContentId } from "../types/organizations.js";
|
|
4
|
+
import "../types/index.js";
|
|
5
|
+
import { EffectivePermissions, RoleRef } from "../shared/actor.js";
|
|
6
|
+
import { OrgRole } from "./org-roles.js";
|
|
7
|
+
//#region src/organizations/effective-permissions.d.ts
|
|
8
|
+
interface AssignmentRow {
|
|
9
|
+
roleKey: string;
|
|
10
|
+
contentId: ContentId | null;
|
|
11
|
+
contentType: ContentType | null;
|
|
12
|
+
}
|
|
13
|
+
interface ResolveInput {
|
|
14
|
+
assignments: readonly AssignmentRow[];
|
|
15
|
+
roles: ReadonlyMap<string, OrgRole>;
|
|
16
|
+
tokenScopes?: readonly Permission[];
|
|
17
|
+
}
|
|
18
|
+
interface ContentEntry {
|
|
19
|
+
permissions: Set<Permission>;
|
|
20
|
+
role: RoleRef;
|
|
21
|
+
}
|
|
22
|
+
declare class ResolvedPermissions implements EffectivePermissions {
|
|
23
|
+
private readonly org;
|
|
24
|
+
private readonly orgRole;
|
|
25
|
+
private readonly content;
|
|
26
|
+
private readonly owner;
|
|
27
|
+
private readonly token;
|
|
28
|
+
constructor(org: Set<Permission>, orgRole: RoleRef, content: Map<ContentId, ContentEntry>, owner: boolean, token: boolean);
|
|
29
|
+
has(permission: Permission, on?: ContentId): boolean;
|
|
30
|
+
require(permission: Permission, on?: ContentId): void;
|
|
31
|
+
anywhere(permission: Permission): boolean;
|
|
32
|
+
where(permission: Permission): {
|
|
33
|
+
org: boolean;
|
|
34
|
+
content: ContentId[];
|
|
35
|
+
};
|
|
36
|
+
roleOn(on?: ContentId): RoleRef;
|
|
37
|
+
isOwner(): boolean;
|
|
38
|
+
isToken(): boolean;
|
|
39
|
+
orgPermissions(): Permission[];
|
|
40
|
+
contentPermissions(): Record<ContentId, Permission[]>;
|
|
41
|
+
teamOn(on?: ContentId): boolean;
|
|
42
|
+
}
|
|
43
|
+
declare function resolvePermissions({ assignments, roles, tokenScopes }: ResolveInput): ResolvedPermissions;
|
|
44
|
+
declare function systemPermissions(): ResolvedPermissions;
|
|
45
|
+
//#endregion
|
|
46
|
+
export { AssignmentRow, ResolveInput, ResolvedPermissions, resolvePermissions, systemPermissions };
|
|
47
|
+
//# sourceMappingURL=effective-permissions.d.ts.map
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { ForbiddenError } from "../shared/errors.js";
|
|
2
|
+
import { ALL_PERMISSIONS, permissionAbout, withImplied } from "./permissions.js";
|
|
3
|
+
import { MEMBER_ROLE_KEY } from "./default-roles.js";
|
|
4
|
+
//#region src/organizations/effective-permissions.ts
|
|
5
|
+
const MEMBER_REF = {
|
|
6
|
+
key: MEMBER_ROLE_KEY,
|
|
7
|
+
name: "Member",
|
|
8
|
+
team: false
|
|
9
|
+
};
|
|
10
|
+
function refOf(role) {
|
|
11
|
+
return {
|
|
12
|
+
key: role.key,
|
|
13
|
+
name: role.name,
|
|
14
|
+
team: role.team
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
function intersect(set, scopes) {
|
|
18
|
+
if (!scopes) return set;
|
|
19
|
+
return new Set([...set].filter((permission) => scopes.has(permission)));
|
|
20
|
+
}
|
|
21
|
+
var ResolvedPermissions = class {
|
|
22
|
+
org;
|
|
23
|
+
orgRole;
|
|
24
|
+
content;
|
|
25
|
+
owner;
|
|
26
|
+
token;
|
|
27
|
+
constructor(org, orgRole, content, owner, token) {
|
|
28
|
+
this.org = org;
|
|
29
|
+
this.orgRole = orgRole;
|
|
30
|
+
this.content = content;
|
|
31
|
+
this.owner = owner;
|
|
32
|
+
this.token = token;
|
|
33
|
+
}
|
|
34
|
+
has(permission, on) {
|
|
35
|
+
if (this.org.has(permission)) return true;
|
|
36
|
+
return on !== void 0 && (this.content.get(on)?.permissions.has(permission) ?? false);
|
|
37
|
+
}
|
|
38
|
+
require(permission, on) {
|
|
39
|
+
if (!this.has(permission, on)) throw Object.assign(new ForbiddenError(`You need ${permission}`), {
|
|
40
|
+
permission,
|
|
41
|
+
...on !== void 0 && { contentId: on }
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
anywhere(permission) {
|
|
45
|
+
if (this.org.has(permission)) return true;
|
|
46
|
+
for (const entry of this.content.values()) if (entry.permissions.has(permission)) return true;
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
where(permission) {
|
|
50
|
+
if (this.org.has(permission)) return {
|
|
51
|
+
org: true,
|
|
52
|
+
content: []
|
|
53
|
+
};
|
|
54
|
+
const content = [];
|
|
55
|
+
for (const [id, entry] of this.content) if (entry.permissions.has(permission)) content.push(id);
|
|
56
|
+
return {
|
|
57
|
+
org: false,
|
|
58
|
+
content
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
roleOn(on) {
|
|
62
|
+
if (on !== void 0) {
|
|
63
|
+
const entry = this.content.get(on);
|
|
64
|
+
if (entry) return entry.role;
|
|
65
|
+
}
|
|
66
|
+
return this.orgRole;
|
|
67
|
+
}
|
|
68
|
+
isOwner() {
|
|
69
|
+
return this.owner;
|
|
70
|
+
}
|
|
71
|
+
isToken() {
|
|
72
|
+
return this.token;
|
|
73
|
+
}
|
|
74
|
+
orgPermissions() {
|
|
75
|
+
return ALL_PERMISSIONS.filter((permission) => this.org.has(permission));
|
|
76
|
+
}
|
|
77
|
+
contentPermissions() {
|
|
78
|
+
const out = {};
|
|
79
|
+
for (const [id, entry] of this.content) out[id] = ALL_PERMISSIONS.filter((permission) => entry.permissions.has(permission));
|
|
80
|
+
return out;
|
|
81
|
+
}
|
|
82
|
+
teamOn(on) {
|
|
83
|
+
if (on !== void 0 && this.content.get(on)?.role.team) return true;
|
|
84
|
+
return this.orgRole.team;
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
function resolvePermissions({ assignments, roles, tokenScopes }) {
|
|
88
|
+
const scopes = tokenScopes ? withImplied(tokenScopes) : null;
|
|
89
|
+
const orgRow = assignments.find((row) => row.contentId === null);
|
|
90
|
+
const listed = orgRow ? roles.get(orgRow.roleKey) : void 0;
|
|
91
|
+
const orgRole = listed?.levels.includes("org") ? listed : void 0;
|
|
92
|
+
const orgSet = orgRole ? withImplied(orgRole.permissions) : /* @__PURE__ */ new Set();
|
|
93
|
+
const content = /* @__PURE__ */ new Map();
|
|
94
|
+
for (const row of assignments) {
|
|
95
|
+
if (row.contentId === null || row.contentType === null) continue;
|
|
96
|
+
const role = roles.get(row.roleKey);
|
|
97
|
+
if (!role || !role.levels.includes(row.contentType)) continue;
|
|
98
|
+
const type = row.contentType;
|
|
99
|
+
const expanded = [...withImplied(role.permissions.filter((permission) => permissionAbout(permission) === type))].filter((permission) => permissionAbout(permission) === type);
|
|
100
|
+
content.set(row.contentId, {
|
|
101
|
+
permissions: intersect(new Set(expanded), scopes),
|
|
102
|
+
role: refOf(role)
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
const owner = orgRow?.roleKey === "owner" && orgRole !== void 0;
|
|
106
|
+
return new ResolvedPermissions(intersect(orgSet, scopes), orgRole ? refOf(orgRole) : roles.get("member") ? refOf(roles.get(MEMBER_ROLE_KEY)) : MEMBER_REF, content, owner && scopes === null, scopes !== null);
|
|
107
|
+
}
|
|
108
|
+
function systemPermissions() {
|
|
109
|
+
return new ResolvedPermissions(new Set(ALL_PERMISSIONS), {
|
|
110
|
+
key: "system",
|
|
111
|
+
name: "System",
|
|
112
|
+
team: false
|
|
113
|
+
}, /* @__PURE__ */ new Map(), false, false);
|
|
114
|
+
}
|
|
115
|
+
//#endregion
|
|
116
|
+
export { ResolvedPermissions, resolvePermissions, systemPermissions };
|
|
117
|
+
|
|
118
|
+
//# sourceMappingURL=effective-permissions.js.map
|