@standardagents/builder 0.17.3 → 0.18.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.
package/dist/index.js CHANGED
@@ -19697,6 +19697,7 @@ const PUBLIC_ROUTES = [
19697
19697
  '/api/auth/bootstrap',
19698
19698
  '/api/auth/login',
19699
19699
  '/api/auth/config',
19700
+ '/api/auth/platform-replica',
19700
19701
  '/api/auth/sa/start', // Login with Standard Agents (OAuth) \u2014 unauthenticated entry
19701
19702
  '/api/auth/sa/callback', // OAuth callback (sets the session cookie)
19702
19703
  '/api/config',
@@ -19744,16 +19745,25 @@ function isPublicRoute(routePath, hosted) {
19744
19745
  return true;
19745
19746
  }
19746
19747
 
19747
- // Platform proxy routes handle their own auth.
19748
+ // Platform proxy routes handle their own auth in local dev only.
19749
+ if (hosted && (routePath.startsWith('/api/platform/') || routePath === '/api/platform')) {
19750
+ return false;
19751
+ }
19748
19752
  if (routePath.startsWith('/api/platform/') || routePath === '/api/platform') {
19749
19753
  return true;
19750
19754
  }
19751
19755
 
19752
- // Platform session proxy and auth bridge handle auth via platform cookies.
19756
+ // Platform session proxy and auth bridge are local-dev helpers only.
19757
+ if (hosted && (routePath.startsWith('/api/platform-session/') || routePath === '/api/platform-session')) {
19758
+ return false;
19759
+ }
19753
19760
  if (routePath.startsWith('/api/platform-session/') || routePath === '/api/platform-session') {
19754
19761
  return true;
19755
19762
  }
19756
19763
 
19764
+ if (hosted && (routePath.startsWith('/api/platform-auth/') || routePath === '/api/platform-auth')) {
19765
+ return false;
19766
+ }
19757
19767
  if (routePath.startsWith('/api/platform-auth/') || routePath === '/api/platform-auth') {
19758
19768
  return true;
19759
19769
  }
@@ -19761,6 +19771,36 @@ function isPublicRoute(routePath, hosted) {
19761
19771
  return false;
19762
19772
  }
19763
19773
 
19774
+ function platformEndpoint(env) {
19775
+ const configured =
19776
+ env && (env.PLATFORM_ENDPOINT || env.STANDARD_AGENTS_PLATFORM_URL || env.PLATFORM_URL || env.STANDARD_AGENTS_PUBLIC_URL);
19777
+ if (typeof configured === 'string' && configured.trim()) {
19778
+ return configured.trim().replace(/\\/+$/, '');
19779
+ }
19780
+ return 'https://platform.standardagents.ai';
19781
+ }
19782
+
19783
+ function hostedInstanceRedirectId(request, env) {
19784
+ const configured = env && (env.STANDARD_AGENTS_PROJECT_ID || env.STANDARD_AGENTS_INSTANCE_ID || env.STANDARD_AGENTS_INSTANCE_SUBDOMAIN);
19785
+ if (typeof configured === 'string' && configured.trim()) {
19786
+ return configured.trim();
19787
+ }
19788
+ return new URL(request.url).hostname;
19789
+ }
19790
+
19791
+ function platformLoginUrl(request, env) {
19792
+ const requestUrl = new URL(request.url);
19793
+ const url = new URL('/login', platformEndpoint(env));
19794
+ url.searchParams.set('redirect', hostedInstanceRedirectId(request, env));
19795
+ url.searchParams.set('return_to', requestUrl.pathname + requestUrl.search || '/');
19796
+ return url.toString();
19797
+ }
19798
+
19799
+ function isHtmlNavigationRequest(request) {
19800
+ if (request.method !== 'GET' && request.method !== 'HEAD') return false;
19801
+ return (request.headers.get('Accept') || '').includes('text/html');
19802
+ }
19803
+
19764
19804
  // CORS headers for API responses
19765
19805
  const CORS_HEADERS = {
19766
19806
  "Access-Control-Allow-Origin": "*",
@@ -19852,6 +19892,21 @@ ${packedThreadRouteCode}
19852
19892
  }
19853
19893
 
19854
19894
  authContext = authResult;
19895
+
19896
+ if (routePath.startsWith('/api/threads/')) {
19897
+ const threadId = routeMatch.params?.id || routeMatch.params?.threadId;
19898
+ if (threadId) {
19899
+ const agentBuilderId = env.AGENT_BUILDER.idFromName('singleton');
19900
+ const agentBuilder = env.AGENT_BUILDER.get(agentBuilderId);
19901
+ const thread = await agentBuilder.getThread(threadId);
19902
+ if (!thread) {
19903
+ return addCorsHeaders(Response.json({ error: \`Thread not found: \${threadId}\` }, { status: 404 }));
19904
+ }
19905
+ if (authContext.user.role !== 'admin' && (thread.user_id === null || thread.user_id !== authContext.user.id)) {
19906
+ return addCorsHeaders(Response.json({ error: "Forbidden: You don't have access to this thread" }, { status: 403 }));
19907
+ }
19908
+ }
19909
+ }
19855
19910
  }
19856
19911
 
19857
19912
  let controller = await routeMatch.data();
@@ -19887,6 +19942,19 @@ ${packedThreadRouteCode}
19887
19942
  });
19888
19943
  }
19889
19944
 
19945
+ // Hosted browser navigations do not render a local login page. Redirect
19946
+ // anonymous users directly to the platform, where the instance membership is
19947
+ // resolved and returned as a signed handoff token.
19948
+ if (isHostedInstance(env) && isHtmlNavigationRequest(request)) {
19949
+ const authResult = await requireAuth(request, env);
19950
+ if (authResult instanceof Response) {
19951
+ return new Response(null, {
19952
+ status: 302,
19953
+ headers: { Location: platformLoginUrl(request, env) },
19954
+ });
19955
+ }
19956
+ }
19957
+
19890
19958
  // Serve UI for all other routes (SPA fallback)
19891
19959
  return serveUI(routePath, env);
19892
19960
  }
@@ -20607,7 +20675,7 @@ function isValidUserToken(token) {
20607
20675
  return token.startsWith("agtuser_") && token.length > 10;
20608
20676
  }
20609
20677
  function isValidApiKey(key) {
20610
- return key.startsWith("agtbldr_") && key.length > 10;
20678
+ return key.startsWith("agtbldr_") && key.length > 10 || key.startsWith("sak_live_") && key.length > 10;
20611
20679
  }
20612
20680
  async function verifySignedToken(signedToken, encryptionKey) {
20613
20681
  try {
@@ -20731,7 +20799,11 @@ async function authenticate(request, env) {
20731
20799
  user: {
20732
20800
  id: user.id,
20733
20801
  username: user.username,
20734
- role: user.role
20802
+ role: user.role,
20803
+ platform_user_id: user.platform_user_id ?? null,
20804
+ email: user.email ?? null,
20805
+ display_name: user.display_name ?? null,
20806
+ avatar_url: user.avatar_url ?? null
20735
20807
  },
20736
20808
  authType: "session"
20737
20809
  };
@@ -20749,7 +20821,11 @@ async function authenticate(request, env) {
20749
20821
  user: {
20750
20822
  id: user.id,
20751
20823
  username: user.username,
20752
- role: user.role
20824
+ role: user.role,
20825
+ platform_user_id: user.platform_user_id ?? null,
20826
+ email: user.email ?? null,
20827
+ display_name: user.display_name ?? null,
20828
+ avatar_url: user.avatar_url ?? null
20753
20829
  },
20754
20830
  authType: "api_key"
20755
20831
  };
@@ -23539,9 +23615,9 @@ var DurableThread = class extends DurableObject {
23539
23615
  * Each migration is run in order, starting from the current version + 1.
23540
23616
  */
23541
23617
  async runMigrations(fromVersion) {
23542
- for (const migration37 of migrations) {
23543
- if (migration37.version > fromVersion) {
23544
- await migration37.up(this.ctx.storage.sql);
23618
+ for (const migration38 of migrations) {
23619
+ if (migration38.version > fromVersion) {
23620
+ await migration38.up(this.ctx.storage.sql);
23545
23621
  }
23546
23622
  }
23547
23623
  }
@@ -26976,9 +27052,38 @@ var migration36 = {
26976
27052
  }
26977
27053
  };
26978
27054
 
27055
+ // src/durable-objects/agentbuilder-migrations/0007_platform_identity_replica.ts
27056
+ var migration37 = {
27057
+ version: 7,
27058
+ async up(sql) {
27059
+ sql.exec(`ALTER TABLE users ADD COLUMN platform_user_id TEXT`);
27060
+ sql.exec(`ALTER TABLE users ADD COLUMN email TEXT`);
27061
+ sql.exec(`ALTER TABLE users ADD COLUMN display_name TEXT`);
27062
+ sql.exec(`ALTER TABLE users ADD COLUMN avatar_url TEXT`);
27063
+ sql.exec(`ALTER TABLE users ADD COLUMN instance_role TEXT NOT NULL DEFAULT 'admin' CHECK (instance_role IN ('admin', 'user'))`);
27064
+ sql.exec(`ALTER TABLE users ADD COLUMN source TEXT NOT NULL DEFAULT 'local' CHECK (source IN ('local', 'standard_agents'))`);
27065
+ sql.exec(`ALTER TABLE users ADD COLUMN replica_active INTEGER NOT NULL DEFAULT 1`);
27066
+ sql.exec(`ALTER TABLE users ADD COLUMN replica_updated_at INTEGER`);
27067
+ sql.exec(`ALTER TABLE users ADD COLUMN deleted_at INTEGER`);
27068
+ sql.exec(`CREATE INDEX IF NOT EXISTS idx_users_platform_user_id ON users(platform_user_id)`);
27069
+ sql.exec(`CREATE INDEX IF NOT EXISTS idx_users_replica_active ON users(replica_active)`);
27070
+ sql.exec(`CREATE INDEX IF NOT EXISTS idx_users_instance_role ON users(instance_role)`);
27071
+ sql.exec(`ALTER TABLE api_keys ADD COLUMN platform_key_id TEXT`);
27072
+ sql.exec(`ALTER TABLE api_keys ADD COLUMN scope TEXT`);
27073
+ sql.exec(`ALTER TABLE api_keys ADD COLUMN source TEXT NOT NULL DEFAULT 'local' CHECK (source IN ('local', 'standard_agents'))`);
27074
+ sql.exec(`ALTER TABLE api_keys ADD COLUMN replica_active INTEGER NOT NULL DEFAULT 1`);
27075
+ sql.exec(`ALTER TABLE api_keys ADD COLUMN replica_updated_at INTEGER`);
27076
+ sql.exec(`CREATE INDEX IF NOT EXISTS idx_api_keys_platform_key_id ON api_keys(platform_key_id)`);
27077
+ sql.exec(`CREATE INDEX IF NOT EXISTS idx_api_keys_replica_active ON api_keys(replica_active)`);
27078
+ sql.exec(`
27079
+ INSERT OR REPLACE INTO _metadata (key, value) VALUES ('schema_version', '7')
27080
+ `);
27081
+ }
27082
+ };
27083
+
26979
27084
  // src/durable-objects/agentbuilder-migrations/index.ts
26980
- var migrations2 = [migration31, migration32, migration33, migration34, migration35, migration36];
26981
- var LATEST_SCHEMA_VERSION2 = 6;
27085
+ var migrations2 = [migration31, migration32, migration33, migration34, migration35, migration36, migration37];
27086
+ var LATEST_SCHEMA_VERSION2 = 7;
26982
27087
 
26983
27088
  // src/utils/crypto.ts
26984
27089
  var CryptoUtil = class {
@@ -27602,9 +27707,9 @@ ${result ?? error ?? "No result content."}${attachmentSummary}`;
27602
27707
  }
27603
27708
  }
27604
27709
  async runMigrations(fromVersion) {
27605
- for (const migration37 of migrations2) {
27606
- if (migration37.version > fromVersion) {
27607
- await migration37.up(this.ctx.storage.sql);
27710
+ for (const migration38 of migrations2) {
27711
+ if (migration38.version > fromVersion) {
27712
+ await migration38.up(this.ctx.storage.sql);
27608
27713
  }
27609
27714
  }
27610
27715
  }
@@ -28637,27 +28742,54 @@ ${result ?? error ?? "No result content."}${attachmentSummary}`;
28637
28742
  // ============================================================
28638
28743
  // User Authentication Methods
28639
28744
  // ============================================================
28745
+ normalizeReplicaUsername(input, fallback) {
28746
+ const normalized = (input || fallback).trim().toLowerCase().replace(/[^a-z0-9_-]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 50);
28747
+ return normalized || fallback;
28748
+ }
28749
+ async availableReplicaUsername(candidate, platformUserId) {
28750
+ const existing = await this.ctx.storage.sql.exec(
28751
+ `SELECT id, platform_user_id FROM users WHERE username = ? LIMIT 1`,
28752
+ candidate
28753
+ );
28754
+ const row = existing.toArray()[0];
28755
+ if (!row || row.platform_user_id === platformUserId) {
28756
+ return candidate;
28757
+ }
28758
+ const suffix = platformUserId.replace(/[^a-zA-Z0-9]/g, "").slice(0, 8) || "user";
28759
+ const trimmed = candidate.slice(0, Math.max(1, 50 - suffix.length - 1)).replace(/-+$/g, "");
28760
+ return `${trimmed || "sa"}-${suffix}`;
28761
+ }
28762
+ userFromRow(row) {
28763
+ return {
28764
+ id: row.id,
28765
+ username: row.username,
28766
+ password_hash: row.password_hash,
28767
+ role: row.role === "user" ? "user" : "admin",
28768
+ platform_user_id: row.platform_user_id ?? null,
28769
+ email: row.email ?? null,
28770
+ display_name: row.display_name ?? null,
28771
+ avatar_url: row.avatar_url ?? null,
28772
+ source: row.source ?? "local",
28773
+ replica_active: row.replica_active ?? 1,
28774
+ created_at: row.created_at,
28775
+ updated_at: row.updated_at
28776
+ };
28777
+ }
28640
28778
  /**
28641
28779
  * Get a user by username.
28642
28780
  */
28643
28781
  async getUserByUsername(username) {
28644
28782
  await this.ensureMigrated();
28645
28783
  const cursor = await this.ctx.storage.sql.exec(
28646
- `SELECT id, username, password_hash, role, created_at, updated_at
28647
- FROM users WHERE username = ?`,
28784
+ `SELECT id, username, password_hash, COALESCE(instance_role, role) AS role,
28785
+ platform_user_id, email, display_name, avatar_url, source, replica_active,
28786
+ created_at, updated_at
28787
+ FROM users WHERE username = ? AND deleted_at IS NULL AND replica_active != 0`,
28648
28788
  username
28649
28789
  );
28650
28790
  const rows = cursor.toArray();
28651
28791
  if (rows.length === 0) return null;
28652
- const row = rows[0];
28653
- return {
28654
- id: row.id,
28655
- username: row.username,
28656
- password_hash: row.password_hash,
28657
- role: row.role,
28658
- created_at: row.created_at,
28659
- updated_at: row.updated_at
28660
- };
28792
+ return this.userFromRow(rows[0]);
28661
28793
  }
28662
28794
  /**
28663
28795
  * Get a user by ID.
@@ -28665,21 +28797,15 @@ ${result ?? error ?? "No result content."}${attachmentSummary}`;
28665
28797
  async getUserById(id) {
28666
28798
  await this.ensureMigrated();
28667
28799
  const cursor = await this.ctx.storage.sql.exec(
28668
- `SELECT id, username, password_hash, role, created_at, updated_at
28669
- FROM users WHERE id = ?`,
28800
+ `SELECT id, username, password_hash, COALESCE(instance_role, role) AS role,
28801
+ platform_user_id, email, display_name, avatar_url, source, replica_active,
28802
+ created_at, updated_at
28803
+ FROM users WHERE id = ? AND deleted_at IS NULL AND replica_active != 0`,
28670
28804
  id
28671
28805
  );
28672
28806
  const rows = cursor.toArray();
28673
28807
  if (rows.length === 0) return null;
28674
- const row = rows[0];
28675
- return {
28676
- id: row.id,
28677
- username: row.username,
28678
- password_hash: row.password_hash,
28679
- role: row.role,
28680
- created_at: row.created_at,
28681
- updated_at: row.updated_at
28682
- };
28808
+ return this.userFromRow(rows[0]);
28683
28809
  }
28684
28810
  /**
28685
28811
  * Create a new user.
@@ -28689,12 +28815,23 @@ ${result ?? error ?? "No result content."}${attachmentSummary}`;
28689
28815
  const id = crypto.randomUUID();
28690
28816
  const now = Math.floor(Date.now() / 1e3);
28691
28817
  await this.ctx.storage.sql.exec(
28692
- `INSERT INTO users (id, username, password_hash, role, created_at, updated_at)
28693
- VALUES (?, ?, ?, ?, ?, ?)`,
28818
+ `INSERT INTO users (
28819
+ id, username, password_hash, role, instance_role, platform_user_id,
28820
+ email, display_name, avatar_url, source, replica_active,
28821
+ replica_updated_at, created_at, updated_at
28822
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
28694
28823
  id,
28695
28824
  params.username,
28696
28825
  params.password_hash,
28826
+ "admin",
28697
28827
  params.role || "admin",
28828
+ params.platform_user_id ?? null,
28829
+ params.email ?? null,
28830
+ params.display_name ?? null,
28831
+ params.avatar_url ?? null,
28832
+ params.source ?? "local",
28833
+ 1,
28834
+ params.source === "standard_agents" ? now : null,
28698
28835
  now,
28699
28836
  now
28700
28837
  );
@@ -28703,6 +28840,12 @@ ${result ?? error ?? "No result content."}${attachmentSummary}`;
28703
28840
  username: params.username,
28704
28841
  password_hash: params.password_hash,
28705
28842
  role: params.role || "admin",
28843
+ platform_user_id: params.platform_user_id ?? null,
28844
+ email: params.email ?? null,
28845
+ display_name: params.display_name ?? null,
28846
+ avatar_url: params.avatar_url ?? null,
28847
+ source: params.source ?? "local",
28848
+ replica_active: 1,
28706
28849
  created_at: now,
28707
28850
  updated_at: now
28708
28851
  };
@@ -28722,11 +28865,24 @@ ${result ?? error ?? "No result content."}${attachmentSummary}`;
28722
28865
  */
28723
28866
  async listUsers() {
28724
28867
  await this.ensureMigrated();
28725
- const cursor = await this.ctx.storage.sql.exec(`SELECT id, username, role, created_at, updated_at FROM users ORDER BY created_at DESC`);
28868
+ const cursor = await this.ctx.storage.sql.exec(
28869
+ `SELECT id, username, COALESCE(instance_role, role) AS role,
28870
+ platform_user_id, email, display_name, avatar_url, source, replica_active,
28871
+ created_at, updated_at
28872
+ FROM users
28873
+ WHERE deleted_at IS NULL
28874
+ ORDER BY created_at DESC`
28875
+ );
28726
28876
  return cursor.toArray().map((row) => ({
28727
28877
  id: row.id,
28728
28878
  username: row.username,
28729
- role: row.role,
28879
+ role: row.role === "user" ? "user" : "admin",
28880
+ platform_user_id: row.platform_user_id ?? null,
28881
+ email: row.email ?? null,
28882
+ display_name: row.display_name ?? null,
28883
+ avatar_url: row.avatar_url ?? null,
28884
+ source: row.source ?? "local",
28885
+ replica_active: row.replica_active ?? 1,
28730
28886
  created_at: row.created_at,
28731
28887
  updated_at: row.updated_at
28732
28888
  }));
@@ -28750,14 +28906,42 @@ ${result ?? error ?? "No result content."}${attachmentSummary}`;
28750
28906
  values.push(params.password_hash);
28751
28907
  }
28752
28908
  if (params.role !== void 0) {
28753
- updates.push("role = ?");
28909
+ updates.push("instance_role = ?");
28754
28910
  values.push(params.role);
28755
28911
  }
28912
+ if (params.platform_user_id !== void 0) {
28913
+ updates.push("platform_user_id = ?");
28914
+ values.push(params.platform_user_id);
28915
+ }
28916
+ if (params.email !== void 0) {
28917
+ updates.push("email = ?");
28918
+ values.push(params.email);
28919
+ }
28920
+ if (params.display_name !== void 0) {
28921
+ updates.push("display_name = ?");
28922
+ values.push(params.display_name);
28923
+ }
28924
+ if (params.avatar_url !== void 0) {
28925
+ updates.push("avatar_url = ?");
28926
+ values.push(params.avatar_url);
28927
+ }
28928
+ if (params.source !== void 0) {
28929
+ updates.push("source = ?");
28930
+ values.push(params.source);
28931
+ }
28932
+ if (params.replica_active !== void 0) {
28933
+ updates.push("replica_active = ?");
28934
+ values.push(params.replica_active);
28935
+ }
28756
28936
  if (updates.length === 0) {
28757
28937
  return existing;
28758
28938
  }
28759
28939
  updates.push("updated_at = ?");
28760
28940
  values.push(now);
28941
+ if (params.source === "standard_agents" || params.replica_active !== void 0) {
28942
+ updates.push("replica_updated_at = ?");
28943
+ values.push(now);
28944
+ }
28761
28945
  values.push(id);
28762
28946
  await this.ctx.storage.sql.exec(
28763
28947
  `UPDATE users SET ${updates.join(", ")} WHERE id = ?`,
@@ -28778,9 +28962,223 @@ ${result ?? error ?? "No result content."}${attachmentSummary}`;
28778
28962
  `DELETE FROM api_keys WHERE user_id = ?`,
28779
28963
  id
28780
28964
  );
28781
- await this.ctx.storage.sql.exec(`DELETE FROM users WHERE id = ?`, id);
28965
+ await this.ctx.storage.sql.exec(
28966
+ `UPDATE users SET deleted_at = ?, replica_active = 0 WHERE id = ?`,
28967
+ Math.floor(Date.now() / 1e3),
28968
+ id
28969
+ );
28782
28970
  return true;
28783
28971
  }
28972
+ /**
28973
+ * Upsert a platform-replicated identity and return the local user row.
28974
+ */
28975
+ async upsertPlatformReplicaUser(replica) {
28976
+ await this.ensureMigrated();
28977
+ const now = Math.floor(Date.now() / 1e3);
28978
+ const fallback = this.normalizeReplicaUsername(
28979
+ replica.email?.split("@")[0] ?? null,
28980
+ `sa-${replica.platform_user_id.slice(0, 12)}`
28981
+ );
28982
+ const username = this.normalizeReplicaUsername(
28983
+ replica.username ?? replica.display_name ?? replica.email ?? null,
28984
+ fallback
28985
+ );
28986
+ const existingCursor = await this.ctx.storage.sql.exec(
28987
+ `SELECT id FROM users WHERE platform_user_id = ? LIMIT 1`,
28988
+ replica.platform_user_id
28989
+ );
28990
+ const existing = existingCursor.toArray()[0];
28991
+ const availableUsername = await this.availableReplicaUsername(username, replica.platform_user_id);
28992
+ if (existing) {
28993
+ await this.ctx.storage.sql.exec(
28994
+ `UPDATE users SET
28995
+ username = ?,
28996
+ instance_role = ?,
28997
+ email = ?,
28998
+ display_name = ?,
28999
+ avatar_url = ?,
29000
+ source = 'standard_agents',
29001
+ replica_active = 1,
29002
+ replica_updated_at = ?,
29003
+ deleted_at = NULL,
29004
+ updated_at = ?
29005
+ WHERE id = ?`,
29006
+ availableUsername,
29007
+ replica.role,
29008
+ replica.email ?? null,
29009
+ replica.display_name ?? null,
29010
+ replica.avatar_url ?? null,
29011
+ now,
29012
+ now,
29013
+ existing.id
29014
+ );
29015
+ const updated = await this.getUserById(existing.id);
29016
+ if (!updated) {
29017
+ throw new Error("Failed to load replicated user after update");
29018
+ }
29019
+ return updated;
29020
+ }
29021
+ return this.createUser({
29022
+ username: availableUsername,
29023
+ password_hash: `platform-replica:${crypto.randomUUID()}`,
29024
+ role: replica.role,
29025
+ platform_user_id: replica.platform_user_id,
29026
+ email: replica.email ?? null,
29027
+ display_name: replica.display_name ?? null,
29028
+ avatar_url: replica.avatar_url ?? null,
29029
+ source: "standard_agents"
29030
+ });
29031
+ }
29032
+ /**
29033
+ * Apply a full platform read-replica snapshot for this instance.
29034
+ */
29035
+ async applyPlatformReplicaSnapshot(snapshot) {
29036
+ await this.ensureMigrated();
29037
+ const activeUserIds = /* @__PURE__ */ new Set();
29038
+ for (const replicaUser of snapshot.users) {
29039
+ const user = await this.upsertPlatformReplicaUser(replicaUser);
29040
+ activeUserIds.add(user.id);
29041
+ }
29042
+ const activeList = Array.from(activeUserIds);
29043
+ if (activeList.length > 0) {
29044
+ const placeholders = activeList.map(() => "?").join(", ");
29045
+ const inactive = await this.ctx.storage.sql.exec(
29046
+ `SELECT id FROM users
29047
+ WHERE source = 'standard_agents'
29048
+ AND replica_active != 0
29049
+ AND id NOT IN (${placeholders})`,
29050
+ ...activeList
29051
+ );
29052
+ for (const row of inactive.toArray()) {
29053
+ await this.ctx.storage.sql.exec(`DELETE FROM sessions WHERE user_id = ?`, row.id);
29054
+ await this.ctx.storage.sql.exec(
29055
+ `UPDATE users SET replica_active = 0, deleted_at = ?, replica_updated_at = ?, updated_at = ? WHERE id = ?`,
29056
+ Math.floor(Date.now() / 1e3),
29057
+ Math.floor(Date.now() / 1e3),
29058
+ Math.floor(Date.now() / 1e3),
29059
+ row.id
29060
+ );
29061
+ }
29062
+ } else {
29063
+ const inactive = await this.ctx.storage.sql.exec(
29064
+ `SELECT id FROM users WHERE source = 'standard_agents' AND replica_active != 0`
29065
+ );
29066
+ for (const row of inactive.toArray()) {
29067
+ await this.ctx.storage.sql.exec(`DELETE FROM sessions WHERE user_id = ?`, row.id);
29068
+ }
29069
+ const now2 = Math.floor(Date.now() / 1e3);
29070
+ await this.ctx.storage.sql.exec(
29071
+ `UPDATE users SET replica_active = 0, deleted_at = ?, replica_updated_at = ?, updated_at = ?
29072
+ WHERE source = 'standard_agents'`,
29073
+ now2,
29074
+ now2,
29075
+ now2
29076
+ );
29077
+ }
29078
+ const activePlatformKeyIds = /* @__PURE__ */ new Set();
29079
+ const keys = snapshot.api_keys ?? [];
29080
+ for (const key of keys) {
29081
+ const user = key.user_platform_id ? await this.getUserByPlatformUserId(key.user_platform_id) : await this.getFirstReplicaAdminUser();
29082
+ const keyUser = user ?? await this.getFirstReplicaAdminUser();
29083
+ if (!keyUser) continue;
29084
+ activePlatformKeyIds.add(key.id);
29085
+ await this.upsertPlatformReplicaApiKey(key, keyUser.id);
29086
+ }
29087
+ const now = Math.floor(Date.now() / 1e3);
29088
+ if (activePlatformKeyIds.size > 0) {
29089
+ const ids = Array.from(activePlatformKeyIds);
29090
+ const placeholders = ids.map(() => "?").join(", ");
29091
+ await this.ctx.storage.sql.exec(
29092
+ `UPDATE api_keys SET replica_active = 0, replica_updated_at = ?
29093
+ WHERE source = 'standard_agents' AND platform_key_id NOT IN (${placeholders})`,
29094
+ now,
29095
+ ...ids
29096
+ );
29097
+ } else {
29098
+ await this.ctx.storage.sql.exec(
29099
+ `UPDATE api_keys SET replica_active = 0, replica_updated_at = ?
29100
+ WHERE source = 'standard_agents'`,
29101
+ now
29102
+ );
29103
+ }
29104
+ return { users: activeUserIds.size, api_keys: activePlatformKeyIds.size };
29105
+ }
29106
+ async getUserByPlatformUserId(platformUserId) {
29107
+ await this.ensureMigrated();
29108
+ const cursor = await this.ctx.storage.sql.exec(
29109
+ `SELECT id FROM users
29110
+ WHERE platform_user_id = ? AND deleted_at IS NULL AND replica_active != 0
29111
+ LIMIT 1`,
29112
+ platformUserId
29113
+ );
29114
+ const row = cursor.toArray()[0];
29115
+ return row ? this.getUserById(row.id) : null;
29116
+ }
29117
+ async getFirstReplicaAdminUser() {
29118
+ await this.ensureMigrated();
29119
+ const cursor = await this.ctx.storage.sql.exec(
29120
+ `SELECT id FROM users
29121
+ WHERE source = 'standard_agents'
29122
+ AND instance_role = 'admin'
29123
+ AND deleted_at IS NULL
29124
+ AND replica_active != 0
29125
+ ORDER BY created_at ASC
29126
+ LIMIT 1`
29127
+ );
29128
+ const row = cursor.toArray()[0];
29129
+ return row ? this.getUserById(row.id) : null;
29130
+ }
29131
+ async upsertPlatformReplicaApiKey(key, userId) {
29132
+ await this.ensureMigrated();
29133
+ const now = Math.floor(Date.now() / 1e3);
29134
+ const existing = await this.ctx.storage.sql.exec(
29135
+ `SELECT id FROM api_keys WHERE platform_key_id = ? LIMIT 1`,
29136
+ key.id
29137
+ );
29138
+ const row = existing.toArray()[0];
29139
+ const name = key.name || `Standard Agents ${key.key_prefix}`;
29140
+ const lastFive = key.last_five || key.key_prefix.slice(-5);
29141
+ if (row) {
29142
+ await this.ctx.storage.sql.exec(
29143
+ `UPDATE api_keys SET
29144
+ name = ?,
29145
+ key_hash = ?,
29146
+ key_prefix = ?,
29147
+ last_five = ?,
29148
+ user_id = ?,
29149
+ scope = ?,
29150
+ source = 'standard_agents',
29151
+ replica_active = 1,
29152
+ replica_updated_at = ?
29153
+ WHERE id = ?`,
29154
+ name,
29155
+ key.key_hash,
29156
+ key.key_prefix,
29157
+ lastFive,
29158
+ userId,
29159
+ key.scope ?? null,
29160
+ now,
29161
+ row.id
29162
+ );
29163
+ return;
29164
+ }
29165
+ await this.ctx.storage.sql.exec(
29166
+ `INSERT INTO api_keys (
29167
+ id, name, key_hash, key_prefix, last_five, user_id, created_at,
29168
+ platform_key_id, scope, source, replica_active, replica_updated_at
29169
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 'standard_agents', 1, ?)`,
29170
+ `platform:${key.id}`,
29171
+ name,
29172
+ key.key_hash,
29173
+ key.key_prefix,
29174
+ lastFive,
29175
+ userId,
29176
+ key.created_at ?? now,
29177
+ key.id,
29178
+ key.scope ?? null,
29179
+ now
29180
+ );
29181
+ }
28784
29182
  // ============================================================
28785
29183
  // Session Methods
28786
29184
  // ============================================================
@@ -28809,8 +29207,13 @@ ${result ?? error ?? "No result content."}${attachmentSummary}`;
28809
29207
  await this.ensureMigrated();
28810
29208
  const now = Math.floor(Date.now() / 1e3);
28811
29209
  const cursor = await this.ctx.storage.sql.exec(
28812
- `SELECT user_id, expires_at FROM sessions
28813
- WHERE token_hash = ? AND expires_at > ?`,
29210
+ `SELECT sessions.user_id, sessions.expires_at
29211
+ FROM sessions
29212
+ JOIN users ON users.id = sessions.user_id
29213
+ WHERE sessions.token_hash = ?
29214
+ AND sessions.expires_at > ?
29215
+ AND users.deleted_at IS NULL
29216
+ AND users.replica_active != 0`,
28814
29217
  tokenHash,
28815
29218
  now
28816
29219
  );
@@ -28867,14 +29270,23 @@ ${result ?? error ?? "No result content."}${attachmentSummary}`;
28867
29270
  */
28868
29271
  async validateApiKey(keyHash) {
28869
29272
  await this.ensureMigrated();
28870
- const cursor = await this.ctx.storage.sql.exec(`SELECT id, user_id FROM api_keys WHERE key_hash = ?`, keyHash);
29273
+ const cursor = await this.ctx.storage.sql.exec(
29274
+ `SELECT api_keys.id, api_keys.user_id
29275
+ FROM api_keys
29276
+ JOIN users ON users.id = api_keys.user_id
29277
+ WHERE api_keys.key_hash = ?
29278
+ AND api_keys.replica_active != 0
29279
+ AND users.deleted_at IS NULL
29280
+ AND users.replica_active != 0`,
29281
+ keyHash
29282
+ );
28871
29283
  const rows = cursor.toArray();
28872
29284
  if (rows.length === 0) {
28873
29285
  return null;
28874
29286
  }
28875
29287
  const now = Math.floor(Date.now() / 1e3);
28876
29288
  await this.ctx.storage.sql.exec(
28877
- `UPDATE api_keys SET last_used_at = ? WHERE key_hash = ?`,
29289
+ `UPDATE api_keys SET last_used_at = ? WHERE key_hash = ? AND replica_active != 0`,
28878
29290
  now,
28879
29291
  keyHash
28880
29292
  );
@@ -28886,8 +29298,10 @@ ${result ?? error ?? "No result content."}${attachmentSummary}`;
28886
29298
  async listApiKeys(userId) {
28887
29299
  await this.ensureMigrated();
28888
29300
  const cursor = await this.ctx.storage.sql.exec(
28889
- `SELECT id, name, key_prefix, last_five, created_at, last_used_at
28890
- FROM api_keys WHERE user_id = ? ORDER BY created_at DESC`,
29301
+ `SELECT id, name, key_prefix, last_five, source, created_at, last_used_at
29302
+ FROM api_keys
29303
+ WHERE user_id = ? AND replica_active != 0
29304
+ ORDER BY created_at DESC`,
28891
29305
  userId
28892
29306
  );
28893
29307
  return cursor.toArray();