Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion apps/sim/lib/admin/invitation-operation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import {
import {
DIRECT_GRANT_EMAIL_EVENT_TYPE,
type DirectGrantEmailPayload,
} from '@/lib/invitations/direct-grant'
} from '@/lib/invitations/direct-grant-event'
import { MAX_INVITE_EMAILS, MAX_INVITE_WORKSPACES } from '@/lib/invitations/limits'

export const ADMIN_INVITATION_OPERATION_EVENT_TYPE = 'admin.organization-invitation-operation'
Expand Down
2 changes: 1 addition & 1 deletion apps/sim/lib/billing/enterprise-provisioning.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ import {
outboxEventHasSourceOperationId,
} from '@/lib/core/outbox/service'
import type { DbOrTx } from '@/lib/db/types'
import { DIRECT_GRANT_EMAIL_EVENT_TYPE } from '@/lib/invitations/direct-grant'
import { DIRECT_GRANT_EMAIL_EVENT_TYPE } from '@/lib/invitations/direct-grant-event'
import { MAX_INVITE_EMAILS, MAX_INVITE_WORKSPACES } from '@/lib/invitations/limits'
import { sendInvitationEmail } from '@/lib/invitations/send'
import {
Expand Down
4 changes: 3 additions & 1 deletion apps/sim/lib/invitations/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ import { isOrgAdminRole, PERMISSION_RANK, type PermissionType } from '@sim/platf
import { generateId } from '@sim/utils/id'
import { normalizeEmail } from '@sim/utils/string'
import { and, asc, count, eq, inArray, lte, sql } from 'drizzle-orm'
import { setActiveOrganizationForCurrentSession } from '@/lib/auth/active-organization'
import { applySessionPolicyToNewMember } from '@/lib/auth/session-policy'
import { getOrganizationSubscription } from '@/lib/billing/core/billing'
import { getHighestPriorityPersonalSubscription } from '@/lib/billing/core/plan'
Expand Down Expand Up @@ -1435,6 +1434,9 @@ async function runInvitationAcceptancePostCommitEffects(

if (effects.organizationId) {
try {
const { setActiveOrganizationForCurrentSession } = await import(
'@/lib/auth/active-organization'
)
await setActiveOrganizationForCurrentSession(effects.organizationId)
} catch (activeOrgError) {
logger.error('Failed to activate organization after accepting invitation', {
Expand Down
9 changes: 9 additions & 0 deletions apps/sim/lib/invitations/direct-grant-event.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
export const DIRECT_GRANT_EMAIL_EVENT_TYPE = 'invitation.send-workspace-added'

export interface DirectGrantEmailPayload {
email: string
inviterName: string
workspaceId: string
workspaceName: string
sourceOperationId?: string
}
2 changes: 1 addition & 1 deletion apps/sim/lib/invitations/direct-grant.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,11 +78,11 @@ vi.mock('@/lib/posthog/server', () => ({
}))

import {
DIRECT_GRANT_EMAIL_EVENT_TYPE,
DirectGrantContextChangedError,
directGrantOutboxHandlers,
grantWorkspaceAccessDirectly,
} from '@/lib/invitations/direct-grant'
import { DIRECT_GRANT_EMAIL_EVENT_TYPE } from '@/lib/invitations/direct-grant-event'

const baseInput = {
userId: 'user-2',
Expand Down
14 changes: 4 additions & 10 deletions apps/sim/lib/invitations/direct-grant.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@ import { PlatformEvents } from '@/lib/core/telemetry'
import { syncWorkspaceEnvCredentials } from '@/lib/credentials/environment'
import type { DbOrTx } from '@/lib/db/types'
import { revokeInvitationWorkspaceGrantTx } from '@/lib/invitations/core'
import {
DIRECT_GRANT_EMAIL_EVENT_TYPE,
type DirectGrantEmailPayload,
} from '@/lib/invitations/direct-grant-event'
import { acquireInvitationMutationLocks } from '@/lib/invitations/locks'
import { sendWorkspaceAddedEmail } from '@/lib/invitations/send'
import { captureServerEvent } from '@/lib/posthog/server'
Expand All @@ -34,16 +38,6 @@ import {

const logger = createLogger('InvitationDirectGrant')

export const DIRECT_GRANT_EMAIL_EVENT_TYPE = 'invitation.send-workspace-added'

export interface DirectGrantEmailPayload {
email: string
inviterName: string
workspaceId: string
workspaceName: string
sourceOperationId?: string
}

export type DirectGrantOutcome =
| { outcome: 'added'; permission: PermissionType }
| { outcome: 'updated'; permission: PermissionType; previousPermission: PermissionType }
Expand Down
90 changes: 90 additions & 0 deletions apps/sim/lib/invitations/import-boundary.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
/**
* @vitest-environment node
*/
import { existsSync, readFileSync, statSync } from 'node:fs'
import { dirname, join, relative, resolve } from 'node:path'
import { fileURLToPath } from 'node:url'
import { describe, expect, it } from 'vitest'

const APP_DIR = resolve(dirname(fileURLToPath(import.meta.url)), '../..')
const DIRECT_GRANT_ENTRY = join(APP_DIR, 'lib/invitations/direct-grant.ts')
const DIRECT_GRANT_EVENT = join(APP_DIR, 'lib/invitations/direct-grant-event.ts')
const AUTH_ENTRY = join(APP_DIR, 'lib/auth/auth.ts')
const EXTENSIONS = ['.ts', '.tsx', '.js', '.jsx', '.mjs'] as const
const IMPORT_PATTERN = /(?:^|\n)\s*import\s+(?!type\b)(?:[\s\S]*?from\s*)?['"]([^'"]+)['"]/g
const REEXPORT_PATTERN =
/(?:^|\n)\s*export\s+(?!type\b)(?:\*(?:\s+as\s+[\w$]+)?|\{[\s\S]*?\})\s*from\s*['"]([^'"]+)['"]/g

function resolveStaticSpecifier(specifier: string, importer: string): string | null {
let base: string
if (specifier.startsWith('@/')) base = join(APP_DIR, specifier.slice(2))
else if (specifier.startsWith('.')) base = resolve(dirname(importer), specifier)
else return null

if (existsSync(base) && statSync(base).isFile()) return base
for (const extension of EXTENSIONS) {
if (existsSync(base + extension)) return base + extension
}
if (existsSync(base) && statSync(base).isDirectory()) {
for (const extension of EXTENSIONS) {
const indexPath = join(base, `index${extension}`)
if (existsSync(indexPath)) return indexPath
}
}
return null
}

function getStaticDependencies(file: string): string[] {
const source = readFileSync(file, 'utf8')
const dependencies = new Set<string>()

for (const pattern of [IMPORT_PATTERN, REEXPORT_PATTERN]) {
pattern.lastIndex = 0
let match = pattern.exec(source)
while (match !== null) {
const resolved = resolveStaticSpecifier(match[1], file)
if (resolved) dependencies.add(resolved)
match = pattern.exec(source)
}
}

return [...dependencies]
}

function findStaticPath(entry: string, target: string): string[] {
const importedBy = new Map<string, string | null>([[entry, null]])
const queue = [entry]

while (queue.length > 0) {
const file = queue.shift() as string
if (file === target) {
const path: string[] = []
let cursor: string | null = file
while (cursor) {
path.unshift(relative(APP_DIR, cursor))
cursor = importedBy.get(cursor) ?? null
}
return path
}

for (const dependency of getStaticDependencies(file)) {
if (!importedBy.has(dependency)) {
importedBy.set(dependency, file)
queue.push(dependency)
}
}
}

return []
}

describe('invitation import boundaries', () => {
it('keeps direct grants out of the auth initialization graph', () => {
const path = findStaticPath(DIRECT_GRANT_ENTRY, AUTH_ENTRY)
expect(path, `Unexpected static import path:\n${path.join('\n -> ')}`).toEqual([])
})

it('keeps the direct-grant event contract dependency-free', () => {
expect(getStaticDependencies(DIRECT_GRANT_EVENT)).toEqual([])
})
})
Loading