@shadowob/shared 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,69 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import { CLIENT_EVENTS, SERVER_EVENTS } from '../src/constants/events'
3
+ import { LIMITS } from '../src/constants/limits'
4
+
5
+ describe('CLIENT_EVENTS', () => {
6
+ it('should have all expected client events', () => {
7
+ expect(CLIENT_EVENTS.CHANNEL_JOIN).toBe('channel:join')
8
+ expect(CLIENT_EVENTS.CHANNEL_LEAVE).toBe('channel:leave')
9
+ expect(CLIENT_EVENTS.MESSAGE_SEND).toBe('message:send')
10
+ expect(CLIENT_EVENTS.MESSAGE_TYPING).toBe('message:typing')
11
+ expect(CLIENT_EVENTS.PRESENCE_UPDATE).toBe('presence:update')
12
+ })
13
+
14
+ it('should be readonly', () => {
15
+ const events = CLIENT_EVENTS
16
+ expect(Object.keys(events)).toHaveLength(5)
17
+ })
18
+ })
19
+
20
+ describe('SERVER_EVENTS', () => {
21
+ it('should have all expected server events', () => {
22
+ expect(SERVER_EVENTS.MESSAGE_NEW).toBe('message:new')
23
+ expect(SERVER_EVENTS.MESSAGE_UPDATE).toBe('message:update')
24
+ expect(SERVER_EVENTS.MESSAGE_DELETE).toBe('message:delete')
25
+ expect(SERVER_EVENTS.MEMBER_TYPING).toBe('member:typing')
26
+ expect(SERVER_EVENTS.MEMBER_JOIN).toBe('member:join')
27
+ expect(SERVER_EVENTS.MEMBER_LEAVE).toBe('member:leave')
28
+ expect(SERVER_EVENTS.PRESENCE_CHANGE).toBe('presence:change')
29
+ expect(SERVER_EVENTS.REACTION_ADD).toBe('reaction:add')
30
+ expect(SERVER_EVENTS.REACTION_REMOVE).toBe('reaction:remove')
31
+ expect(SERVER_EVENTS.NOTIFICATION_NEW).toBe('notification:new')
32
+ expect(SERVER_EVENTS.DM_MESSAGE_NEW).toBe('dm:message:new')
33
+ })
34
+
35
+ it('should have 11 server events', () => {
36
+ expect(Object.keys(SERVER_EVENTS)).toHaveLength(11)
37
+ })
38
+ })
39
+
40
+ describe('LIMITS', () => {
41
+ it('should define message limits', () => {
42
+ expect(LIMITS.MESSAGE_CONTENT_MAX).toBe(4000)
43
+ expect(LIMITS.MESSAGES_PER_PAGE).toBe(50)
44
+ })
45
+
46
+ it('should define username limits', () => {
47
+ expect(LIMITS.USERNAME_MIN).toBe(3)
48
+ expect(LIMITS.USERNAME_MAX).toBe(32)
49
+ })
50
+
51
+ it('should define file upload limit', () => {
52
+ expect(LIMITS.FILE_UPLOAD_MAX_SIZE).toBe(10 * 1024 * 1024)
53
+ })
54
+
55
+ it('should define server/channel limits', () => {
56
+ expect(LIMITS.SERVER_NAME_MAX).toBe(100)
57
+ expect(LIMITS.CHANNEL_NAME_MAX).toBe(100)
58
+ expect(LIMITS.SERVERS_PER_USER_MAX).toBe(100)
59
+ expect(LIMITS.CHANNELS_PER_SERVER_MAX).toBe(200)
60
+ })
61
+
62
+ it('should define invite code length', () => {
63
+ expect(LIMITS.INVITE_CODE_LENGTH).toBe(8)
64
+ })
65
+
66
+ it('should define password min length', () => {
67
+ expect(LIMITS.PASSWORD_MIN).toBe(8)
68
+ })
69
+ })
@@ -0,0 +1,80 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import { formatDate, generateInviteCode, isValidEmail, slugify } from '../src/utils'
3
+
4
+ describe('generateInviteCode', () => {
5
+ it('should generate a string of length 8', () => {
6
+ const code = generateInviteCode()
7
+ expect(code).toHaveLength(8)
8
+ })
9
+
10
+ it('should only contain alphanumeric characters', () => {
11
+ const code = generateInviteCode()
12
+ expect(code).toMatch(/^[A-Za-z0-9]+$/)
13
+ })
14
+
15
+ it('should generate unique codes', () => {
16
+ const codes = new Set(Array.from({ length: 100 }, () => generateInviteCode()))
17
+ expect(codes.size).toBe(100)
18
+ })
19
+ })
20
+
21
+ describe('formatDate', () => {
22
+ it('should format a Date object to ISO string', () => {
23
+ const date = new Date('2024-01-15T12:30:00Z')
24
+ expect(formatDate(date)).toBe('2024-01-15T12:30:00.000Z')
25
+ })
26
+
27
+ it('should format a date string to ISO string', () => {
28
+ const result = formatDate('2024-01-15T12:30:00Z')
29
+ expect(result).toBe('2024-01-15T12:30:00.000Z')
30
+ })
31
+
32
+ it('should handle various date string formats', () => {
33
+ const result = formatDate('2024-01-15')
34
+ expect(result).toContain('2024-01-15')
35
+ })
36
+ })
37
+
38
+ describe('isValidEmail', () => {
39
+ it('should return true for valid emails', () => {
40
+ expect(isValidEmail('user@example.com')).toBe(true)
41
+ expect(isValidEmail('first.last@domain.org')).toBe(true)
42
+ expect(isValidEmail('user+tag@sub.domain.com')).toBe(true)
43
+ })
44
+
45
+ it('should return false for invalid emails', () => {
46
+ expect(isValidEmail('')).toBe(false)
47
+ expect(isValidEmail('user')).toBe(false)
48
+ expect(isValidEmail('user@')).toBe(false)
49
+ expect(isValidEmail('@domain.com')).toBe(false)
50
+ expect(isValidEmail('user @domain.com')).toBe(false)
51
+ expect(isValidEmail('user@domain')).toBe(false)
52
+ })
53
+ })
54
+
55
+ describe('slugify', () => {
56
+ it('should convert text to lowercase slug', () => {
57
+ expect(slugify('Hello World')).toBe('hello-world')
58
+ })
59
+
60
+ it('should replace special characters with hyphens', () => {
61
+ expect(slugify('Hello, World!')).toBe('hello-world')
62
+ })
63
+
64
+ it('should trim leading/trailing hyphens', () => {
65
+ expect(slugify(' Hello World ')).toBe('hello-world')
66
+ expect(slugify('---hello---')).toBe('hello')
67
+ })
68
+
69
+ it('should handle multiple consecutive special characters', () => {
70
+ expect(slugify('hello...world')).toBe('hello-world')
71
+ })
72
+
73
+ it('should keep numbers', () => {
74
+ expect(slugify('Version 2.0')).toBe('version-2-0')
75
+ })
76
+
77
+ it('should handle empty string', () => {
78
+ expect(slugify('')).toBe('')
79
+ })
80
+ })
package/package.json ADDED
@@ -0,0 +1,20 @@
1
+ {
2
+ "name": "@shadowob/shared",
3
+ "version": "0.1.1",
4
+ "type": "module",
5
+ "main": "./src/index.ts",
6
+ "types": "./src/index.ts",
7
+ "exports": {
8
+ ".": "./src/index.ts",
9
+ "./types": "./src/types/index.ts",
10
+ "./constants": "./src/constants/index.ts",
11
+ "./utils": "./src/utils/index.ts"
12
+ },
13
+ "dependencies": {
14
+ "nanoid": "^5.1.5"
15
+ },
16
+ "scripts": {
17
+ "test": "vitest run",
18
+ "test:watch": "vitest"
19
+ }
20
+ }
@@ -0,0 +1,28 @@
1
+ // ─── Client → Server ───
2
+
3
+ export const CLIENT_EVENTS = {
4
+ CHANNEL_JOIN: 'channel:join',
5
+ CHANNEL_LEAVE: 'channel:leave',
6
+ MESSAGE_SEND: 'message:send',
7
+ MESSAGE_TYPING: 'message:typing',
8
+ PRESENCE_UPDATE: 'presence:update',
9
+ } as const
10
+
11
+ // ─── Server → Client ───
12
+
13
+ export const SERVER_EVENTS = {
14
+ MESSAGE_NEW: 'message:new',
15
+ MESSAGE_UPDATE: 'message:update',
16
+ MESSAGE_DELETE: 'message:delete',
17
+ MEMBER_TYPING: 'member:typing',
18
+ MEMBER_JOIN: 'member:join',
19
+ MEMBER_LEAVE: 'member:leave',
20
+ PRESENCE_CHANGE: 'presence:change',
21
+ REACTION_ADD: 'reaction:add',
22
+ REACTION_REMOVE: 'reaction:remove',
23
+ NOTIFICATION_NEW: 'notification:new',
24
+ DM_MESSAGE_NEW: 'dm:message:new',
25
+ } as const
26
+
27
+ export type ClientEvent = (typeof CLIENT_EVENTS)[keyof typeof CLIENT_EVENTS]
28
+ export type ServerEvent = (typeof SERVER_EVENTS)[keyof typeof SERVER_EVENTS]
@@ -0,0 +1,2 @@
1
+ export * from './events'
2
+ export * from './limits'
@@ -0,0 +1,30 @@
1
+ export const LIMITS = {
2
+ /** Max message content length */
3
+ MESSAGE_CONTENT_MAX: 4000,
4
+ /** Max username length */
5
+ USERNAME_MAX: 32,
6
+ /** Min username length */
7
+ USERNAME_MIN: 3,
8
+ /** Max display name length */
9
+ DISPLAY_NAME_MAX: 64,
10
+ /** Max server name length */
11
+ SERVER_NAME_MAX: 100,
12
+ /** Max channel name length */
13
+ CHANNEL_NAME_MAX: 100,
14
+ /** Max thread name length */
15
+ THREAD_NAME_MAX: 100,
16
+ /** Max file upload size (10MB) */
17
+ FILE_UPLOAD_MAX_SIZE: 10 * 1024 * 1024,
18
+ /** Messages per page (cursor pagination) */
19
+ MESSAGES_PER_PAGE: 50,
20
+ /** Max servers per user */
21
+ SERVERS_PER_USER_MAX: 100,
22
+ /** Max channels per server */
23
+ CHANNELS_PER_SERVER_MAX: 200,
24
+ /** Invite code length */
25
+ INVITE_CODE_LENGTH: 8,
26
+ /** Password min length */
27
+ PASSWORD_MIN: 8,
28
+ /** Max reactions per message per user */
29
+ REACTIONS_PER_MESSAGE_MAX: 20,
30
+ } as const
package/src/index.ts ADDED
@@ -0,0 +1,3 @@
1
+ export * from './constants'
2
+ export * from './types'
3
+ export * from './utils'
@@ -0,0 +1,44 @@
1
+ export type AgentStatus = 'running' | 'stopped' | 'error'
2
+
3
+ export type AgentKernelType = 'claude-code' | 'cursor' | 'mcp-server' | 'custom'
4
+
5
+ export type AgentCapability =
6
+ | 'chat'
7
+ | 'code-gen'
8
+ | 'code-review'
9
+ | 'research'
10
+ | 'tools'
11
+ | 'file-access'
12
+
13
+ export interface Agent {
14
+ id: string
15
+ userId: string
16
+ kernelType: AgentKernelType
17
+ config: Record<string, unknown>
18
+ containerId: string | null
19
+ status: AgentStatus
20
+ ownerId: string
21
+ createdAt: string
22
+ updatedAt: string
23
+ user?: {
24
+ id: string
25
+ username: string
26
+ displayName: string
27
+ avatarUrl: string | null
28
+ }
29
+ capabilities?: AgentCapability[]
30
+ }
31
+
32
+ export interface CreateAgentRequest {
33
+ name: string
34
+ kernelType: AgentKernelType
35
+ config?: Record<string, unknown>
36
+ }
37
+
38
+ export interface AgentInfo {
39
+ id: string
40
+ name: string
41
+ kernelType: AgentKernelType
42
+ status: AgentStatus
43
+ capabilities: AgentCapability[]
44
+ }
@@ -0,0 +1,24 @@
1
+ export type ChannelType = 'text' | 'voice' | 'announcement'
2
+
3
+ export interface Channel {
4
+ id: string
5
+ name: string
6
+ type: ChannelType
7
+ serverId: string
8
+ topic: string | null
9
+ position: number
10
+ createdAt: string
11
+ updatedAt: string
12
+ }
13
+
14
+ export interface CreateChannelRequest {
15
+ name: string
16
+ type?: ChannelType
17
+ topic?: string
18
+ }
19
+
20
+ export interface UpdateChannelRequest {
21
+ name?: string
22
+ topic?: string
23
+ position?: number
24
+ }
@@ -0,0 +1,5 @@
1
+ export * from './agent.types'
2
+ export * from './channel.types'
3
+ export * from './message.types'
4
+ export * from './server.types'
5
+ export * from './user.types'
@@ -0,0 +1,89 @@
1
+ export interface Message {
2
+ id: string
3
+ content: string
4
+ channelId: string
5
+ authorId: string
6
+ threadId: string | null
7
+ replyToId: string | null
8
+ isEdited: boolean
9
+ isPinned: boolean
10
+ createdAt: string
11
+ updatedAt: string
12
+ author?: {
13
+ id: string
14
+ username: string
15
+ displayName: string
16
+ avatarUrl: string | null
17
+ isBot: boolean
18
+ }
19
+ attachments?: Attachment[]
20
+ reactions?: ReactionGroup[]
21
+ }
22
+
23
+ export interface Attachment {
24
+ id: string
25
+ messageId: string
26
+ filename: string
27
+ url: string
28
+ contentType: string
29
+ size: number
30
+ width: number | null
31
+ height: number | null
32
+ createdAt: string
33
+ }
34
+
35
+ export interface ReactionGroup {
36
+ emoji: string
37
+ count: number
38
+ userIds: string[]
39
+ }
40
+
41
+ export interface Thread {
42
+ id: string
43
+ name: string
44
+ channelId: string
45
+ parentMessageId: string
46
+ creatorId: string
47
+ isArchived: boolean
48
+ createdAt: string
49
+ updatedAt: string
50
+ }
51
+
52
+ export interface SendMessageRequest {
53
+ content: string
54
+ threadId?: string
55
+ replyToId?: string
56
+ }
57
+
58
+ export interface UpdateMessageRequest {
59
+ content: string
60
+ }
61
+
62
+ export type NotificationType = 'mention' | 'reply' | 'dm' | 'system'
63
+
64
+ export interface Notification {
65
+ id: string
66
+ userId: string
67
+ type: NotificationType
68
+ title: string
69
+ body: string | null
70
+ referenceId: string | null
71
+ referenceType: string | null
72
+ isRead: boolean
73
+ createdAt: string
74
+ }
75
+
76
+ export interface DmChannel {
77
+ id: string
78
+ userAId: string
79
+ userBId: string
80
+ lastMessageAt: string | null
81
+ createdAt: string
82
+ otherUser?: {
83
+ id: string
84
+ username: string
85
+ displayName: string
86
+ avatarUrl: string | null
87
+ status: string
88
+ }
89
+ }
@@ -0,0 +1,38 @@
1
+ export interface Server {
2
+ id: string
3
+ name: string
4
+ iconUrl: string | null
5
+ ownerId: string
6
+ inviteCode: string
7
+ createdAt: string
8
+ updatedAt: string
9
+ }
10
+
11
+ export interface CreateServerRequest {
12
+ name: string
13
+ iconUrl?: string
14
+ }
15
+
16
+ export interface UpdateServerRequest {
17
+ name?: string
18
+ iconUrl?: string
19
+ }
20
+
21
+ export type MemberRole = 'owner' | 'admin' | 'member'
22
+
23
+ export interface Member {
24
+ id: string
25
+ userId: string
26
+ serverId: string
27
+ role: MemberRole
28
+ nickname: string | null
29
+ joinedAt: string
30
+ user?: {
31
+ id: string
32
+ username: string
33
+ displayName: string
34
+ avatarUrl: string | null
35
+ status: string
36
+ isBot: boolean
37
+ }
38
+ }
@@ -0,0 +1,40 @@
1
+ export type UserStatus = 'online' | 'idle' | 'dnd' | 'offline'
2
+
3
+ export interface User {
4
+ id: string
5
+ email: string
6
+ username: string
7
+ displayName: string
8
+ avatarUrl: string | null
9
+ status: UserStatus
10
+ isBot: boolean
11
+ createdAt: string
12
+ updatedAt: string
13
+ }
14
+
15
+ export interface UserProfile {
16
+ id: string
17
+ username: string
18
+ displayName: string
19
+ avatarUrl: string | null
20
+ status: UserStatus
21
+ isBot: boolean
22
+ }
23
+
24
+ export interface LoginRequest {
25
+ email: string
26
+ password: string
27
+ }
28
+
29
+ export interface RegisterRequest {
30
+ email: string
31
+ username: string
32
+ displayName: string
33
+ password: string
34
+ }
35
+
36
+ export interface AuthResponse {
37
+ user: User
38
+ accessToken: string
39
+ refreshToken: string
40
+ }
@@ -0,0 +1,21 @@
1
+ import { customAlphabet } from 'nanoid'
2
+
3
+ const alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'
4
+
5
+ export const generateInviteCode = customAlphabet(alphabet, 8)
6
+
7
+ export function formatDate(date: string | Date): string {
8
+ const d = typeof date === 'string' ? new Date(date) : date
9
+ return d.toISOString()
10
+ }
11
+
12
+ export function isValidEmail(email: string): boolean {
13
+ return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)
14
+ }
15
+
16
+ export function slugify(text: string): string {
17
+ return text
18
+ .toLowerCase()
19
+ .replace(/[^a-z0-9]+/g, '-')
20
+ .replace(/^-+|-+$/g, '')
21
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,8 @@
1
+ {
2
+ "extends": "../../tsconfig.json",
3
+ "compilerOptions": {
4
+ "rootDir": "./src",
5
+ "outDir": "./dist"
6
+ },
7
+ "include": ["src"]
8
+ }