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.
Files changed (61) hide show
  1. package/lib/account-auth-policy.js +21 -0
  2. package/lib/account-auth-socket.js +29 -0
  3. package/lib/account-auth-throttle.js +44 -0
  4. package/lib/account-auth-transport.js +48 -0
  5. package/lib/account-login-page.js +28 -0
  6. package/lib/account-passkey-challenges.js +31 -0
  7. package/lib/account-passkey-page.js +89 -0
  8. package/lib/account-passkey-policy.js +39 -0
  9. package/lib/account-password-page.js +69 -0
  10. package/lib/account-passwords.js +36 -0
  11. package/lib/account-remembered.js +77 -0
  12. package/lib/account-session-page.js +51 -0
  13. package/lib/account-session-store.js +82 -0
  14. package/lib/daemon.js +2 -0
  15. package/lib/migrate-single-user.js +3 -23
  16. package/lib/project-connection.js +1 -1
  17. package/lib/project-server-settings.js +1 -0
  18. package/lib/project-sessions.js +3 -1
  19. package/lib/project-user-knowledge.js +2 -1
  20. package/lib/public/app.js +2 -0
  21. package/lib/public/auth/account-login.css +98 -0
  22. package/lib/public/auth/account-login.js +104 -0
  23. package/lib/public/css/command-palette.css +12 -33
  24. package/lib/public/css/user-settings-sessions.css +42 -0
  25. package/lib/public/index.html +17 -5
  26. package/lib/public/modules/account-auth-settings.js +71 -0
  27. package/lib/public/modules/account-transport-settings.js +51 -0
  28. package/lib/public/modules/app-header.js +3 -2
  29. package/lib/public/modules/app-messages.js +16 -4
  30. package/lib/public/modules/app-rendering.js +4 -3
  31. package/lib/public/modules/chat-render-runtime.js +8 -0
  32. package/lib/public/modules/clay-log-links.js +23 -0
  33. package/lib/public/modules/command-palette.js +94 -5
  34. package/lib/public/modules/markdown.js +1 -1
  35. package/lib/public/modules/passkey-browser.js +69 -0
  36. package/lib/public/modules/passkey-server-settings.js +57 -0
  37. package/lib/public/modules/project-logs-render.js +3 -2
  38. package/lib/public/modules/server-settings-controls.js +10 -0
  39. package/lib/public/modules/server-settings.js +7 -2
  40. package/lib/public/modules/user-settings-account.js +14 -1
  41. package/lib/public/modules/user-settings-sessions.js +120 -0
  42. package/lib/public/modules/user-settings.js +3 -0
  43. package/lib/public/modules/you-activity.js +3 -1
  44. package/lib/public/modules/you.js +91 -6
  45. package/lib/public/style.css +1 -0
  46. package/lib/server-account-passkeys.js +186 -0
  47. package/lib/server-account-passwords.js +127 -0
  48. package/lib/server-account-security.js +53 -0
  49. package/lib/server-auth.js +107 -79
  50. package/lib/server-home-chat.js +3 -3
  51. package/lib/server-home-clay-session-links.js +17 -9
  52. package/lib/server-settings.js +2 -1
  53. package/lib/server.js +3 -1
  54. package/lib/sessions.js +3 -1
  55. package/lib/user-knowledge-service.js +23 -1
  56. package/lib/users-auth.js +38 -3
  57. package/lib/users-create.js +97 -0
  58. package/lib/users-server-pin.js +37 -0
  59. package/lib/users.js +11 -84
  60. package/lib/workspace-query-service.js +13 -2
  61. 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
- return crypto.createHash("sha256").update("clay-user:" + pin).digest("hex");
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
- var pinH = hashPin(pin);
109
- if (user.pinHash !== pinH) return null;
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
- return {
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.1.1-beta.1",
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",