@standardagents/builder 0.17.2 → 0.18.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.
package/dist/index.js CHANGED
@@ -19696,8 +19696,10 @@ import { isThreadEndpoint } from "@standardagents/spec";
19696
19696
  const PUBLIC_ROUTES = [
19697
19697
  '/api/auth/bootstrap',
19698
19698
  '/api/auth/login',
19699
- '/api/auth/bootstrap',
19700
19699
  '/api/auth/config',
19700
+ '/api/auth/platform-replica',
19701
+ '/api/auth/sa/start', // Login with Standard Agents (OAuth) \u2014 unauthenticated entry
19702
+ '/api/auth/sa/callback', // OAuth callback (sets the session cookie)
19701
19703
  '/api/config',
19702
19704
  '/api/auth/oauth/github',
19703
19705
  '/api/auth/oauth/google',
@@ -19710,15 +19712,31 @@ const PUBLIC_ROUTES = [
19710
19712
  '/api/hooks' // Hook metadata is safe to expose publicly
19711
19713
  ];
19712
19714
 
19715
+ // True when the platform deployed this instance (injects STANDARD_AGENTS_HOSTED).
19716
+ // Hosted instances are internet-reachable and multi-tenant, so the thread data
19717
+ // API and event/stream WebSockets must NOT be anonymously public the way they
19718
+ // are in single-user local dev \u2014 they require a session (admin) or API key (SDK).
19719
+ function isHostedInstance(env) {
19720
+ const value = env && env.STANDARD_AGENTS_HOSTED;
19721
+ if (typeof value === 'string') {
19722
+ const trimmed = value.trim().toLowerCase();
19723
+ return trimmed !== '' && trimmed !== '0' && trimmed !== 'false';
19724
+ }
19725
+ return Boolean(value);
19726
+ }
19727
+
19713
19728
  // Check if a route is public (no auth required)
19714
- function isPublicRoute(routePath) {
19729
+ function isPublicRoute(routePath, hosted) {
19715
19730
  // Exact match for auth routes
19716
19731
  if (PUBLIC_ROUTES.includes(routePath)) {
19717
19732
  return true;
19718
19733
  }
19719
19734
 
19720
- // Thread routes are always public
19721
- if (routePath.startsWith('/api/threads/') || routePath === '/api/threads') {
19735
+ // Thread routes (REST + message/log stream WebSockets) are public in local
19736
+ // single-user dev, but on a hosted deployment they require auth \u2014 requireAuth
19737
+ // accepts the admin's session (cookie or token) or the SDK's API key, so this
19738
+ // only blocks anonymous access to another tenant's threads/messages/files.
19739
+ if (!hosted && (routePath.startsWith('/api/threads/') || routePath === '/api/threads')) {
19722
19740
  return true;
19723
19741
  }
19724
19742
 
@@ -19727,16 +19745,25 @@ function isPublicRoute(routePath) {
19727
19745
  return true;
19728
19746
  }
19729
19747
 
19730
- // 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
+ }
19731
19752
  if (routePath.startsWith('/api/platform/') || routePath === '/api/platform') {
19732
19753
  return true;
19733
19754
  }
19734
19755
 
19735
- // 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
+ }
19736
19760
  if (routePath.startsWith('/api/platform-session/') || routePath === '/api/platform-session') {
19737
19761
  return true;
19738
19762
  }
19739
19763
 
19764
+ if (hosted && (routePath.startsWith('/api/platform-auth/') || routePath === '/api/platform-auth')) {
19765
+ return false;
19766
+ }
19740
19767
  if (routePath.startsWith('/api/platform-auth/') || routePath === '/api/platform-auth') {
19741
19768
  return true;
19742
19769
  }
@@ -19744,6 +19771,36 @@ function isPublicRoute(routePath) {
19744
19771
  return false;
19745
19772
  }
19746
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
+
19747
19804
  // CORS headers for API responses
19748
19805
  const CORS_HEADERS = {
19749
19806
  "Access-Control-Allow-Origin": "*",
@@ -19820,7 +19877,7 @@ ${packedThreadRouteCode}
19820
19877
 
19821
19878
  if (routeMatch) {
19822
19879
  // Check if authentication is required for this route
19823
- const publicRoute = isPublicRoute(routePath);
19880
+ const publicRoute = isPublicRoute(routePath, isHostedInstance(env));
19824
19881
  const isApiRoute = routePath.startsWith('/api/');
19825
19882
 
19826
19883
  let authContext = null;
@@ -19835,6 +19892,21 @@ ${packedThreadRouteCode}
19835
19892
  }
19836
19893
 
19837
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
+ }
19838
19910
  }
19839
19911
 
19840
19912
  let controller = await routeMatch.data();
@@ -19870,6 +19942,19 @@ ${packedThreadRouteCode}
19870
19942
  });
19871
19943
  }
19872
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
+
19873
19958
  // Serve UI for all other routes (SPA fallback)
19874
19959
  return serveUI(routePath, env);
19875
19960
  }
@@ -20573,11 +20658,24 @@ async function hashToken(token) {
20573
20658
  const hashArray = new Uint8Array(hashBuffer);
20574
20659
  return Array.from(hashArray, (byte) => byte.toString(16).padStart(2, "0")).join("");
20575
20660
  }
20661
+ var SESSION_COOKIE_NAME = "agtuser_session";
20662
+ function readSessionCookie(request) {
20663
+ const header = request.headers.get("Cookie");
20664
+ if (!header) return null;
20665
+ for (const part of header.split(";")) {
20666
+ const eq = part.indexOf("=");
20667
+ if (eq === -1) continue;
20668
+ if (part.slice(0, eq).trim() === SESSION_COOKIE_NAME) {
20669
+ return decodeURIComponent(part.slice(eq + 1).trim()) || null;
20670
+ }
20671
+ }
20672
+ return null;
20673
+ }
20576
20674
  function isValidUserToken(token) {
20577
20675
  return token.startsWith("agtuser_") && token.length > 10;
20578
20676
  }
20579
20677
  function isValidApiKey(key) {
20580
- return key.startsWith("agtbldr_") && key.length > 10;
20678
+ return key.startsWith("agtbldr_") && key.length > 10 || key.startsWith("sak_live_") && key.length > 10;
20581
20679
  }
20582
20680
  async function verifySignedToken(signedToken, encryptionKey) {
20583
20681
  try {
@@ -20646,6 +20744,10 @@ function extractBearerToken(request) {
20646
20744
  if (authHeader && authHeader.startsWith("Bearer ")) {
20647
20745
  return authHeader.substring(7);
20648
20746
  }
20747
+ const cookieToken = readSessionCookie(request);
20748
+ if (cookieToken) {
20749
+ return cookieToken;
20750
+ }
20649
20751
  const isWebSocket = request.headers.get("upgrade")?.toLowerCase() === "websocket";
20650
20752
  if (isWebSocket) {
20651
20753
  try {
@@ -20697,7 +20799,11 @@ async function authenticate(request, env) {
20697
20799
  user: {
20698
20800
  id: user.id,
20699
20801
  username: user.username,
20700
- 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
20701
20807
  },
20702
20808
  authType: "session"
20703
20809
  };
@@ -20715,7 +20821,11 @@ async function authenticate(request, env) {
20715
20821
  user: {
20716
20822
  id: user.id,
20717
20823
  username: user.username,
20718
- 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
20719
20829
  },
20720
20830
  authType: "api_key"
20721
20831
  };
@@ -23505,9 +23615,9 @@ var DurableThread = class extends DurableObject {
23505
23615
  * Each migration is run in order, starting from the current version + 1.
23506
23616
  */
23507
23617
  async runMigrations(fromVersion) {
23508
- for (const migration37 of migrations) {
23509
- if (migration37.version > fromVersion) {
23510
- 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);
23511
23621
  }
23512
23622
  }
23513
23623
  }
@@ -26942,9 +27052,38 @@ var migration36 = {
26942
27052
  }
26943
27053
  };
26944
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
+
26945
27084
  // src/durable-objects/agentbuilder-migrations/index.ts
26946
- var migrations2 = [migration31, migration32, migration33, migration34, migration35, migration36];
26947
- var LATEST_SCHEMA_VERSION2 = 6;
27085
+ var migrations2 = [migration31, migration32, migration33, migration34, migration35, migration36, migration37];
27086
+ var LATEST_SCHEMA_VERSION2 = 7;
26948
27087
 
26949
27088
  // src/utils/crypto.ts
26950
27089
  var CryptoUtil = class {
@@ -27568,9 +27707,9 @@ ${result ?? error ?? "No result content."}${attachmentSummary}`;
27568
27707
  }
27569
27708
  }
27570
27709
  async runMigrations(fromVersion) {
27571
- for (const migration37 of migrations2) {
27572
- if (migration37.version > fromVersion) {
27573
- 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);
27574
27713
  }
27575
27714
  }
27576
27715
  }
@@ -28603,27 +28742,54 @@ ${result ?? error ?? "No result content."}${attachmentSummary}`;
28603
28742
  // ============================================================
28604
28743
  // User Authentication Methods
28605
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
+ }
28606
28778
  /**
28607
28779
  * Get a user by username.
28608
28780
  */
28609
28781
  async getUserByUsername(username) {
28610
28782
  await this.ensureMigrated();
28611
28783
  const cursor = await this.ctx.storage.sql.exec(
28612
- `SELECT id, username, password_hash, role, created_at, updated_at
28613
- 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`,
28614
28788
  username
28615
28789
  );
28616
28790
  const rows = cursor.toArray();
28617
28791
  if (rows.length === 0) return null;
28618
- const row = rows[0];
28619
- return {
28620
- id: row.id,
28621
- username: row.username,
28622
- password_hash: row.password_hash,
28623
- role: row.role,
28624
- created_at: row.created_at,
28625
- updated_at: row.updated_at
28626
- };
28792
+ return this.userFromRow(rows[0]);
28627
28793
  }
28628
28794
  /**
28629
28795
  * Get a user by ID.
@@ -28631,21 +28797,15 @@ ${result ?? error ?? "No result content."}${attachmentSummary}`;
28631
28797
  async getUserById(id) {
28632
28798
  await this.ensureMigrated();
28633
28799
  const cursor = await this.ctx.storage.sql.exec(
28634
- `SELECT id, username, password_hash, role, created_at, updated_at
28635
- 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`,
28636
28804
  id
28637
28805
  );
28638
28806
  const rows = cursor.toArray();
28639
28807
  if (rows.length === 0) return null;
28640
- const row = rows[0];
28641
- return {
28642
- id: row.id,
28643
- username: row.username,
28644
- password_hash: row.password_hash,
28645
- role: row.role,
28646
- created_at: row.created_at,
28647
- updated_at: row.updated_at
28648
- };
28808
+ return this.userFromRow(rows[0]);
28649
28809
  }
28650
28810
  /**
28651
28811
  * Create a new user.
@@ -28655,12 +28815,23 @@ ${result ?? error ?? "No result content."}${attachmentSummary}`;
28655
28815
  const id = crypto.randomUUID();
28656
28816
  const now = Math.floor(Date.now() / 1e3);
28657
28817
  await this.ctx.storage.sql.exec(
28658
- `INSERT INTO users (id, username, password_hash, role, created_at, updated_at)
28659
- 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 (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
28660
28823
  id,
28661
28824
  params.username,
28662
28825
  params.password_hash,
28826
+ "admin",
28663
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,
28664
28835
  now,
28665
28836
  now
28666
28837
  );
@@ -28669,6 +28840,12 @@ ${result ?? error ?? "No result content."}${attachmentSummary}`;
28669
28840
  username: params.username,
28670
28841
  password_hash: params.password_hash,
28671
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,
28672
28849
  created_at: now,
28673
28850
  updated_at: now
28674
28851
  };
@@ -28688,11 +28865,24 @@ ${result ?? error ?? "No result content."}${attachmentSummary}`;
28688
28865
  */
28689
28866
  async listUsers() {
28690
28867
  await this.ensureMigrated();
28691
- 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
+ );
28692
28876
  return cursor.toArray().map((row) => ({
28693
28877
  id: row.id,
28694
28878
  username: row.username,
28695
- 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,
28696
28886
  created_at: row.created_at,
28697
28887
  updated_at: row.updated_at
28698
28888
  }));
@@ -28716,14 +28906,42 @@ ${result ?? error ?? "No result content."}${attachmentSummary}`;
28716
28906
  values.push(params.password_hash);
28717
28907
  }
28718
28908
  if (params.role !== void 0) {
28719
- updates.push("role = ?");
28909
+ updates.push("instance_role = ?");
28720
28910
  values.push(params.role);
28721
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
+ }
28722
28936
  if (updates.length === 0) {
28723
28937
  return existing;
28724
28938
  }
28725
28939
  updates.push("updated_at = ?");
28726
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
+ }
28727
28945
  values.push(id);
28728
28946
  await this.ctx.storage.sql.exec(
28729
28947
  `UPDATE users SET ${updates.join(", ")} WHERE id = ?`,
@@ -28744,9 +28962,223 @@ ${result ?? error ?? "No result content."}${attachmentSummary}`;
28744
28962
  `DELETE FROM api_keys WHERE user_id = ?`,
28745
28963
  id
28746
28964
  );
28747
- 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
+ );
28748
28970
  return true;
28749
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
+ }
28750
29182
  // ============================================================
28751
29183
  // Session Methods
28752
29184
  // ============================================================
@@ -28775,8 +29207,13 @@ ${result ?? error ?? "No result content."}${attachmentSummary}`;
28775
29207
  await this.ensureMigrated();
28776
29208
  const now = Math.floor(Date.now() / 1e3);
28777
29209
  const cursor = await this.ctx.storage.sql.exec(
28778
- `SELECT user_id, expires_at FROM sessions
28779
- 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`,
28780
29217
  tokenHash,
28781
29218
  now
28782
29219
  );
@@ -28833,14 +29270,23 @@ ${result ?? error ?? "No result content."}${attachmentSummary}`;
28833
29270
  */
28834
29271
  async validateApiKey(keyHash) {
28835
29272
  await this.ensureMigrated();
28836
- 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
+ );
28837
29283
  const rows = cursor.toArray();
28838
29284
  if (rows.length === 0) {
28839
29285
  return null;
28840
29286
  }
28841
29287
  const now = Math.floor(Date.now() / 1e3);
28842
29288
  await this.ctx.storage.sql.exec(
28843
- `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`,
28844
29290
  now,
28845
29291
  keyHash
28846
29292
  );
@@ -28852,8 +29298,10 @@ ${result ?? error ?? "No result content."}${attachmentSummary}`;
28852
29298
  async listApiKeys(userId) {
28853
29299
  await this.ensureMigrated();
28854
29300
  const cursor = await this.ctx.storage.sql.exec(
28855
- `SELECT id, name, key_prefix, last_five, created_at, last_used_at
28856
- 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`,
28857
29305
  userId
28858
29306
  );
28859
29307
  return cursor.toArray();