@auxiora/social 1.0.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.
@@ -0,0 +1,70 @@
1
+ import { getLogger } from '@auxiora/logger';
2
+ const logger = getLogger('social:user-resolver');
3
+ /**
4
+ * Resolves channel senders to Auxiora user identities.
5
+ * Maps channel IDs (Discord, Telegram, Slack, etc.) to users.
6
+ */
7
+ export class UserResolver {
8
+ userManager;
9
+ constructor(userManager) {
10
+ this.userManager = userManager;
11
+ }
12
+ /**
13
+ * Resolve a channel sender to a user identity.
14
+ * Returns undefined if no user is mapped to this sender.
15
+ */
16
+ async resolveUser(channelType, senderId) {
17
+ const user = await this.userManager.authenticateUser(channelType, senderId);
18
+ if (user) {
19
+ logger.debug('Resolved user', { channelType, senderId, userId: user.id });
20
+ }
21
+ return user;
22
+ }
23
+ /**
24
+ * Resolve or create a default user for a channel sender.
25
+ * Creates a new viewer user if none is mapped.
26
+ */
27
+ async resolveOrCreate(channelType, senderId, defaultName) {
28
+ const existing = await this.resolveUser(channelType, senderId);
29
+ if (existing)
30
+ return existing;
31
+ const name = defaultName ?? `${channelType}:${senderId}`;
32
+ const user = await this.userManager.createUser(name, 'viewer', {
33
+ channels: [{ channelType, senderId }],
34
+ });
35
+ logger.debug('Auto-created user for channel sender', {
36
+ channelType,
37
+ senderId,
38
+ userId: user.id,
39
+ });
40
+ return user;
41
+ }
42
+ /**
43
+ * Map a channel sender to an existing user.
44
+ */
45
+ async mapChannel(userId, channelType, senderId) {
46
+ const user = await this.userManager.getUser(userId);
47
+ if (!user)
48
+ return false;
49
+ const existing = user.channels.find(c => c.channelType === channelType && c.senderId === senderId);
50
+ if (existing)
51
+ return true;
52
+ const channels = [...user.channels, { channelType, senderId }];
53
+ const updated = await this.userManager.updateUser(userId, { channels });
54
+ return updated !== undefined;
55
+ }
56
+ /**
57
+ * Remove a channel mapping from a user.
58
+ */
59
+ async unmapChannel(userId, channelType, senderId) {
60
+ const user = await this.userManager.getUser(userId);
61
+ if (!user)
62
+ return false;
63
+ const channels = user.channels.filter(c => !(c.channelType === channelType && c.senderId === senderId));
64
+ if (channels.length === user.channels.length)
65
+ return false;
66
+ const updated = await this.userManager.updateUser(userId, { channels });
67
+ return updated !== undefined;
68
+ }
69
+ }
70
+ //# sourceMappingURL=user-resolver.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"user-resolver.js","sourceRoot":"","sources":["../src/user-resolver.ts"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,27 @@
1
+ {
2
+ "name": "@auxiora/social",
3
+ "version": "1.0.0",
4
+ "description": "Multi-user identity, roles, and permissions management",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "types": "dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.js"
12
+ }
13
+ },
14
+ "dependencies": {
15
+ "@auxiora/audit": "1.0.0",
16
+ "@auxiora/core": "1.0.0",
17
+ "@auxiora/logger": "1.0.0"
18
+ },
19
+ "engines": {
20
+ "node": ">=22.0.0"
21
+ },
22
+ "scripts": {
23
+ "build": "tsc",
24
+ "clean": "rm -rf dist",
25
+ "typecheck": "tsc --noEmit"
26
+ }
27
+ }
package/src/index.ts ADDED
@@ -0,0 +1,11 @@
1
+ export type {
2
+ PermissionScope,
3
+ Role,
4
+ UserIdentity,
5
+ UserChannelMapping,
6
+ TeamConfig,
7
+ } from './types.js';
8
+ export { BUILT_IN_ROLES } from './types.js';
9
+ export { UserManager } from './user-manager.js';
10
+ export { RoleManager } from './role-manager.js';
11
+ export { UserResolver } from './user-resolver.js';
@@ -0,0 +1,123 @@
1
+ import * as fs from 'node:fs/promises';
2
+ import * as path from 'node:path';
3
+ import * as crypto from 'node:crypto';
4
+ import { getLogger } from '@auxiora/logger';
5
+ import { audit } from '@auxiora/audit';
6
+ import { getAuxioraDir } from '@auxiora/core';
7
+ import type { Role, PermissionScope } from './types.js';
8
+ import { BUILT_IN_ROLES } from './types.js';
9
+ import type { UserManager } from './user-manager.js';
10
+
11
+ const logger = getLogger('social:role-manager');
12
+
13
+ export class RoleManager {
14
+ private filePath: string;
15
+ private userManager: UserManager;
16
+
17
+ constructor(userManager: UserManager, options?: { dir?: string }) {
18
+ const dir = options?.dir ?? path.join(getAuxioraDir(), 'social');
19
+ this.filePath = path.join(dir, 'roles.json');
20
+ this.userManager = userManager;
21
+ }
22
+
23
+ async createRole(name: string, permissions: PermissionScope[]): Promise<Role> {
24
+ const roles = await this.readFile();
25
+
26
+ if (roles.some(r => r.name === name)) {
27
+ throw new Error(`Role already exists: ${name}`);
28
+ }
29
+
30
+ const role: Role = {
31
+ id: `role-${crypto.randomUUID().slice(0, 8)}`,
32
+ name,
33
+ permissions,
34
+ builtIn: false,
35
+ createdAt: Date.now(),
36
+ };
37
+
38
+ roles.push(role);
39
+ await this.writeFile(roles);
40
+ void audit('social.role_created', { id: role.id, name });
41
+ logger.debug('Created role', { id: role.id, name });
42
+ return role;
43
+ }
44
+
45
+ async getRole(id: string): Promise<Role | undefined> {
46
+ const builtIn = BUILT_IN_ROLES.find(r => r.id === id);
47
+ if (builtIn) return builtIn;
48
+
49
+ const roles = await this.readFile();
50
+ return roles.find(r => r.id === id);
51
+ }
52
+
53
+ async getRoleByName(name: string): Promise<Role | undefined> {
54
+ const builtIn = BUILT_IN_ROLES.find(r => r.name === name || r.id === name);
55
+ if (builtIn) return builtIn;
56
+
57
+ const roles = await this.readFile();
58
+ return roles.find(r => r.name === name);
59
+ }
60
+
61
+ async listRoles(): Promise<Role[]> {
62
+ const custom = await this.readFile();
63
+ return [...BUILT_IN_ROLES, ...custom];
64
+ }
65
+
66
+ async deleteRole(id: string): Promise<boolean> {
67
+ const builtIn = BUILT_IN_ROLES.find(r => r.id === id);
68
+ if (builtIn) {
69
+ throw new Error('Cannot delete built-in role');
70
+ }
71
+
72
+ const roles = await this.readFile();
73
+ const filtered = roles.filter(r => r.id !== id);
74
+ if (filtered.length === roles.length) return false;
75
+
76
+ await this.writeFile(filtered);
77
+ void audit('social.role_deleted', { id });
78
+ logger.debug('Deleted role', { id });
79
+ return true;
80
+ }
81
+
82
+ async assignRole(userId: string, roleId: string): Promise<boolean> {
83
+ const role = await this.getRole(roleId);
84
+ if (!role) return false;
85
+
86
+ const user = await this.userManager.updateUser(userId, { role: roleId });
87
+ if (!user) return false;
88
+
89
+ void audit('social.role_assigned', { userId, roleId });
90
+ return true;
91
+ }
92
+
93
+ async checkPermission(userId: string, scope: PermissionScope): Promise<boolean> {
94
+ const user = await this.userManager.getUser(userId);
95
+ if (!user) return false;
96
+
97
+ const role = await this.getRole(user.role);
98
+ if (!role) return false;
99
+
100
+ // Admin role has all permissions
101
+ if (role.permissions.includes('admin')) return true;
102
+
103
+ return role.permissions.includes(scope);
104
+ }
105
+
106
+ private async readFile(): Promise<Role[]> {
107
+ try {
108
+ const content = await fs.readFile(this.filePath, 'utf-8');
109
+ return JSON.parse(content) as Role[];
110
+ } catch (error) {
111
+ if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
112
+ return [];
113
+ }
114
+ throw error;
115
+ }
116
+ }
117
+
118
+ private async writeFile(roles: Role[]): Promise<void> {
119
+ const dir = path.dirname(this.filePath);
120
+ await fs.mkdir(dir, { recursive: true });
121
+ await fs.writeFile(this.filePath, JSON.stringify(roles, null, 2), 'utf-8');
122
+ }
123
+ }
package/src/types.ts ADDED
@@ -0,0 +1,104 @@
1
+ /** Permission scopes for role-based access control. */
2
+ export type PermissionScope =
3
+ | 'memory:read'
4
+ | 'memory:write'
5
+ | 'memory:delete'
6
+ | 'sessions:read'
7
+ | 'sessions:write'
8
+ | 'behaviors:read'
9
+ | 'behaviors:write'
10
+ | 'behaviors:delete'
11
+ | 'vault:read'
12
+ | 'vault:write'
13
+ | 'trust:read'
14
+ | 'trust:write'
15
+ | 'plugins:manage'
16
+ | 'webhooks:manage'
17
+ | 'users:read'
18
+ | 'users:write'
19
+ | 'users:delete'
20
+ | 'workflows:read'
21
+ | 'workflows:write'
22
+ | 'workflows:approve'
23
+ | 'agent-protocol:send'
24
+ | 'agent-protocol:receive'
25
+ | 'admin';
26
+
27
+ /** A named role with associated permissions. */
28
+ export interface Role {
29
+ id: string;
30
+ name: string;
31
+ permissions: PermissionScope[];
32
+ builtIn: boolean;
33
+ createdAt: number;
34
+ }
35
+
36
+ /** A user identity within the system. */
37
+ export interface UserIdentity {
38
+ id: string;
39
+ name: string;
40
+ role: string;
41
+ channels: UserChannelMapping[];
42
+ trustOverrides: Record<string, number>;
43
+ memoryPartition: string;
44
+ personalityRelationship: string;
45
+ createdAt: number;
46
+ updatedAt: number;
47
+ lastActiveAt: number;
48
+ }
49
+
50
+ /** Maps an external channel sender to this user. */
51
+ export interface UserChannelMapping {
52
+ channelType: string;
53
+ senderId: string;
54
+ }
55
+
56
+ /** Configuration for a team of users. */
57
+ export interface TeamConfig {
58
+ name: string;
59
+ ownerId: string;
60
+ memberIds: string[];
61
+ sharedMemoryPartition: string;
62
+ createdAt: number;
63
+ }
64
+
65
+ /** Built-in role definitions. */
66
+ export const BUILT_IN_ROLES: Role[] = [
67
+ {
68
+ id: 'admin',
69
+ name: 'Admin',
70
+ permissions: ['admin'],
71
+ builtIn: true,
72
+ createdAt: 0,
73
+ },
74
+ {
75
+ id: 'member',
76
+ name: 'Member',
77
+ permissions: [
78
+ 'memory:read',
79
+ 'memory:write',
80
+ 'sessions:read',
81
+ 'sessions:write',
82
+ 'behaviors:read',
83
+ 'workflows:read',
84
+ 'workflows:write',
85
+ 'workflows:approve',
86
+ 'agent-protocol:send',
87
+ 'agent-protocol:receive',
88
+ ],
89
+ builtIn: true,
90
+ createdAt: 0,
91
+ },
92
+ {
93
+ id: 'viewer',
94
+ name: 'Viewer',
95
+ permissions: [
96
+ 'memory:read',
97
+ 'sessions:read',
98
+ 'behaviors:read',
99
+ 'workflows:read',
100
+ ],
101
+ builtIn: true,
102
+ createdAt: 0,
103
+ },
104
+ ];
@@ -0,0 +1,155 @@
1
+ import * as fs from 'node:fs/promises';
2
+ import * as path from 'node:path';
3
+ import * as crypto from 'node:crypto';
4
+ import { getLogger } from '@auxiora/logger';
5
+ import { audit } from '@auxiora/audit';
6
+ import { getAuxioraDir } from '@auxiora/core';
7
+ import type { UserIdentity, UserChannelMapping } from './types.js';
8
+
9
+ const logger = getLogger('social:user-manager');
10
+
11
+ export class UserManager {
12
+ private filePath: string;
13
+
14
+ constructor(options?: { dir?: string }) {
15
+ const dir = options?.dir ?? path.join(getAuxioraDir(), 'social');
16
+ this.filePath = path.join(dir, 'users.json');
17
+ }
18
+
19
+ async createUser(
20
+ name: string,
21
+ role: string,
22
+ options?: {
23
+ channels?: UserChannelMapping[];
24
+ memoryPartition?: string;
25
+ personalityRelationship?: string;
26
+ },
27
+ ): Promise<UserIdentity> {
28
+ const users = await this.readFile();
29
+
30
+ const id = `user-${crypto.randomUUID().slice(0, 8)}`;
31
+ const now = Date.now();
32
+
33
+ const user: UserIdentity = {
34
+ id,
35
+ name,
36
+ role,
37
+ channels: options?.channels ?? [],
38
+ trustOverrides: {},
39
+ memoryPartition: options?.memoryPartition ?? `private:${id}`,
40
+ personalityRelationship: options?.personalityRelationship ?? 'default',
41
+ createdAt: now,
42
+ updatedAt: now,
43
+ lastActiveAt: now,
44
+ };
45
+
46
+ users.push(user);
47
+ await this.writeFile(users);
48
+ void audit('social.user_created', { id, name, role });
49
+ logger.debug('Created user', { id, name });
50
+ return user;
51
+ }
52
+
53
+ async getUser(id: string): Promise<UserIdentity | undefined> {
54
+ const users = await this.readFile();
55
+ return users.find(u => u.id === id);
56
+ }
57
+
58
+ async getUserByName(name: string): Promise<UserIdentity | undefined> {
59
+ const users = await this.readFile();
60
+ return users.find(u => u.name === name);
61
+ }
62
+
63
+ async updateUser(
64
+ id: string,
65
+ updates: Partial<Pick<UserIdentity, 'name' | 'role' | 'channels' | 'trustOverrides' | 'memoryPartition' | 'personalityRelationship'>>,
66
+ ): Promise<UserIdentity | undefined> {
67
+ const users = await this.readFile();
68
+ const user = users.find(u => u.id === id);
69
+ if (!user) return undefined;
70
+
71
+ if (updates.name !== undefined) user.name = updates.name;
72
+ if (updates.role !== undefined) user.role = updates.role;
73
+ if (updates.channels !== undefined) user.channels = updates.channels;
74
+ if (updates.trustOverrides !== undefined) user.trustOverrides = updates.trustOverrides;
75
+ if (updates.memoryPartition !== undefined) user.memoryPartition = updates.memoryPartition;
76
+ if (updates.personalityRelationship !== undefined) user.personalityRelationship = updates.personalityRelationship;
77
+ user.updatedAt = Date.now();
78
+
79
+ await this.writeFile(users);
80
+ void audit('social.user_updated', { id, updates: Object.keys(updates) });
81
+ logger.debug('Updated user', { id });
82
+ return user;
83
+ }
84
+
85
+ async deleteUser(id: string): Promise<boolean> {
86
+ const users = await this.readFile();
87
+ const filtered = users.filter(u => u.id !== id);
88
+ if (filtered.length === users.length) return false;
89
+
90
+ await this.writeFile(filtered);
91
+ void audit('social.user_deleted', { id });
92
+ logger.debug('Deleted user', { id });
93
+ return true;
94
+ }
95
+
96
+ async listUsers(): Promise<UserIdentity[]> {
97
+ return this.readFile();
98
+ }
99
+
100
+ async authenticateUser(channelType: string, senderId: string): Promise<UserIdentity | undefined> {
101
+ const users = await this.readFile();
102
+ const user = users.find(u =>
103
+ u.channels.some(c => c.channelType === channelType && c.senderId === senderId),
104
+ );
105
+
106
+ if (user) {
107
+ user.lastActiveAt = Date.now();
108
+ await this.writeFile(users);
109
+ }
110
+
111
+ return user;
112
+ }
113
+
114
+ async switchUser(userId: string, channelType: string, senderId: string): Promise<UserIdentity | undefined> {
115
+ const users = await this.readFile();
116
+ const user = users.find(u => u.id === userId);
117
+ if (!user) return undefined;
118
+
119
+ // Remove this channel mapping from any other user
120
+ for (const u of users) {
121
+ u.channels = u.channels.filter(
122
+ c => !(c.channelType === channelType && c.senderId === senderId),
123
+ );
124
+ }
125
+
126
+ // Add mapping to the target user
127
+ if (!user.channels.some(c => c.channelType === channelType && c.senderId === senderId)) {
128
+ user.channels.push({ channelType, senderId });
129
+ }
130
+ user.lastActiveAt = Date.now();
131
+ user.updatedAt = Date.now();
132
+
133
+ await this.writeFile(users);
134
+ void audit('social.user_switched', { userId, channelType, senderId });
135
+ return user;
136
+ }
137
+
138
+ private async readFile(): Promise<UserIdentity[]> {
139
+ try {
140
+ const content = await fs.readFile(this.filePath, 'utf-8');
141
+ return JSON.parse(content) as UserIdentity[];
142
+ } catch (error) {
143
+ if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
144
+ return [];
145
+ }
146
+ throw error;
147
+ }
148
+ }
149
+
150
+ private async writeFile(users: UserIdentity[]): Promise<void> {
151
+ const dir = path.dirname(this.filePath);
152
+ await fs.mkdir(dir, { recursive: true });
153
+ await fs.writeFile(this.filePath, JSON.stringify(users, null, 2), 'utf-8');
154
+ }
155
+ }
@@ -0,0 +1,98 @@
1
+ import { getLogger } from '@auxiora/logger';
2
+ import type { UserIdentity } from './types.js';
3
+ import type { UserManager } from './user-manager.js';
4
+
5
+ const logger = getLogger('social:user-resolver');
6
+
7
+ /**
8
+ * Resolves channel senders to Auxiora user identities.
9
+ * Maps channel IDs (Discord, Telegram, Slack, etc.) to users.
10
+ */
11
+ export class UserResolver {
12
+ constructor(private userManager: UserManager) {}
13
+
14
+ /**
15
+ * Resolve a channel sender to a user identity.
16
+ * Returns undefined if no user is mapped to this sender.
17
+ */
18
+ async resolveUser(
19
+ channelType: string,
20
+ senderId: string,
21
+ ): Promise<UserIdentity | undefined> {
22
+ const user = await this.userManager.authenticateUser(channelType, senderId);
23
+
24
+ if (user) {
25
+ logger.debug('Resolved user', { channelType, senderId, userId: user.id });
26
+ }
27
+
28
+ return user;
29
+ }
30
+
31
+ /**
32
+ * Resolve or create a default user for a channel sender.
33
+ * Creates a new viewer user if none is mapped.
34
+ */
35
+ async resolveOrCreate(
36
+ channelType: string,
37
+ senderId: string,
38
+ defaultName?: string,
39
+ ): Promise<UserIdentity> {
40
+ const existing = await this.resolveUser(channelType, senderId);
41
+ if (existing) return existing;
42
+
43
+ const name = defaultName ?? `${channelType}:${senderId}`;
44
+ const user = await this.userManager.createUser(name, 'viewer', {
45
+ channels: [{ channelType, senderId }],
46
+ });
47
+
48
+ logger.debug('Auto-created user for channel sender', {
49
+ channelType,
50
+ senderId,
51
+ userId: user.id,
52
+ });
53
+
54
+ return user;
55
+ }
56
+
57
+ /**
58
+ * Map a channel sender to an existing user.
59
+ */
60
+ async mapChannel(
61
+ userId: string,
62
+ channelType: string,
63
+ senderId: string,
64
+ ): Promise<boolean> {
65
+ const user = await this.userManager.getUser(userId);
66
+ if (!user) return false;
67
+
68
+ const existing = user.channels.find(
69
+ c => c.channelType === channelType && c.senderId === senderId,
70
+ );
71
+ if (existing) return true;
72
+
73
+ const channels = [...user.channels, { channelType, senderId }];
74
+ const updated = await this.userManager.updateUser(userId, { channels });
75
+ return updated !== undefined;
76
+ }
77
+
78
+ /**
79
+ * Remove a channel mapping from a user.
80
+ */
81
+ async unmapChannel(
82
+ userId: string,
83
+ channelType: string,
84
+ senderId: string,
85
+ ): Promise<boolean> {
86
+ const user = await this.userManager.getUser(userId);
87
+ if (!user) return false;
88
+
89
+ const channels = user.channels.filter(
90
+ c => !(c.channelType === channelType && c.senderId === senderId),
91
+ );
92
+
93
+ if (channels.length === user.channels.length) return false;
94
+
95
+ const updated = await this.userManager.updateUser(userId, { channels });
96
+ return updated !== undefined;
97
+ }
98
+ }