clay-server 5.1.1-beta.1 → 5.2.0-beta.2
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/lib/account-auth-policy.js +21 -0
- package/lib/account-auth-socket.js +29 -0
- package/lib/account-auth-throttle.js +44 -0
- package/lib/account-auth-transport.js +48 -0
- package/lib/account-login-page.js +28 -0
- package/lib/account-passkey-challenges.js +31 -0
- package/lib/account-passkey-page.js +89 -0
- package/lib/account-passkey-policy.js +39 -0
- package/lib/account-password-page.js +69 -0
- package/lib/account-passwords.js +36 -0
- package/lib/account-remembered.js +77 -0
- package/lib/account-session-page.js +51 -0
- package/lib/account-session-store.js +82 -0
- package/lib/daemon.js +2 -0
- package/lib/migrate-single-user.js +3 -23
- package/lib/project-connection.js +1 -1
- package/lib/project-server-settings.js +1 -0
- package/lib/project-sessions.js +3 -1
- package/lib/project-user-knowledge.js +2 -1
- package/lib/public/app.js +2 -0
- package/lib/public/auth/account-login.css +98 -0
- package/lib/public/auth/account-login.js +104 -0
- package/lib/public/css/command-palette.css +12 -33
- package/lib/public/css/user-settings-sessions.css +42 -0
- package/lib/public/index.html +17 -5
- package/lib/public/modules/account-auth-settings.js +71 -0
- package/lib/public/modules/account-transport-settings.js +51 -0
- package/lib/public/modules/app-header.js +3 -2
- package/lib/public/modules/app-messages.js +16 -4
- package/lib/public/modules/app-rendering.js +4 -3
- package/lib/public/modules/chat-render-runtime.js +8 -0
- package/lib/public/modules/clay-log-links.js +23 -0
- package/lib/public/modules/command-palette.js +94 -5
- package/lib/public/modules/markdown.js +1 -1
- package/lib/public/modules/passkey-browser.js +69 -0
- package/lib/public/modules/passkey-server-settings.js +57 -0
- package/lib/public/modules/project-logs-render.js +3 -2
- package/lib/public/modules/server-settings-controls.js +10 -0
- package/lib/public/modules/server-settings.js +7 -2
- package/lib/public/modules/user-settings-account.js +14 -1
- package/lib/public/modules/user-settings-sessions.js +120 -0
- package/lib/public/modules/user-settings.js +3 -0
- package/lib/public/modules/you-activity.js +3 -1
- package/lib/public/modules/you.js +91 -6
- package/lib/public/style.css +1 -0
- package/lib/server-account-passkeys.js +186 -0
- package/lib/server-account-passwords.js +127 -0
- package/lib/server-account-security.js +53 -0
- package/lib/server-auth.js +107 -79
- package/lib/server-home-chat.js +3 -3
- package/lib/server-home-clay-session-links.js +17 -9
- package/lib/server-settings.js +2 -1
- package/lib/server.js +3 -1
- package/lib/sessions.js +3 -1
- package/lib/user-knowledge-service.js +23 -1
- package/lib/users-auth.js +38 -3
- package/lib/users-create.js +97 -0
- package/lib/users-server-pin.js +37 -0
- package/lib/users.js +11 -84
- package/lib/workspace-query-service.js +13 -2
- package/package.json +2 -1
|
@@ -22,6 +22,7 @@ function attachUserKnowledgeService(deps) {
|
|
|
22
22
|
if (record.seen) result.seen[record.seen.ref] = Math.max(result.seen[record.seen.ref] || 0, record.seen.revision);
|
|
23
23
|
if (record.entry) result.entries[record.entry.ref] = record.entry;
|
|
24
24
|
if (record.report) result.reports[record.report.ref] = record.report;
|
|
25
|
+
(Array.isArray(record.reports) ? record.reports : []).forEach(function (report) { result.reports[report.ref] = report; });
|
|
25
26
|
if (record.activity) result.activity.push(record.activity);
|
|
26
27
|
});
|
|
27
28
|
return result;
|
|
@@ -73,6 +74,26 @@ function attachUserKnowledgeService(deps) {
|
|
|
73
74
|
});
|
|
74
75
|
return Object.assign({}, entry, { comments: comments, commentCount: comments.length });
|
|
75
76
|
}
|
|
77
|
+
function remove(id,args) {
|
|
78
|
+
var state = snapshot(id);
|
|
79
|
+
var previous = state.entries[args.ref];
|
|
80
|
+
if (!previous || previous.archived || previous.deleted) throw new Error('User knowledge not found.');
|
|
81
|
+
if (!Number.isInteger(args.expectedRevision) || args.expectedRevision !== previous.revisions) throw new Error('Knowledge changed. Read it again before deleting.');
|
|
82
|
+
var now = Date.now();
|
|
83
|
+
var entry = Object.assign({}, previous, { archived: true, deleted: true, deletedAt: now, updatedAt: now,
|
|
84
|
+
revisions: previous.revisions + 1, updatedBy: { type: 'user', displayName: 'You' }, reason: 'Deleted by the account owner.' });
|
|
85
|
+
var reports = Object.values(state.reports).filter(function (report) {
|
|
86
|
+
return report.status === 'pending' && (report.entryRef === previous.ref || report.commentEntryRef === previous.ref);
|
|
87
|
+
});
|
|
88
|
+
var reportUpdates = reports.map(function (report) { return Object.assign({}, report, {
|
|
89
|
+
status: 'declined', reviewedAt: now, reason: 'The knowledge entry was deleted by the account owner.'
|
|
90
|
+
}); });
|
|
91
|
+
var activity = { id: crypto.randomBytes(12).toString('hex'), ref: previous.ref, title: previous.title,
|
|
92
|
+
summary: previous.summary, at: now, action: 'delete', revision: entry.revisions, deleted: true };
|
|
93
|
+
storage(id).append({ kind: 'user-knowledge', entry: entry, reports: reportUpdates, activity: activity });
|
|
94
|
+
try { deps.onChange(owner(id), activity); } catch (error) { /* The deletion survives notification failures. */ }
|
|
95
|
+
return { entry: entry, activity: activity };
|
|
96
|
+
}
|
|
76
97
|
function comment(id,args,source) {
|
|
77
98
|
var entry = read(id,args.ref);
|
|
78
99
|
return report(id,{observation:args.body,evidence:'Comment on '+entry.ref+': '+args.body,evidenceType:'explicit'},source,entry.ref);
|
|
@@ -141,6 +162,7 @@ function attachUserKnowledgeService(deps) {
|
|
|
141
162
|
var reason=text(args.reason,2000,true);
|
|
142
163
|
var previous=args.ref ? state.entries[args.ref]:null;
|
|
143
164
|
if (args.ref && !previous) throw new Error('User knowledge not found.');
|
|
165
|
+
if (previous && (previous.archived || previous.deleted)) throw new Error('User knowledge not found.');
|
|
144
166
|
if (previous && args.expectedRevision!==previous.revisions) throw new Error('Knowledge changed. Read it again before updating.');
|
|
145
167
|
var entry=null;
|
|
146
168
|
var now=Date.now();
|
|
@@ -162,6 +184,6 @@ function attachUserKnowledgeService(deps) {
|
|
|
162
184
|
}
|
|
163
185
|
};
|
|
164
186
|
}
|
|
165
|
-
return {bind:bind,list:list,read:read,report:report,comment:comment,attention:attention,acknowledge:acknowledge,activity:function(id,args){return page(snapshot(id).activity.slice().reverse(),args);},pendingCount:function(id){return Object.values(snapshot(id).reports).filter(function(r){return r.status==='pending';}).length;}};
|
|
187
|
+
return {bind:bind,list:list,read:read,remove:remove,report:report,comment:comment,attention:attention,acknowledge:acknowledge,activity:function(id,args){return page(snapshot(id).activity.slice().reverse(),args);},pendingCount:function(id){return Object.values(snapshot(id).reports).filter(function(r){return r.status==='pending';}).length;}};
|
|
166
188
|
}
|
|
167
189
|
module.exports={attachUserKnowledgeService:attachUserKnowledgeService};
|
package/lib/users-auth.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
var crypto = require("crypto");
|
|
2
|
+
var serverPin = require('./users-server-pin');
|
|
2
3
|
|
|
3
4
|
function attachAuth(deps) {
|
|
4
5
|
var loadUsers = deps.loadUsers;
|
|
@@ -80,7 +81,29 @@ function attachAuth(deps) {
|
|
|
80
81
|
// --- Pin hashing ---
|
|
81
82
|
|
|
82
83
|
function hashPin(pin) {
|
|
83
|
-
|
|
84
|
+
var salt = crypto.randomBytes(16).toString("hex");
|
|
85
|
+
var hash = crypto.scryptSync("clay-user:" + pin, salt, 64, { N: 16384, r: 8, p: 1 }).toString("hex");
|
|
86
|
+
return "scrypt-v1:" + salt + ":" + hash;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function verifyPin(pin, storedHash, user) {
|
|
90
|
+
// Older JSON clients could submit a numeric PIN; preserve that spelling.
|
|
91
|
+
if (typeof pin === "number" && Number.isSafeInteger(pin) && pin >= 0) pin = String(pin);
|
|
92
|
+
if (typeof pin !== "string" || typeof storedHash !== "string") return false;
|
|
93
|
+
var migratedHash = serverPin.resolve(storedHash, user, deps.loadConfig);
|
|
94
|
+
if (migratedHash) return serverPin.verify(pin, migratedHash);
|
|
95
|
+
var expected;
|
|
96
|
+
var actual;
|
|
97
|
+
if (/^[a-f0-9]{64}$/.test(storedHash)) {
|
|
98
|
+
expected = Buffer.from(storedHash, "hex");
|
|
99
|
+
actual = crypto.createHash("sha256").update("clay-user:" + pin).digest();
|
|
100
|
+
} else {
|
|
101
|
+
var parts = /^scrypt-v1:([a-f0-9]{32}):([a-f0-9]{128})$/.exec(storedHash);
|
|
102
|
+
if (!parts) return false;
|
|
103
|
+
expected = Buffer.from(parts[2], "hex");
|
|
104
|
+
actual = crypto.scryptSync("clay-user:" + pin, parts[1], 64, { N: 16384, r: 8, p: 1 });
|
|
105
|
+
}
|
|
106
|
+
return crypto.timingSafeEqual(actual, expected);
|
|
84
107
|
}
|
|
85
108
|
|
|
86
109
|
// Generate a random 6-digit PIN
|
|
@@ -96,6 +119,7 @@ function attachAuth(deps) {
|
|
|
96
119
|
// --- Authentication ---
|
|
97
120
|
|
|
98
121
|
function authenticateUser(username, pin) {
|
|
122
|
+
if (typeof username !== "string") return null;
|
|
99
123
|
var data = loadUsers();
|
|
100
124
|
var user = null;
|
|
101
125
|
for (var i = 0; i < data.users.length; i++) {
|
|
@@ -105,8 +129,18 @@ function attachAuth(deps) {
|
|
|
105
129
|
}
|
|
106
130
|
}
|
|
107
131
|
if (!user) return null;
|
|
108
|
-
|
|
109
|
-
if (user.pinHash !==
|
|
132
|
+
if (!verifyPin(pin, user.pinHash, user)) return null;
|
|
133
|
+
if (user.pinHash.indexOf('scrypt-v1:') !== 0) {
|
|
134
|
+
var previousHash = user.pinHash;
|
|
135
|
+
try {
|
|
136
|
+
user.pinHash = hashPin(pin);
|
|
137
|
+
saveUsers(data);
|
|
138
|
+
} catch (e) {
|
|
139
|
+
// A best-effort upgrade must never reject a verified existing PIN.
|
|
140
|
+
user.pinHash = previousHash;
|
|
141
|
+
console.error("[users] PIN hash upgrade could not be saved; it will be retried on login.");
|
|
142
|
+
}
|
|
143
|
+
}
|
|
110
144
|
return user;
|
|
111
145
|
}
|
|
112
146
|
|
|
@@ -136,6 +170,7 @@ function attachAuth(deps) {
|
|
|
136
170
|
clearSetupCode: clearSetupCode,
|
|
137
171
|
validateSetupCode: validateSetupCode,
|
|
138
172
|
hashPin: hashPin,
|
|
173
|
+
verifyPin: verifyPin,
|
|
139
174
|
generatePin: generatePin,
|
|
140
175
|
authenticateUser: authenticateUser,
|
|
141
176
|
generateUserAuthToken: generateUserAuthToken,
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
var crypto = require("crypto");
|
|
2
|
+
var serverPin = require("./users-server-pin");
|
|
3
|
+
|
|
4
|
+
function attachCreation(deps) {
|
|
5
|
+
var loadUsers = deps.loadUsers;
|
|
6
|
+
var saveUsers = deps.saveUsers;
|
|
7
|
+
var generateUserId = deps.generateUserId;
|
|
8
|
+
var ensureUserBuiltinMates = deps.ensureUserBuiltinMates;
|
|
9
|
+
var auth = deps.auth;
|
|
10
|
+
|
|
11
|
+
function createUser(opts) {
|
|
12
|
+
var data = loadUsers();
|
|
13
|
+
// Check username uniqueness
|
|
14
|
+
for (var i = 0; i < data.users.length; i++) {
|
|
15
|
+
if (data.users[i].username.toLowerCase() === opts.username.toLowerCase()) {
|
|
16
|
+
return { error: "This username is already taken" };
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
// Check email uniqueness (when provided)
|
|
20
|
+
if (opts.email) {
|
|
21
|
+
for (var i = 0; i < data.users.length; i++) {
|
|
22
|
+
if (data.users[i].email && data.users[i].email.toLowerCase() === opts.email.toLowerCase()) {
|
|
23
|
+
return { error: "This email is already registered" };
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
var user = {
|
|
28
|
+
id: generateUserId(),
|
|
29
|
+
username: opts.username,
|
|
30
|
+
email: opts.email || null,
|
|
31
|
+
displayName: opts.displayName || opts.username,
|
|
32
|
+
pinHash: auth.hashPin(opts.pin),
|
|
33
|
+
role: opts.role || "user",
|
|
34
|
+
mustChangePin: !!opts.mustChangePin,
|
|
35
|
+
createdAt: Date.now(),
|
|
36
|
+
linuxUser: opts.linuxUser || null,
|
|
37
|
+
profile: opts.profile || {
|
|
38
|
+
name: opts.displayName || opts.username,
|
|
39
|
+
lang: "en-US",
|
|
40
|
+
avatarColor: "#5857fc",
|
|
41
|
+
avatarStyle: "imprint",
|
|
42
|
+
avatarSeed: crypto.randomBytes(4).toString("hex"),
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
data.users.push(user);
|
|
46
|
+
saveUsers(data);
|
|
47
|
+
|
|
48
|
+
// Seed immediately for ordinary multi-user storage. OS-user provisioning
|
|
49
|
+
// calls this again after the Linux mapping exists, targeting its final home.
|
|
50
|
+
ensureUserBuiltinMates(user.id);
|
|
51
|
+
|
|
52
|
+
return { ok: true, user: user };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function createUserWithoutPin(opts, migration) {
|
|
56
|
+
var data = loadUsers();
|
|
57
|
+
for (var i = 0; i < data.users.length; i++) {
|
|
58
|
+
if (data.users[i].username.toLowerCase() === opts.username.toLowerCase()) {
|
|
59
|
+
return { error: "This username is already taken" };
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
if (opts.email) {
|
|
63
|
+
for (var i = 0; i < data.users.length; i++) {
|
|
64
|
+
if (data.users[i].email && data.users[i].email.toLowerCase() === opts.email.toLowerCase()) {
|
|
65
|
+
return { error: "This email is already registered" };
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
var user = {
|
|
70
|
+
id: generateUserId(),
|
|
71
|
+
username: opts.username,
|
|
72
|
+
email: opts.email || null,
|
|
73
|
+
displayName: opts.displayName || opts.username,
|
|
74
|
+
pinHash: migration && migration.pinHash ? serverPin.tag(migration.pinHash) : null,
|
|
75
|
+
role: opts.role || "user",
|
|
76
|
+
createdAt: Date.now(),
|
|
77
|
+
profile: opts.profile || {
|
|
78
|
+
name: opts.displayName || opts.username,
|
|
79
|
+
lang: "en-US",
|
|
80
|
+
avatarColor: "#5857fc",
|
|
81
|
+
avatarStyle: "imprint",
|
|
82
|
+
avatarSeed: crypto.randomBytes(4).toString("hex"),
|
|
83
|
+
},
|
|
84
|
+
};
|
|
85
|
+
data.users.push(user);
|
|
86
|
+
if (migration) { data.multiUser = true; data.setupCode = null; }
|
|
87
|
+
saveUsers(data);
|
|
88
|
+
|
|
89
|
+
ensureUserBuiltinMates(user.id);
|
|
90
|
+
|
|
91
|
+
return { ok: true, user: user };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return { createUser: createUser, createUserWithoutPin: createUserWithoutPin };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
module.exports = { attachCreation: attachCreation };
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
var crypto = require('crypto');
|
|
2
|
+
var PREFIX = 'server-pin-v1:';
|
|
3
|
+
|
|
4
|
+
function isServerHash(hash) {
|
|
5
|
+
return typeof hash === 'string' && (/^[a-f0-9]{64}$/.test(hash) || /^[a-f0-9]{32}:[a-f0-9]{128}$/.test(hash));
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
function tag(hash) {
|
|
9
|
+
if (!isServerHash(hash)) throw new Error('Invalid legacy server PIN hash');
|
|
10
|
+
return PREFIX + hash;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function resolve(hash, user, loadConfig) {
|
|
14
|
+
if (typeof hash !== 'string') return null;
|
|
15
|
+
if (hash.indexOf(PREFIX) === 0) {
|
|
16
|
+
var original = hash.slice(PREFIX.length);
|
|
17
|
+
return isServerHash(original) ? original : null;
|
|
18
|
+
}
|
|
19
|
+
if (!user || !isServerHash(hash) || typeof loadConfig !== 'function') return null;
|
|
20
|
+
// Only the recorded migrated owner may use an untagged server credential.
|
|
21
|
+
var config;
|
|
22
|
+
try { config = loadConfig(); } catch (e) { return null; }
|
|
23
|
+
if (config && config.singleUserMigratedAt && config.singleUserMigratedUserId === user.id && config.pinHash === hash) return hash;
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function verify(pin, hash) {
|
|
28
|
+
if (!isServerHash(hash)) return false;
|
|
29
|
+
var parts = hash.split(':');
|
|
30
|
+
var expected = Buffer.from(parts.length === 2 ? parts[1] : hash, 'hex');
|
|
31
|
+
var actual = parts.length === 2
|
|
32
|
+
? crypto.scryptSync('clay:' + pin, parts[0], 64)
|
|
33
|
+
: crypto.createHash('sha256').update('clay:' + pin).digest();
|
|
34
|
+
return crypto.timingSafeEqual(actual, expected);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
module.exports = { tag: tag, resolve: resolve, verify: verify };
|
package/lib/users.js
CHANGED
|
@@ -71,50 +71,6 @@ function ensureUserBuiltinMates(userId) {
|
|
|
71
71
|
}
|
|
72
72
|
}
|
|
73
73
|
|
|
74
|
-
function createUser(opts) {
|
|
75
|
-
var data = loadUsers();
|
|
76
|
-
// Check username uniqueness
|
|
77
|
-
for (var i = 0; i < data.users.length; i++) {
|
|
78
|
-
if (data.users[i].username.toLowerCase() === opts.username.toLowerCase()) {
|
|
79
|
-
return { error: "This username is already taken" };
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
// Check email uniqueness (when provided)
|
|
83
|
-
if (opts.email) {
|
|
84
|
-
for (var i = 0; i < data.users.length; i++) {
|
|
85
|
-
if (data.users[i].email && data.users[i].email.toLowerCase() === opts.email.toLowerCase()) {
|
|
86
|
-
return { error: "This email is already registered" };
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
var user = {
|
|
91
|
-
id: generateUserId(),
|
|
92
|
-
username: opts.username,
|
|
93
|
-
email: opts.email || null,
|
|
94
|
-
displayName: opts.displayName || opts.username,
|
|
95
|
-
pinHash: auth.hashPin(opts.pin),
|
|
96
|
-
role: opts.role || "user",
|
|
97
|
-
mustChangePin: !!opts.mustChangePin,
|
|
98
|
-
createdAt: Date.now(),
|
|
99
|
-
linuxUser: opts.linuxUser || null,
|
|
100
|
-
profile: opts.profile || {
|
|
101
|
-
name: opts.displayName || opts.username,
|
|
102
|
-
lang: "en-US",
|
|
103
|
-
avatarColor: "#5857fc",
|
|
104
|
-
avatarStyle: "imprint",
|
|
105
|
-
avatarSeed: crypto.randomBytes(4).toString("hex"),
|
|
106
|
-
},
|
|
107
|
-
};
|
|
108
|
-
data.users.push(user);
|
|
109
|
-
saveUsers(data);
|
|
110
|
-
|
|
111
|
-
// Seed immediately for ordinary multi-user storage. OS-user provisioning
|
|
112
|
-
// calls this again after the Linux mapping exists, targeting its final home.
|
|
113
|
-
ensureUserBuiltinMates(user.id);
|
|
114
|
-
|
|
115
|
-
return { ok: true, user: user };
|
|
116
|
-
}
|
|
117
|
-
|
|
118
74
|
function createAdmin(opts) {
|
|
119
75
|
return createUser({
|
|
120
76
|
username: opts.username,
|
|
@@ -188,7 +144,7 @@ function getSoloNoPinUser() {
|
|
|
188
144
|
var data = loadUsers();
|
|
189
145
|
if (!data.users || data.users.length !== 1) return null;
|
|
190
146
|
var u = data.users[0];
|
|
191
|
-
if (u.pinHash) return null;
|
|
147
|
+
if (u.pinHash || u.passwordHash || data.accountAuthMethod === 'password') return null;
|
|
192
148
|
return u;
|
|
193
149
|
}
|
|
194
150
|
|
|
@@ -224,6 +180,11 @@ function updateUserPin(userId, newPin) {
|
|
|
224
180
|
for (var i = 0; i < data.users.length; i++) {
|
|
225
181
|
if (data.users[i].id === userId) {
|
|
226
182
|
data.users[i].pinHash = auth.hashPin(newPin);
|
|
183
|
+
if (data.accountAuthMethod === 'password') {
|
|
184
|
+
delete data.users[i].passwordHash;
|
|
185
|
+
delete data.users[i].passkeys;
|
|
186
|
+
delete data.users[i].passkeyUserHandle;
|
|
187
|
+
}
|
|
227
188
|
data.users[i].mustChangePin = false;
|
|
228
189
|
saveUsers(data);
|
|
229
190
|
return { ok: true };
|
|
@@ -306,44 +267,6 @@ function createInvite(createdByUserId, targetEmail) {
|
|
|
306
267
|
return invite;
|
|
307
268
|
}
|
|
308
269
|
|
|
309
|
-
function createUserWithoutPin(opts) {
|
|
310
|
-
var data = loadUsers();
|
|
311
|
-
for (var i = 0; i < data.users.length; i++) {
|
|
312
|
-
if (data.users[i].username.toLowerCase() === opts.username.toLowerCase()) {
|
|
313
|
-
return { error: "This username is already taken" };
|
|
314
|
-
}
|
|
315
|
-
}
|
|
316
|
-
if (opts.email) {
|
|
317
|
-
for (var i = 0; i < data.users.length; i++) {
|
|
318
|
-
if (data.users[i].email && data.users[i].email.toLowerCase() === opts.email.toLowerCase()) {
|
|
319
|
-
return { error: "This email is already registered" };
|
|
320
|
-
}
|
|
321
|
-
}
|
|
322
|
-
}
|
|
323
|
-
var user = {
|
|
324
|
-
id: generateUserId(),
|
|
325
|
-
username: opts.username,
|
|
326
|
-
email: opts.email || null,
|
|
327
|
-
displayName: opts.displayName || opts.username,
|
|
328
|
-
pinHash: null,
|
|
329
|
-
role: opts.role || "user",
|
|
330
|
-
createdAt: Date.now(),
|
|
331
|
-
profile: opts.profile || {
|
|
332
|
-
name: opts.displayName || opts.username,
|
|
333
|
-
lang: "en-US",
|
|
334
|
-
avatarColor: "#5857fc",
|
|
335
|
-
avatarStyle: "imprint",
|
|
336
|
-
avatarSeed: crypto.randomBytes(4).toString("hex"),
|
|
337
|
-
},
|
|
338
|
-
};
|
|
339
|
-
data.users.push(user);
|
|
340
|
-
saveUsers(data);
|
|
341
|
-
|
|
342
|
-
ensureUserBuiltinMates(user.id);
|
|
343
|
-
|
|
344
|
-
return { ok: true, user: user };
|
|
345
|
-
}
|
|
346
|
-
|
|
347
270
|
function findInvite(code) {
|
|
348
271
|
var data = loadUsers();
|
|
349
272
|
for (var i = 0; i < data.invites.length; i++) {
|
|
@@ -400,7 +323,10 @@ function removeExpiredInvites() {
|
|
|
400
323
|
|
|
401
324
|
// --- Wire extracted modules ---
|
|
402
325
|
|
|
403
|
-
var auth = attachAuth({ loadUsers: loadUsers, saveUsers: saveUsers, findAdmin: findAdmin });
|
|
326
|
+
var auth = attachAuth({ loadUsers: loadUsers, saveUsers: saveUsers, findAdmin: findAdmin, loadConfig: require('./config').loadConfig });
|
|
327
|
+
var creation = require("./users-create").attachCreation({ loadUsers: loadUsers, saveUsers: saveUsers, generateUserId: generateUserId, ensureUserBuiltinMates: ensureUserBuiltinMates, auth: auth });
|
|
328
|
+
var createUser = creation.createUser;
|
|
329
|
+
var createUserWithoutPin = creation.createUserWithoutPin;
|
|
404
330
|
var permissions = attachPermissions({ loadUsers: loadUsers, saveUsers: saveUsers, findUserById: findUserById });
|
|
405
331
|
var preferences = attachPreferences({ loadUsers: loadUsers, saveUsers: saveUsers });
|
|
406
332
|
var projectMateFilterPreferences = attachProjectMateFilterPreferences({ loadUsers: loadUsers, saveUsers: saveUsers });
|
|
@@ -489,6 +415,7 @@ module.exports = {
|
|
|
489
415
|
validateSetupCode: validateSetupCode,
|
|
490
416
|
generateUserId: generateUserId,
|
|
491
417
|
hashPin: hashPin,
|
|
418
|
+
verifyPin: auth.verifyPin,
|
|
492
419
|
createUser: createUser,
|
|
493
420
|
createAdmin: createAdmin,
|
|
494
421
|
findAdmin: findAdmin,
|
|
@@ -168,6 +168,13 @@ function attachWorkspaceQueryService(ctx) {
|
|
|
168
168
|
} else if (source.session.ownerId) {
|
|
169
169
|
return null;
|
|
170
170
|
}
|
|
171
|
+
if (source.viewerId !== undefined) {
|
|
172
|
+
var viewerPrincipal = {
|
|
173
|
+
userId: isMultiUser() ? source.viewerId : null,
|
|
174
|
+
singleUserOwnerId: isMultiUser() ? null : source.viewerId,
|
|
175
|
+
};
|
|
176
|
+
if (!ownsSession(viewerPrincipal, source.session)) return null;
|
|
177
|
+
}
|
|
171
178
|
return createBound({
|
|
172
179
|
userId: isMultiUser() ? source.session.ownerId : null,
|
|
173
180
|
sourceProjectSlug: source.projectSlug,
|
|
@@ -366,11 +373,13 @@ function attachWorkspaceQueryService(ctx) {
|
|
|
366
373
|
|
|
367
374
|
function resolveSessionNavigation(principal, ref) {
|
|
368
375
|
if (typeof ref !== "string" || !/^session:[A-Za-z0-9_-]{24}$/.test(ref)) throw new Error("Invalid session reference.");
|
|
376
|
+
var entryIndex = arguments.length > 2 ? arguments[2] : null;
|
|
377
|
+
if (entryIndex !== null && entryIndex !== undefined && (!Number.isSafeInteger(entryIndex) || entryIndex < 0)) throw new Error("Invalid session entry reference.");
|
|
369
378
|
var projects = collect(principal);
|
|
370
379
|
for (var p = 0; p < projects.length; p++) {
|
|
371
380
|
var session = findSession(projects[p], ref);
|
|
372
381
|
if (!session) continue;
|
|
373
|
-
|
|
382
|
+
var target = {
|
|
374
383
|
projectSlug: projects[p].projectSlug,
|
|
375
384
|
projectTitle: projects[p].projectTitle,
|
|
376
385
|
isMate: projects[p].isMate,
|
|
@@ -379,6 +388,8 @@ function attachWorkspaceQueryService(ctx) {
|
|
|
379
388
|
homeSessionId: session.cliSessionId || "local:" + session.localId,
|
|
380
389
|
title: cleanMetadata(session.title || "New Session", 160),
|
|
381
390
|
};
|
|
391
|
+
if (entryIndex !== null && entryIndex !== undefined) target.entryIndex = entryIndex;
|
|
392
|
+
return target;
|
|
382
393
|
}
|
|
383
394
|
throw new Error("Session not found in the owned workspace.");
|
|
384
395
|
}
|
|
@@ -450,7 +461,7 @@ function attachWorkspaceQueryService(ctx) {
|
|
|
450
461
|
if (!principal.isClay) throw new Error("Workspace-wide activity is available only to builtin Clay.");
|
|
451
462
|
return workspaceActivity(principal, args || {});
|
|
452
463
|
},
|
|
453
|
-
resolveSessionNavigation: function (args) { return resolveSessionNavigation(principal, args && args.sessionRef); },
|
|
464
|
+
resolveSessionNavigation: function (args) { return resolveSessionNavigation(principal, args && args.sessionRef, args && args.entryIndex); },
|
|
454
465
|
listRecentDecisions: function (args) {
|
|
455
466
|
if (!principal.isClay) throw new Error("Workspace decisions are available only to builtin Clay.");
|
|
456
467
|
return recentDecisions(principal, args || {});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "clay-server",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.2.0-beta.2",
|
|
4
4
|
"description": "Self-hosted team workspace for Claude Code and Codex. Multi-user, browser-based, with persistent AI mates.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"clay-server": "./bin/cli.js"
|
|
@@ -60,6 +60,7 @@
|
|
|
60
60
|
"@modelcontextprotocol/sdk": "^1.32.1",
|
|
61
61
|
"@openai/codex": "^0.162.1",
|
|
62
62
|
"@seald-io/nedb": "^4.1.2",
|
|
63
|
+
"@simplewebauthn/server": "13.3.3",
|
|
63
64
|
"codemirror": "^5.65.21",
|
|
64
65
|
"imapflow": "^1.7.8",
|
|
65
66
|
"ipaddr.js": "^2.5.0",
|