@chrysb/alphaclaw 0.9.16 → 0.9.18

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 (42) hide show
  1. package/README.md +25 -0
  2. package/lib/public/css/tailwind.generated.css +1 -1
  3. package/lib/public/dist/app.bundle.js +1858 -1758
  4. package/lib/public/js/components/agents-tab/agent-overview/model-card.js +59 -7
  5. package/lib/public/js/components/agents-tab/agent-overview/use-model-card.js +124 -0
  6. package/lib/public/js/components/api-feature-panel.js +76 -0
  7. package/lib/public/js/components/envars.js +1 -1
  8. package/lib/public/js/components/general/index.js +6 -0
  9. package/lib/public/js/components/general/use-general-tab.js +69 -0
  10. package/lib/public/js/components/row-accessory-select.js +52 -0
  11. package/lib/public/js/lib/api.js +26 -0
  12. package/lib/public/js/lib/model-catalog.js +6 -0
  13. package/lib/public/js/lib/model-config.js +12 -7
  14. package/lib/public/js/lib/storage-keys.js +4 -0
  15. package/lib/public/js/lib/thinking-levels.js +37 -0
  16. package/lib/server/agents/agents.js +33 -7
  17. package/lib/server/agents/channels.js +4 -2
  18. package/lib/server/alphaclaw-config.js +99 -0
  19. package/lib/server/chat-ws.js +4 -1
  20. package/lib/server/constants.js +73 -0
  21. package/lib/server/cost-utils.js +2 -0
  22. package/lib/server/db/auth/index.js +147 -0
  23. package/lib/server/db/auth/schema.js +17 -0
  24. package/lib/server/gateway.js +321 -20
  25. package/lib/server/helpers.js +1 -3
  26. package/lib/server/init/register-server-routes.js +45 -18
  27. package/lib/server/init/runtime-init.js +4 -0
  28. package/lib/server/init/server-lifecycle.js +1 -24
  29. package/lib/server/login-throttle.js +261 -60
  30. package/lib/server/model-catalog-bootstrap.json +5 -0
  31. package/lib/server/onboarding/index.js +2 -2
  32. package/lib/server/onboarding/openclaw.js +27 -3
  33. package/lib/server/openclaw-thinking.js +103 -0
  34. package/lib/server/openclaw-version.js +1 -1
  35. package/lib/server/routes/agents.js +10 -3
  36. package/lib/server/routes/models.js +35 -1
  37. package/lib/server/routes/onboarding.js +2 -2
  38. package/lib/server/routes/proxy.js +219 -1
  39. package/lib/server/routes/system.js +63 -2
  40. package/lib/server/usage-tracker-config.js +52 -1
  41. package/lib/server.js +60 -22
  42. package/package.json +2 -2
@@ -0,0 +1,37 @@
1
+ const kThinkingLevelLabelOverrides = {
2
+ off: "Off",
3
+ on: "On",
4
+ minimal: "Minimal",
5
+ low: "Low",
6
+ medium: "Medium",
7
+ high: "High",
8
+ adaptive: "Adaptive",
9
+ xhigh: "Extra high",
10
+ max: "Maximum",
11
+ };
12
+
13
+ export const formatThinkingLevelLabel = (levelId = "") => {
14
+ const normalized = String(levelId || "").trim().toLowerCase();
15
+ if (!normalized) return "";
16
+ if (kThinkingLevelLabelOverrides[normalized]) {
17
+ return kThinkingLevelLabelOverrides[normalized];
18
+ }
19
+ return normalized
20
+ .split(/[-_]/g)
21
+ .filter(Boolean)
22
+ .map((part) => part.charAt(0).toUpperCase() + part.slice(1))
23
+ .join(" ");
24
+ };
25
+
26
+ export const formatInheritedThinkingLabel = (levelId = "") => {
27
+ const label = formatThinkingLevelLabel(levelId);
28
+ return label ? `Inherited: ${label}` : "Inherited";
29
+ };
30
+
31
+ export const shouldShowThinkingLevelSelect = (levels = []) => {
32
+ const normalized = (Array.isArray(levels) ? levels : [])
33
+ .map((entry) => String(entry?.id || entry || "").trim().toLowerCase())
34
+ .filter(Boolean);
35
+ if (normalized.length === 0) return false;
36
+ return !(normalized.length === 1 && normalized[0] === "off");
37
+ };
@@ -1,4 +1,5 @@
1
1
  const path = require("path");
2
+ const { normalizeThinkingDefaultValue } = require("../openclaw-thinking");
2
3
 
3
4
  const {
4
5
  kDefaultAgentId,
@@ -42,11 +43,25 @@ const toReadableAgent = (agent = {}) => ({
42
43
  });
43
44
 
44
45
  const createAgentsDomain = ({ fsImpl, OPENCLAW_DIR }) => {
45
- const listAgents = () => {
46
- const cfg = withNormalizedAgentsConfig({
46
+ const readAgentsConfig = () =>
47
+ withNormalizedAgentsConfig({
47
48
  OPENCLAW_DIR,
48
49
  cfg: loadConfig({ fsImpl, OPENCLAW_DIR }),
49
50
  });
51
+
52
+ const getAgentDefaults = () => {
53
+ const cfg = readAgentsConfig();
54
+ const thinkingDefault = cfg.agents?.defaults?.thinkingDefault;
55
+ return {
56
+ thinkingDefault:
57
+ typeof thinkingDefault === "string" && thinkingDefault.trim()
58
+ ? thinkingDefault.trim()
59
+ : null,
60
+ };
61
+ };
62
+
63
+ const listAgents = () => {
64
+ const cfg = readAgentsConfig();
50
65
  return (cfg.agents?.list || []).map((entry) => toReadableAgent(entry));
51
66
  };
52
67
 
@@ -131,12 +146,9 @@ const createAgentsDomain = ({ fsImpl, OPENCLAW_DIR }) => {
131
146
  return toReadableAgent(nextAgent);
132
147
  };
133
148
 
134
- const updateAgent = (agentId, patch = {}) => {
149
+ const updateAgent = async (agentId, patch = {}) => {
135
150
  const normalized = String(agentId || "").trim();
136
- const cfg = withNormalizedAgentsConfig({
137
- OPENCLAW_DIR,
138
- cfg: loadConfig({ fsImpl, OPENCLAW_DIR }),
139
- });
151
+ const cfg = readAgentsConfig();
140
152
  const index = cfg.agents.list.findIndex((entry) => entry.id === normalized);
141
153
  if (index < 0) throw new Error(`Agent "${normalized}" not found`);
142
154
  const current = cfg.agents.list[index];
@@ -188,6 +200,19 @@ const createAgentsDomain = ({ fsImpl, OPENCLAW_DIR }) => {
188
200
  delete next.tools;
189
201
  }
190
202
  }
203
+ if (patch.thinkingDefault !== undefined) {
204
+ if (patch.thinkingDefault === null) {
205
+ delete next.thinkingDefault;
206
+ } else {
207
+ const normalizedThinking = await normalizeThinkingDefaultValue(
208
+ patch.thinkingDefault,
209
+ );
210
+ if (!normalizedThinking) {
211
+ throw new Error("Invalid thinkingDefault value");
212
+ }
213
+ next.thinkingDefault = normalizedThinking;
214
+ }
215
+ }
191
216
  cfg.agents.list[index] = next;
192
217
  saveConfig({ fsImpl, OPENCLAW_DIR, config: cfg });
193
218
  return toReadableAgent(next);
@@ -253,6 +278,7 @@ const createAgentsDomain = ({ fsImpl, OPENCLAW_DIR }) => {
253
278
  return {
254
279
  listAgents,
255
280
  getAgent,
281
+ getAgentDefaults,
256
282
  getAgentWorkspaceSize,
257
283
  createAgent,
258
284
  updateAgent,
@@ -270,7 +270,7 @@ const createChannelsDomain = ({
270
270
 
271
271
  const previousConfig = cloneJson(cfg);
272
272
  try {
273
- onProgress({ phase: "restarting", label: "Rebooting..." });
273
+ onProgress({ phase: "configuring", label: "Configuring..." });
274
274
  writeEnvFile(nextEnvVars);
275
275
  reloadEnv();
276
276
  assertActiveChannelTokenEnvVars({
@@ -280,7 +280,6 @@ const createChannelsDomain = ({
280
280
  }),
281
281
  envVars: nextEnvVars,
282
282
  });
283
- await restartGateway();
284
283
  const pluginEnabledCfg = withNormalizedAgentsConfig({
285
284
  OPENCLAW_DIR,
286
285
  cfg: loadConfig({ fsImpl, OPENCLAW_DIR }),
@@ -374,6 +373,8 @@ const createChannelsDomain = ({
374
373
  "Could not bind channel account",
375
374
  );
376
375
  }
376
+ onProgress({ phase: "restarting", label: "Rebooting..." });
377
+ await restartGateway();
377
378
  } catch (error) {
378
379
  try {
379
380
  await clawCmd(
@@ -760,6 +761,7 @@ const createChannelsDomain = ({
760
761
  }
761
762
 
762
763
  cleanupChannelAccountPairingFiles({ provider, accountId });
764
+ await restartGateway();
763
765
  return { ok: true };
764
766
  }
765
767
 
@@ -0,0 +1,99 @@
1
+ const fs = require("fs");
2
+ const path = require("path");
3
+
4
+ const kConfigFileName = "alphaclaw.json";
5
+ const kDefaultAlphaclawConfig = Object.freeze({
6
+ features: Object.freeze({
7
+ openaiCompatApi: Object.freeze({
8
+ enabled: false,
9
+ }),
10
+ }),
11
+ });
12
+
13
+ const resolveAlphaclawConfigPath = ({ openclawDir } = {}) =>
14
+ path.join(openclawDir || process.cwd(), kConfigFileName);
15
+
16
+ const normalizeOpenAiCompatApiFeature = (feature = {}) => ({
17
+ ...(feature && typeof feature === "object" ? feature : {}),
18
+ enabled: feature?.enabled === true,
19
+ });
20
+
21
+ const normalizeAlphaclawConfig = (raw = {}) => {
22
+ const base = raw && typeof raw === "object" && !Array.isArray(raw) ? raw : {};
23
+ const features =
24
+ base.features && typeof base.features === "object" && !Array.isArray(base.features)
25
+ ? base.features
26
+ : {};
27
+ return {
28
+ ...base,
29
+ features: {
30
+ ...features,
31
+ openaiCompatApi: normalizeOpenAiCompatApiFeature(features.openaiCompatApi),
32
+ },
33
+ };
34
+ };
35
+
36
+ const readAlphaclawConfig = ({
37
+ fsModule = fs,
38
+ openclawDir,
39
+ fallback = kDefaultAlphaclawConfig,
40
+ } = {}) => {
41
+ try {
42
+ const configPath = resolveAlphaclawConfigPath({ openclawDir });
43
+ const raw = fsModule.readFileSync(configPath, "utf8");
44
+ return normalizeAlphaclawConfig(JSON.parse(raw));
45
+ } catch {
46
+ return normalizeAlphaclawConfig(fallback);
47
+ }
48
+ };
49
+
50
+ const writeAlphaclawConfig = ({
51
+ fsModule = fs,
52
+ openclawDir,
53
+ config,
54
+ spacing = 2,
55
+ } = {}) => {
56
+ const configPath = resolveAlphaclawConfigPath({ openclawDir });
57
+ fsModule.mkdirSync(path.dirname(configPath), { recursive: true });
58
+ const normalized = normalizeAlphaclawConfig(config);
59
+ fsModule.writeFileSync(configPath, `${JSON.stringify(normalized, null, spacing)}\n`);
60
+ return normalized;
61
+ };
62
+
63
+ const isOpenAiCompatApiEnabled = (options = {}) =>
64
+ readAlphaclawConfig(options).features.openaiCompatApi.enabled === true;
65
+
66
+ const updateOpenAiCompatApiFeature = ({
67
+ fsModule = fs,
68
+ openclawDir,
69
+ enabled,
70
+ } = {}) => {
71
+ const current = readAlphaclawConfig({ fsModule, openclawDir });
72
+ const next = normalizeAlphaclawConfig({
73
+ ...current,
74
+ features: {
75
+ ...current.features,
76
+ openaiCompatApi: {
77
+ ...current.features.openaiCompatApi,
78
+ enabled: enabled === true,
79
+ },
80
+ },
81
+ });
82
+ const changed =
83
+ current.features.openaiCompatApi.enabled !== next.features.openaiCompatApi.enabled;
84
+ return {
85
+ config: writeAlphaclawConfig({ fsModule, openclawDir, config: next }),
86
+ changed,
87
+ };
88
+ };
89
+
90
+ module.exports = {
91
+ kConfigFileName,
92
+ kDefaultAlphaclawConfig,
93
+ isOpenAiCompatApiEnabled,
94
+ normalizeAlphaclawConfig,
95
+ readAlphaclawConfig,
96
+ resolveAlphaclawConfigPath,
97
+ updateOpenAiCompatApiFeature,
98
+ writeAlphaclawConfig,
99
+ };
@@ -6,7 +6,7 @@ const kEnvRefPattern = /^\$\{([A-Z0-9_]+)\}$/i;
6
6
  const kConnectTimeoutMs = 8000;
7
7
  const kHistoryTimeoutMs = 12000;
8
8
  const kGatewayReqTimeoutMs = 15000;
9
- const kGatewayProtocolVersion = 3;
9
+ const kGatewayProtocolVersion = 4;
10
10
  // Gateway method auth (see OpenClaw method-scopes): chat.history needs operator.read;
11
11
  // chat.send / chat.abort need operator.write. Align with CLI_DEFAULT_OPERATOR_SCOPES plus admin.
12
12
  const kGatewayChatBridgeScopes = [
@@ -283,6 +283,9 @@ const sanitizeError = (error) => {
283
283
  ) {
284
284
  return "Gateway authentication failed. Verify OPENCLAW_GATEWAY_TOKEN matches the gateway.";
285
285
  }
286
+ if (lower.includes("protocol mismatch")) {
287
+ return "Chat cannot connect to the gateway (protocol version mismatch). Update AlphaClaw to match your OpenClaw version.";
288
+ }
286
289
  if (lower.includes("method not found") || lower.includes("unknown method")) {
287
290
  return "This gateway build does not support chat APIs. Update OpenClaw.";
288
291
  }
@@ -54,6 +54,22 @@ const kLoginMaxLockMs = parsePositiveInt(
54
54
  process.env.LOGIN_RATE_MAX_LOCK_MS,
55
55
  15 * 60 * 1000,
56
56
  );
57
+ const kLoginGlobalWindowMs = parsePositiveInt(
58
+ process.env.LOGIN_RATE_GLOBAL_WINDOW_MS,
59
+ kLoginWindowMs,
60
+ );
61
+ const kLoginGlobalMaxAttempts = parsePositiveInt(
62
+ process.env.LOGIN_RATE_GLOBAL_MAX_ATTEMPTS,
63
+ Math.max(kLoginMaxAttempts * 5, 25),
64
+ );
65
+ const kLoginGlobalBaseLockMs = parsePositiveInt(
66
+ process.env.LOGIN_RATE_GLOBAL_BASE_LOCK_MS,
67
+ kLoginBaseLockMs,
68
+ );
69
+ const kLoginGlobalMaxLockMs = parsePositiveInt(
70
+ process.env.LOGIN_RATE_GLOBAL_MAX_LOCK_MS,
71
+ kLoginMaxLockMs,
72
+ );
57
73
  const kLoginCleanupIntervalMs = parsePositiveInt(
58
74
  process.env.LOGIN_RATE_CLEANUP_INTERVAL_MS,
59
75
  60 * 1000,
@@ -65,6 +81,45 @@ const kLoginStateTtlMs = Math.max(
65
81
  ),
66
82
  kLoginMaxLockMs,
67
83
  );
84
+ const kOpenAiCompatApiRateWindowMs = parsePositiveInt(
85
+ process.env.OPENAI_COMPAT_API_RATE_WINDOW_MS,
86
+ kLoginWindowMs,
87
+ );
88
+ const kOpenAiCompatApiRateMaxAttempts = parsePositiveInt(
89
+ process.env.OPENAI_COMPAT_API_RATE_MAX_ATTEMPTS,
90
+ 10,
91
+ );
92
+ const kOpenAiCompatApiRateBaseLockMs = parsePositiveInt(
93
+ process.env.OPENAI_COMPAT_API_RATE_BASE_LOCK_MS,
94
+ kLoginBaseLockMs,
95
+ );
96
+ const kOpenAiCompatApiRateMaxLockMs = parsePositiveInt(
97
+ process.env.OPENAI_COMPAT_API_RATE_MAX_LOCK_MS,
98
+ kLoginMaxLockMs,
99
+ );
100
+ const kOpenAiCompatApiRateGlobalWindowMs = parsePositiveInt(
101
+ process.env.OPENAI_COMPAT_API_RATE_GLOBAL_WINDOW_MS,
102
+ kOpenAiCompatApiRateWindowMs,
103
+ );
104
+ const kOpenAiCompatApiRateGlobalMaxAttempts = parsePositiveInt(
105
+ process.env.OPENAI_COMPAT_API_RATE_GLOBAL_MAX_ATTEMPTS,
106
+ Math.max(kOpenAiCompatApiRateMaxAttempts * 10, 100),
107
+ );
108
+ const kOpenAiCompatApiRateGlobalBaseLockMs = parsePositiveInt(
109
+ process.env.OPENAI_COMPAT_API_RATE_GLOBAL_BASE_LOCK_MS,
110
+ kOpenAiCompatApiRateBaseLockMs,
111
+ );
112
+ const kOpenAiCompatApiRateGlobalMaxLockMs = parsePositiveInt(
113
+ process.env.OPENAI_COMPAT_API_RATE_GLOBAL_MAX_LOCK_MS,
114
+ kOpenAiCompatApiRateMaxLockMs,
115
+ );
116
+ const kOpenAiCompatApiRateStateTtlMs = Math.max(
117
+ parsePositiveInt(
118
+ process.env.OPENAI_COMPAT_API_RATE_STATE_TTL_MS,
119
+ Math.max(kOpenAiCompatApiRateWindowMs, kOpenAiCompatApiRateMaxLockMs) * 3,
120
+ ),
121
+ kOpenAiCompatApiRateMaxLockMs,
122
+ );
68
123
 
69
124
  const kOnboardingModelProviders = new Set([
70
125
  "anthropic",
@@ -92,6 +147,11 @@ const kOnboardingModelProviders = new Set([
92
147
  "vllm",
93
148
  ]);
94
149
  const kMinimalFallbackOnboardingModels = [
150
+ {
151
+ key: "anthropic/claude-opus-4-8",
152
+ provider: "anthropic",
153
+ label: "Claude Opus 4.8",
154
+ },
95
155
  {
96
156
  key: "anthropic/claude-opus-4-7",
97
157
  provider: "anthropic",
@@ -445,8 +505,21 @@ module.exports = {
445
505
  kLoginMaxAttempts,
446
506
  kLoginBaseLockMs,
447
507
  kLoginMaxLockMs,
508
+ kLoginGlobalWindowMs,
509
+ kLoginGlobalMaxAttempts,
510
+ kLoginGlobalBaseLockMs,
511
+ kLoginGlobalMaxLockMs,
448
512
  kLoginCleanupIntervalMs,
449
513
  kLoginStateTtlMs,
514
+ kOpenAiCompatApiRateWindowMs,
515
+ kOpenAiCompatApiRateMaxAttempts,
516
+ kOpenAiCompatApiRateBaseLockMs,
517
+ kOpenAiCompatApiRateMaxLockMs,
518
+ kOpenAiCompatApiRateGlobalWindowMs,
519
+ kOpenAiCompatApiRateGlobalMaxAttempts,
520
+ kOpenAiCompatApiRateGlobalBaseLockMs,
521
+ kOpenAiCompatApiRateGlobalMaxLockMs,
522
+ kOpenAiCompatApiRateStateTtlMs,
450
523
  kOnboardingModelProviders,
451
524
  kFallbackOnboardingModels,
452
525
  kVersionCacheTtlMs,
@@ -13,6 +13,8 @@ const kClaudeOpus47Pricing = {
13
13
  };
14
14
 
15
15
  const kGlobalModelPricing = {
16
+ "claude-opus-4-8": kClaudeOpus47Pricing,
17
+ "claude-opus-4.8": kClaudeOpus47Pricing,
16
18
  "claude-opus-4-7": kClaudeOpus47Pricing,
17
19
  "claude-opus-4.7": kClaudeOpus47Pricing,
18
20
  "claude-opus-4-6": {
@@ -0,0 +1,147 @@
1
+ const fs = require("fs");
2
+ const path = require("path");
3
+ const { DatabaseSync } = require("node:sqlite");
4
+ const { createSchema } = require("./schema");
5
+
6
+ let db = null;
7
+
8
+ const ensureDb = () => {
9
+ if (!db) throw new Error("Auth DB not initialized");
10
+ return db;
11
+ };
12
+
13
+ const closeAuthDb = () => {
14
+ if (!db) return;
15
+ const database = db;
16
+ db = null;
17
+ database.close();
18
+ };
19
+
20
+ const initAuthDb = ({ rootDir }) => {
21
+ closeAuthDb();
22
+ const dbDir = path.join(rootDir, "db");
23
+ fs.mkdirSync(dbDir, { recursive: true });
24
+ const dbPath = path.join(dbDir, "auth.db");
25
+ db = new DatabaseSync(dbPath);
26
+ db.exec("PRAGMA busy_timeout = 5000");
27
+ createSchema(db);
28
+ return { path: dbPath };
29
+ };
30
+
31
+ const toStateModel = (row) => {
32
+ if (!row) return null;
33
+ return {
34
+ attempts: Number(row.attempts || 0),
35
+ windowStart: Number(row.window_start || 0),
36
+ lockUntil: Number(row.lock_until || 0),
37
+ failStreak: Number(row.fail_streak || 0),
38
+ lastSeenAt: Number(row.last_seen_at || 0),
39
+ };
40
+ };
41
+
42
+ const createLoginThrottleStore = () => ({
43
+ get: (stateKey) => {
44
+ const row = ensureDb()
45
+ .prepare(
46
+ `
47
+ SELECT
48
+ attempts,
49
+ window_start,
50
+ lock_until,
51
+ fail_streak,
52
+ last_seen_at
53
+ FROM login_throttle_states
54
+ WHERE state_key = $state_key
55
+ LIMIT 1
56
+ `,
57
+ )
58
+ .get({ $state_key: String(stateKey || "") });
59
+ return toStateModel(row);
60
+ },
61
+
62
+ set: (stateKey, state) => {
63
+ ensureDb()
64
+ .prepare(
65
+ `
66
+ INSERT INTO login_throttle_states (
67
+ state_key,
68
+ attempts,
69
+ window_start,
70
+ lock_until,
71
+ fail_streak,
72
+ last_seen_at
73
+ ) VALUES (
74
+ $state_key,
75
+ $attempts,
76
+ $window_start,
77
+ $lock_until,
78
+ $fail_streak,
79
+ $last_seen_at
80
+ )
81
+ ON CONFLICT(state_key) DO UPDATE SET
82
+ attempts = excluded.attempts,
83
+ window_start = excluded.window_start,
84
+ lock_until = excluded.lock_until,
85
+ fail_streak = excluded.fail_streak,
86
+ last_seen_at = excluded.last_seen_at
87
+ `,
88
+ )
89
+ .run({
90
+ $state_key: String(stateKey || ""),
91
+ $attempts: Number(state?.attempts || 0),
92
+ $window_start: Number(state?.windowStart || 0),
93
+ $lock_until: Number(state?.lockUntil || 0),
94
+ $fail_streak: Number(state?.failStreak || 0),
95
+ $last_seen_at: Number(state?.lastSeenAt || 0),
96
+ });
97
+ },
98
+
99
+ delete: (stateKey) => {
100
+ ensureDb()
101
+ .prepare(
102
+ `
103
+ DELETE FROM login_throttle_states
104
+ WHERE state_key = $state_key
105
+ `,
106
+ )
107
+ .run({ $state_key: String(stateKey || "") });
108
+ },
109
+
110
+ entries: () =>
111
+ ensureDb()
112
+ .prepare(
113
+ `
114
+ SELECT
115
+ state_key,
116
+ attempts,
117
+ window_start,
118
+ lock_until,
119
+ fail_streak,
120
+ last_seen_at
121
+ FROM login_throttle_states
122
+ `,
123
+ )
124
+ .all()
125
+ .map((row) => [String(row.state_key || ""), toStateModel(row)]),
126
+
127
+ runExclusive: (callback) => {
128
+ const database = ensureDb();
129
+ database.exec("BEGIN IMMEDIATE");
130
+ try {
131
+ const result = callback();
132
+ database.exec("COMMIT");
133
+ return result;
134
+ } catch (err) {
135
+ try {
136
+ database.exec("ROLLBACK");
137
+ } catch {}
138
+ throw err;
139
+ }
140
+ },
141
+ });
142
+
143
+ module.exports = {
144
+ initAuthDb,
145
+ closeAuthDb,
146
+ createLoginThrottleStore,
147
+ };
@@ -0,0 +1,17 @@
1
+ const createSchema = (db) => {
2
+ db.exec(`
3
+ CREATE TABLE IF NOT EXISTS login_throttle_states (
4
+ state_key TEXT PRIMARY KEY,
5
+ attempts INTEGER NOT NULL DEFAULT 0,
6
+ window_start INTEGER NOT NULL,
7
+ lock_until INTEGER NOT NULL DEFAULT 0,
8
+ fail_streak INTEGER NOT NULL DEFAULT 0,
9
+ last_seen_at INTEGER NOT NULL
10
+ );
11
+
12
+ CREATE INDEX IF NOT EXISTS idx_login_throttle_states_last_seen
13
+ ON login_throttle_states(last_seen_at);
14
+ `);
15
+ };
16
+
17
+ module.exports = { createSchema };