@agent-native/core 0.165.2 → 0.165.3

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 (30) hide show
  1. package/corpus/templates/analytics/changelog/2026-08-20-staged-data-limit-recovery.md +1 -0
  2. package/corpus/templates/clips/app/i18n/en-US.ts +6 -2
  3. package/corpus/templates/clips/app/routes/download.tsx +111 -17
  4. package/corpus/templates/clips/changelog/2026-08-20-cross-tab-recording-controls.md +1 -0
  5. package/corpus/templates/clips/changelog/2026-08-20-overlay-follow-review-fixes.md +1 -0
  6. package/corpus/templates/clips/changelog/2026-08-20-provider-key-recovery.md +1 -0
  7. package/corpus/templates/clips/server/routes/api/clips-latest.json.get.ts +109 -46
  8. package/dist/client/blocks/library/diagram.js +1 -1
  9. package/dist/collab/routes.d.ts +1 -1
  10. package/dist/deploy/workspace-deploy.js +7 -0
  11. package/dist/mcp/screen-memory-stdio.d.ts +7 -7
  12. package/dist/mcp-client/oauth-routes.d.ts +1 -0
  13. package/dist/mcp-client/oauth-routes.js +15 -2
  14. package/dist/mcp-client/remote-store.js +83 -43
  15. package/dist/notifications/routes.d.ts +3 -3
  16. package/dist/observability/routes.d.ts +5 -5
  17. package/dist/progress/routes.d.ts +1 -1
  18. package/dist/resources/handlers.d.ts +2 -2
  19. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  20. package/dist/server/google-oauth-credentials.js +14 -1
  21. package/dist/server/realtime-token.d.ts +1 -1
  22. package/dist/server/transcribe-voice.d.ts +1 -1
  23. package/dist/settings/index.d.ts +1 -1
  24. package/dist/settings/index.js +1 -1
  25. package/dist/settings/store.d.ts +7 -1
  26. package/dist/settings/store.js +25 -3
  27. package/dist/settings/user-settings.d.ts +2 -0
  28. package/dist/settings/user-settings.js +65 -2
  29. package/docs/content/deployment.mdx +89 -14
  30. package/package.json +1 -1
@@ -137,13 +137,13 @@ export declare function screenMemoryMcpToolDefinitions(): ({
137
137
  inputSchema: {
138
138
  type: string;
139
139
  properties: {
140
+ count?: undefined;
140
141
  query?: undefined;
141
142
  minutes?: undefined;
142
143
  limit?: undefined;
143
144
  clientHint?: undefined;
144
145
  timestamp?: undefined;
145
146
  chapterId?: undefined;
146
- count?: undefined;
147
147
  startAt?: undefined;
148
148
  endAt?: undefined;
149
149
  reason?: undefined;
@@ -159,6 +159,7 @@ export declare function screenMemoryMcpToolDefinitions(): ({
159
159
  inputSchema: {
160
160
  type: string;
161
161
  properties: {
162
+ count?: undefined;
162
163
  query: {
163
164
  type: string;
164
165
  description: string;
@@ -174,7 +175,6 @@ export declare function screenMemoryMcpToolDefinitions(): ({
174
175
  clientHint?: undefined;
175
176
  timestamp?: undefined;
176
177
  chapterId?: undefined;
177
- count?: undefined;
178
178
  startAt?: undefined;
179
179
  endAt?: undefined;
180
180
  reason?: undefined;
@@ -190,6 +190,7 @@ export declare function screenMemoryMcpToolDefinitions(): ({
190
190
  inputSchema: {
191
191
  type: string;
192
192
  properties: {
193
+ count?: undefined;
193
194
  minutes: {
194
195
  type: string;
195
196
  description: string;
@@ -199,7 +200,6 @@ export declare function screenMemoryMcpToolDefinitions(): ({
199
200
  clientHint?: undefined;
200
201
  timestamp?: undefined;
201
202
  chapterId?: undefined;
202
- count?: undefined;
203
203
  startAt?: undefined;
204
204
  endAt?: undefined;
205
205
  reason?: undefined;
@@ -216,6 +216,7 @@ export declare function screenMemoryMcpToolDefinitions(): ({
216
216
  type: string;
217
217
  required: string[];
218
218
  properties: {
219
+ count?: undefined;
219
220
  query: {
220
221
  type: string;
221
222
  description: string;
@@ -234,7 +235,6 @@ export declare function screenMemoryMcpToolDefinitions(): ({
234
235
  };
235
236
  timestamp?: undefined;
236
237
  chapterId?: undefined;
237
- count?: undefined;
238
238
  startAt?: undefined;
239
239
  endAt?: undefined;
240
240
  reason?: undefined;
@@ -250,6 +250,7 @@ export declare function screenMemoryMcpToolDefinitions(): ({
250
250
  type: string;
251
251
  required: string[];
252
252
  properties: {
253
+ count?: undefined;
253
254
  query?: undefined;
254
255
  minutes?: undefined;
255
256
  limit?: undefined;
@@ -263,7 +264,6 @@ export declare function screenMemoryMcpToolDefinitions(): ({
263
264
  description: string;
264
265
  };
265
266
  chapterId?: undefined;
266
- count?: undefined;
267
267
  startAt?: undefined;
268
268
  endAt?: undefined;
269
269
  includeMicrophone?: undefined;
@@ -314,13 +314,13 @@ export declare function screenMemoryMcpToolDefinitions(): ({
314
314
  type: string;
315
315
  required: string[];
316
316
  properties: {
317
+ count?: undefined;
317
318
  query?: undefined;
318
319
  minutes?: undefined;
319
320
  limit?: undefined;
320
321
  clientHint?: undefined;
321
322
  timestamp?: undefined;
322
323
  chapterId?: undefined;
323
- count?: undefined;
324
324
  startAt: {
325
325
  type: string;
326
326
  description: string;
@@ -349,13 +349,13 @@ export declare function screenMemoryMcpToolDefinitions(): ({
349
349
  type: string;
350
350
  required: string[];
351
351
  properties: {
352
+ count?: undefined;
352
353
  query?: undefined;
353
354
  minutes?: undefined;
354
355
  limit?: undefined;
355
356
  clientHint?: undefined;
356
357
  timestamp?: undefined;
357
358
  chapterId?: undefined;
358
- count?: undefined;
359
359
  startAt?: undefined;
360
360
  endAt?: undefined;
361
361
  reason?: undefined;
@@ -25,6 +25,7 @@ export interface McpOAuthRoutesOptions {
25
25
  export declare function redirectWithStagedCookies(event: H3Event, location: string): Response;
26
26
  export declare function mountMcpOAuthRoutes(nitroApp: any, options: McpOAuthRoutesOptions): void;
27
27
  export declare function resolveMcpOAuthScope(serverUrl: URL, requestedScope: unknown): RemoteMcpScope | null;
28
+ export declare function stripMcpOAuthAppBasePath(path: string, basePath: string): string;
28
29
  export declare function resolveManagedMcpOAuthClient(serverUrl: URL): Promise<StoredOAuthClientInformation | undefined>;
29
30
  export declare function setMcpOAuthFlowCookie(event: H3Event, flow: McpOAuthFlow, secure: boolean): void;
30
31
  export declare function readMcpOAuthFlowCookie(event: H3Event): McpOAuthFlow | null;
@@ -5,7 +5,7 @@ import { decryptSecretValue, encryptSecretValue } from "../secrets/crypto.js";
5
5
  import { getSession, safeReturnPath } from "../server/auth.js";
6
6
  import { resolveSecret } from "../server/credential-provider.js";
7
7
  import { getH3App } from "../server/framework-request-handler.js";
8
- import { getAppUrl, resolveOAuthRedirectUri } from "../server/google-oauth.js";
8
+ import { getAppBasePath, getAppUrl, resolveOAuthRedirectUri, } from "../server/google-oauth.js";
9
9
  import { runWithRequestContext } from "../server/request-context.js";
10
10
  import { finishMcpOAuthAuthorization, startMcpOAuthAuthorization, validateMcpOAuthCallbackIssuer, } from "./oauth-client.js";
11
11
  import { addOAuthRemoteServer, listRemoteServers, normalizeServerName, replaceOAuthRemoteServer, validateRemoteUrl, } from "./remote-store.js";
@@ -206,6 +206,19 @@ export function resolveMcpOAuthScope(serverUrl, requestedScope) {
206
206
  }
207
207
  return requestedScope === "org" ? "org" : "user";
208
208
  }
209
+ export function stripMcpOAuthAppBasePath(path, basePath) {
210
+ const normalizedBase = `/${basePath.replace(/^\/+|\/+$/g, "")}`;
211
+ if (normalizedBase === "/")
212
+ return path;
213
+ const suffixStart = path.search(/[?#]/);
214
+ const pathname = suffixStart === -1 ? path : path.slice(0, suffixStart);
215
+ const suffix = suffixStart === -1 ? "" : path.slice(suffixStart);
216
+ if (pathname === normalizedBase)
217
+ return `/${suffix}`;
218
+ return pathname.startsWith(`${normalizedBase}/`)
219
+ ? `${pathname.slice(normalizedBase.length)}${suffix}`
220
+ : path;
221
+ }
209
222
  export async function resolveManagedMcpOAuthClient(serverUrl) {
210
223
  const client = MANAGED_MCP_OAUTH_CLIENTS.find((candidate) => candidate.serverOrigin === serverUrl.origin);
211
224
  if (!client)
@@ -286,7 +299,7 @@ async function handleMcpOAuthCallback(event, options) {
286
299
  await options.reconfigure();
287
300
  const returnPath = flow.returnUrl ??
288
301
  `/settings/integrations?connected=mcp-${encodeURIComponent(flow.name)}`;
289
- return redirectWithStagedCookies(event, getAppUrl(event, returnPath));
302
+ return redirectWithStagedCookies(event, getAppUrl(event, stripMcpOAuthAppBasePath(returnPath, getAppBasePath())));
290
303
  }
291
304
  catch {
292
305
  setResponseStatus(event, 400);
@@ -21,8 +21,8 @@
21
21
  */
22
22
  import { createHash } from "node:crypto";
23
23
  import { writeAppSecret, readAppSecret, deleteAppSecret, } from "../secrets/storage.js";
24
- import { getOrgSetting, putOrgSetting, deleteOrgSetting, } from "../settings/org-settings.js";
25
- import { getUserSetting, putUserSetting, deleteUserSetting, } from "../settings/user-settings.js";
24
+ import { getOrgSetting, mutateOrgSetting } from "../settings/org-settings.js";
25
+ import { getUserSetting, mutateUserSetting, } from "../settings/user-settings.js";
26
26
  import { getMcpOAuthAccessToken, revokeMcpOAuthCredentials, saveMcpOAuthCredentials, } from "./oauth-client.js";
27
27
  import { validateRemoteUrl } from "./remote-url.js";
28
28
  export { validateRemoteUrl } from "./remote-url.js";
@@ -107,20 +107,21 @@ function sanitiseOrgId(orgId) {
107
107
  return orgId.toLowerCase().replace(/[^a-z0-9-]/g, "-");
108
108
  }
109
109
  async function readList(scope, scopeId) {
110
- const raw = scope === "user"
110
+ return parseServerList(scope === "user"
111
111
  ? await getUserSetting(scopeId, SETTINGS_KEY)
112
- : await getOrgSetting(scopeId, SETTINGS_KEY);
112
+ : await getOrgSetting(scopeId, SETTINGS_KEY));
113
+ }
114
+ function parseServerList(raw) {
113
115
  if (!raw || !Array.isArray(raw.servers))
114
116
  return [];
115
117
  return raw.servers.filter((s) => s && typeof s.id === "string" && typeof s.url === "string");
116
118
  }
117
- async function writeList(scope, scopeId, servers) {
118
- if (scope === "user") {
119
- await putUserSetting(scopeId, SETTINGS_KEY, { servers });
120
- }
121
- else {
122
- await putOrgSetting(scopeId, SETTINGS_KEY, { servers });
123
- }
119
+ async function mutateServerList(scope, scopeId, updater) {
120
+ const mutate = scope === "user" ? mutateUserSetting : mutateOrgSetting;
121
+ const next = await mutate(scopeId, SETTINGS_KEY, async (raw) => ({
122
+ servers: await updater(parseServerList(raw)),
123
+ }));
124
+ return parseServerList(next);
124
125
  }
125
126
  export async function listRemoteServers(scope, scopeId) {
126
127
  return readList(scope, scopeId);
@@ -228,6 +229,7 @@ export async function replaceOAuthRemoteServer(scope, scopeId, serverId, credent
228
229
  ...credentials,
229
230
  serverUrl: credentialResource.url.toString(),
230
231
  };
232
+ let updated;
231
233
  try {
232
234
  await saveMcpOAuthCredentials({
233
235
  key: nextSecretKey,
@@ -235,21 +237,22 @@ export async function replaceOAuthRemoteServer(scope, scopeId, serverId, credent
235
237
  scopeId,
236
238
  credentials: nextCredentials,
237
239
  });
238
- const server = {
239
- ...current,
240
- url: nextCredentials.serverUrl,
241
- oauthSecretKey: nextSecretKey,
242
- };
243
- const next = [...existing];
244
- next[index] = server;
245
- await writeList(scope, scopeId, next);
246
- await revokeMcpOAuthCredentials({
247
- key: current.oauthSecretKey,
248
- scope,
249
- scopeId,
250
- serverUrl: current.url,
251
- }).catch(() => undefined);
252
- return { ok: true, server };
240
+ updated = await mutateServerList(scope, scopeId, (servers) => {
241
+ const latestIndex = servers.findIndex((server) => server.id === serverId);
242
+ const latest = latestIndex >= 0 ? servers[latestIndex] : undefined;
243
+ if (!latest ||
244
+ latest.oauthSecretKey !== current.oauthSecretKey ||
245
+ latest.url !== current.url) {
246
+ throw new Error("MCP server changed while reconnecting");
247
+ }
248
+ const next = [...servers];
249
+ next[latestIndex] = {
250
+ ...latest,
251
+ url: nextCredentials.serverUrl,
252
+ oauthSecretKey: nextSecretKey,
253
+ };
254
+ return next;
255
+ });
253
256
  }
254
257
  catch (err) {
255
258
  await revokeMcpOAuthCredentials({
@@ -263,6 +266,18 @@ export async function replaceOAuthRemoteServer(scope, scopeId, serverId, credent
263
266
  error: `Failed to replace MCP OAuth credentials: ${err?.message ?? err}`,
264
267
  };
265
268
  }
269
+ // The settings row now points at the replacement grant. Old-grant cleanup
270
+ // is best effort and must not roll back or invalidate the committed row.
271
+ await revokeMcpOAuthCredentials({
272
+ key: current.oauthSecretKey,
273
+ scope,
274
+ scopeId,
275
+ serverUrl: current.url,
276
+ }).catch(() => undefined);
277
+ const server = updated.find((candidate) => candidate.id === serverId);
278
+ return server
279
+ ? { ok: true, server }
280
+ : { ok: false, error: "MCP server was not found" };
266
281
  }
267
282
  export async function addFirstPartyRemoteServer(orgId, input) {
268
283
  const trust = await isFirstPartyRemoteEndpointTrusted(orgId, input.appId, input.url);
@@ -327,7 +342,24 @@ async function addRemoteServerInternal(scope, scopeId, input) {
327
342
  description: input.description?.trim() || undefined,
328
343
  createdAt: Date.now(),
329
344
  };
330
- await writeList(scope, scopeId, [...existing, server]);
345
+ try {
346
+ await mutateServerList(scope, scopeId, (servers) => {
347
+ if (servers.some((candidate) => candidate.name === name)) {
348
+ throw new Error(`A server named "${name}" already exists`);
349
+ }
350
+ return [...servers, server];
351
+ });
352
+ }
353
+ catch (err) {
354
+ if (headerSecretKey) {
355
+ await deleteAppSecret({
356
+ key: headerSecretKey,
357
+ scope: toSecretScope(scope),
358
+ scopeId,
359
+ }).catch(() => undefined);
360
+ }
361
+ return { ok: false, error: err?.message ?? String(err) };
362
+ }
331
363
  return { ok: true, server };
332
364
  }
333
365
  function normalizeEndpointPath(pathname) {
@@ -396,9 +428,29 @@ export async function isFirstPartyRemoteEndpointTrusted(orgId, appId, endpoint)
396
428
  }
397
429
  export async function removeRemoteServer(scope, scopeId, id) {
398
430
  const existing = await readList(scope, scopeId);
399
- const removed = existing.find((s) => s.id === id);
400
- const next = existing.filter((s) => s.id !== id);
401
- if (next.length === existing.length)
431
+ const expected = existing.find((s) => s.id === id);
432
+ if (!expected)
433
+ return false;
434
+ let removed;
435
+ try {
436
+ await mutateServerList(scope, scopeId, (servers) => {
437
+ removed = undefined;
438
+ const latest = servers.find((server) => server.id === id);
439
+ if (!latest)
440
+ return servers;
441
+ if (latest.oauthSecretKey !== expected.oauthSecretKey) {
442
+ throw new Error(`MCP OAuth credentials changed while removing ${expected.name}`);
443
+ }
444
+ removed = latest;
445
+ return servers.filter((server) => server.id !== id);
446
+ });
447
+ }
448
+ catch (err) {
449
+ // eslint-disable-next-line no-console
450
+ console.warn(`[mcp-client] Failed to remove MCP server ${expected.name}: ${err?.message ?? err}`);
451
+ return false;
452
+ }
453
+ if (!removed)
402
454
  return false;
403
455
  if (removed?.oauthSecretKey) {
404
456
  try {
@@ -409,8 +461,7 @@ export async function removeRemoteServer(scope, scopeId, id) {
409
461
  serverUrl: removed.url,
410
462
  });
411
463
  if (result.local === "replaced") {
412
- console.warn(`[mcp-client] MCP OAuth credentials changed while removing ${removed.name}; the server was kept so the newer connection remains manageable.`);
413
- return false;
464
+ console.warn(`[mcp-client] MCP OAuth credentials changed while removing ${removed.name}; the server row was already removed.`);
414
465
  }
415
466
  if (result.remote === "failed") {
416
467
  console.warn(`[mcp-client] MCP OAuth revocation failed for ${removed.name}; local credentials were removed.`);
@@ -421,17 +472,6 @@ export async function removeRemoteServer(scope, scopeId, id) {
421
472
  console.warn(`[mcp-client] Failed to delete MCP OAuth credentials ${removed.oauthSecretKey}: ${err?.message ?? err}`);
422
473
  }
423
474
  }
424
- if (next.length === 0) {
425
- if (scope === "user") {
426
- await deleteUserSetting(scopeId, SETTINGS_KEY);
427
- }
428
- else {
429
- await deleteOrgSetting(scopeId, SETTINGS_KEY);
430
- }
431
- }
432
- else {
433
- await writeList(scope, scopeId, next);
434
- }
435
475
  // Best-effort: drop the encrypted-headers secret too. Errors are logged
436
476
  // but don't fail the deletion — the settings row is already gone, so a
437
477
  // dangling secret is harmless (it just can't be read back).
@@ -16,18 +16,18 @@ export declare function createNotificationsHandler(): import("h3").EventHandlerW
16
16
  ok?: undefined;
17
17
  error?: undefined;
18
18
  } | {
19
+ count?: undefined;
19
20
  updated: number;
20
21
  ok?: undefined;
21
22
  error?: undefined;
22
- count?: undefined;
23
23
  } | {
24
+ count?: undefined;
24
25
  updated?: undefined;
25
26
  error: string;
26
27
  ok?: undefined;
27
- count?: undefined;
28
28
  } | {
29
+ count?: undefined;
29
30
  updated?: undefined;
30
31
  ok: boolean;
31
32
  error?: undefined;
32
- count?: undefined;
33
33
  }>>;
@@ -41,27 +41,27 @@ export declare function createObservabilityHandler(): import("h3").EventHandlerW
41
41
  thumbsUpRate: number;
42
42
  avgEvalScore: number;
43
43
  } | {
44
- error?: undefined;
45
- ok?: undefined;
46
44
  summary: import("./types.js").TraceSummary;
47
45
  spans: import("./types.js").TraceSpan[];
48
46
  id?: undefined;
49
- } | {
50
47
  error?: undefined;
51
48
  ok?: undefined;
49
+ } | {
52
50
  summary?: undefined;
53
51
  spans?: undefined;
54
52
  id: string;
55
- } | {
53
+ error?: undefined;
56
54
  ok?: undefined;
55
+ } | {
57
56
  summary?: undefined;
58
57
  spans?: undefined;
59
58
  id?: undefined;
60
59
  error: any;
60
+ ok?: undefined;
61
61
  } | {
62
- error?: undefined;
63
62
  summary?: undefined;
64
63
  spans?: undefined;
65
64
  id?: undefined;
65
+ error?: undefined;
66
66
  ok: boolean;
67
67
  }>>;
@@ -15,6 +15,6 @@ export declare function createProgressHandler(): import("h3").EventHandlerWithFe
15
15
  error: string;
16
16
  ok?: undefined;
17
17
  } | {
18
- error?: undefined;
19
18
  ok: boolean;
19
+ error?: undefined;
20
20
  }>>;
@@ -51,8 +51,8 @@ export declare function handleDeleteResource(event: any): Promise<{
51
51
  error: string;
52
52
  ok?: undefined;
53
53
  } | {
54
- error?: undefined;
55
54
  ok: boolean;
55
+ error?: undefined;
56
56
  }>;
57
57
  /** POST /_agent-native/resources/upload — upload a file as a resource */
58
58
  export declare function handleUploadResource(event: any): Promise<import("./store.js").Resource | {
@@ -73,10 +73,10 @@ export declare function handleUploadResource(event: any): Promise<import("./stor
73
73
  runId: string | null;
74
74
  expiresAt: number | null;
75
75
  metadata: string | null;
76
- error?: undefined;
77
76
  url: string;
78
77
  provider: string;
79
78
  storageSetupRequired?: undefined;
79
+ error?: undefined;
80
80
  } | {
81
81
  error: string;
82
82
  storageSetupRequired: boolean;
@@ -27,10 +27,10 @@ export declare function resolveAgentEngineApiKeyWriteTarget(event: H3Event, scop
27
27
  export declare function createAgentEngineApiKeyHandler(): import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<{
28
28
  error: any;
29
29
  } | {
30
+ error?: undefined;
30
31
  ok: boolean;
31
32
  key: string;
32
33
  baseUrlKey?: string;
33
34
  scope: AgentEngineApiKeyScope;
34
- error?: undefined;
35
35
  }>>;
36
36
  export {};
@@ -57,7 +57,20 @@ export async function resolveGoogleProviderCredentialCandidatesWithReader(option
57
57
  * remain the backwards-compatible provider OAuth credentials.
58
58
  */
59
59
  export function resolveGoogleSignInCredentials() {
60
- return (readCredentialPair("GOOGLE_SIGN_IN_CLIENT_ID", "GOOGLE_SIGN_IN_CLIENT_SECRET") ?? readCredentialPair("GOOGLE_CLIENT_ID", "GOOGLE_CLIENT_SECRET"));
60
+ const signIn = readCredentialPair("GOOGLE_SIGN_IN_CLIENT_ID", "GOOGLE_SIGN_IN_CLIENT_SECRET");
61
+ const provider = readCredentialPair("GOOGLE_CLIENT_ID", "GOOGLE_CLIENT_SECRET");
62
+ // Both pairs configured for different clients means one of them is dead
63
+ // config that nothing will ever read. Silence here cost a fleet-wide beta
64
+ // sign-in outage: GOOGLE_CLIENT_SECRET was repaired and verified while the
65
+ // callback kept using a stale GOOGLE_SIGN_IN_CLIENT_SECRET, so the repaired
66
+ // variable looked correct and changed nothing. Name the loser explicitly.
67
+ if (signIn && provider && signIn.clientId !== provider.clientId) {
68
+ console.warn("[agent-native][google-oauth] GOOGLE_SIGN_IN_CLIENT_ID and GOOGLE_CLIENT_ID " +
69
+ "are set to different Google clients. Sign-in uses GOOGLE_SIGN_IN_CLIENT_ID; " +
70
+ "GOOGLE_CLIENT_ID/SECRET are ignored for sign-in. Editing them will not " +
71
+ "change sign-in behaviour.");
72
+ }
73
+ return signIn ?? provider;
61
74
  }
62
75
  export function hasGoogleSignInCredentials() {
63
76
  return resolveGoogleSignInCredentials() !== null;
@@ -26,8 +26,8 @@ export declare function createRealtimeTokenHandler(): import("h3").EventHandlerW
26
26
  expiresAt?: undefined;
27
27
  ttlSeconds?: undefined;
28
28
  } | {
29
- error?: undefined;
30
29
  token: string;
31
30
  expiresAt: string;
32
31
  ttlSeconds: number;
32
+ error?: undefined;
33
33
  }>>;
@@ -20,6 +20,6 @@ export declare function createTranscribeVoiceHandler(): import("h3").EventHandle
20
20
  error: string;
21
21
  text?: undefined;
22
22
  } | {
23
- error?: undefined;
24
23
  text: string;
24
+ error?: undefined;
25
25
  }>>;
@@ -1,4 +1,4 @@
1
- export { getSetting, mutateSetting, putSetting, deleteSetting, getAllSettings, getSettingsEmitter, type StoreWriteOptions, } from "./store.js";
1
+ export { getSetting, mutateSetting, putSetting, deleteSetting, deleteSettingIfValue, getAllSettings, getSettingsEmitter, type StoreWriteOptions, } from "./store.js";
2
2
  export { getSettingHandler, putSettingHandler, deleteSettingHandler, } from "./handlers.js";
3
3
  export { readSetting, writeSetting, removeSetting } from "./script-helpers.js";
4
4
  export { getUserSetting, putUserSetting, deleteUserSetting, } from "./user-settings.js";
@@ -1,5 +1,5 @@
1
1
  // Store
2
- export { getSetting, mutateSetting, putSetting, deleteSetting, getAllSettings, getSettingsEmitter, } from "./store.js";
2
+ export { getSetting, mutateSetting, putSetting, deleteSetting, deleteSettingIfValue, getAllSettings, getSettingsEmitter, } from "./store.js";
3
3
  // H3 route handlers
4
4
  export { getSettingHandler, putSettingHandler, deleteSettingHandler, } from "./handlers.js";
5
5
  // Script helpers
@@ -1,7 +1,11 @@
1
1
  import type { EventEmitter } from "node:events";
2
2
  export declare function getSettingsEmitter(): EventEmitter;
3
3
  export declare function ensureTable(): Promise<void>;
4
- export declare function getSetting(key: string): Promise<Record<string, unknown> | null>;
4
+ export interface StoreReadOptions {
5
+ /** Skip the per-request snapshot when a cross-request race must be checked. */
6
+ bypassCache?: boolean;
7
+ }
8
+ export declare function getSetting(key: string, options?: StoreReadOptions): Promise<Record<string, unknown> | null>;
5
9
  export interface StoreWriteOptions {
6
10
  /** Tag identifying who initiated this write (e.g. a tab ID). */
7
11
  requestSource?: string;
@@ -16,6 +20,8 @@ export interface StoreWriteOptions {
16
20
  export declare function mutateSetting(key: string, updater: (current: Record<string, unknown> | null) => Record<string, unknown> | Promise<Record<string, unknown>>, options?: StoreWriteOptions): Promise<Record<string, unknown>>;
17
21
  export declare function putSetting(key: string, value: Record<string, unknown>, options?: StoreWriteOptions): Promise<void>;
18
22
  export declare function deleteSetting(key: string, options?: StoreWriteOptions): Promise<boolean>;
23
+ /** Delete a setting only when its stored value still matches the inspected value. */
24
+ export declare function deleteSettingIfValue(key: string, expected: Record<string, unknown>, options?: StoreWriteOptions): Promise<boolean>;
19
25
  /**
20
26
  * Delete every setting whose key starts with `prefix`. Returns the number of
21
27
  * rows removed. Used when an owning entity is deleted (e.g. an organization
@@ -115,9 +115,9 @@ export async function ensureTable() {
115
115
  }
116
116
  return _initPromise;
117
117
  }
118
- export async function getSetting(key) {
118
+ export async function getSetting(key, options) {
119
119
  const cache = requestSettingsCache();
120
- if (cache?.has(key)) {
120
+ if (!options?.bypassCache && cache?.has(key)) {
121
121
  const cached = cache.get(key);
122
122
  return cached == null ? null : JSON.parse(cached);
123
123
  }
@@ -129,7 +129,8 @@ export async function getSetting(key) {
129
129
  args: [key],
130
130
  });
131
131
  const raw = rows.length === 0 ? null : rows[0].value;
132
- cache?.set(key, raw);
132
+ if (!options?.bypassCache)
133
+ cache?.set(key, raw);
133
134
  return raw == null ? null : JSON.parse(raw);
134
135
  }
135
136
  const SETTINGS_MUTATION_ATTEMPTS = 25;
@@ -221,6 +222,27 @@ export async function deleteSetting(key, options) {
221
222
  }
222
223
  return false;
223
224
  }
225
+ /** Delete a setting only when its stored value still matches the inspected value. */
226
+ export async function deleteSettingIfValue(key, expected, options) {
227
+ await ensureTable();
228
+ const client = getDbExec();
229
+ const table = settingsTable();
230
+ const result = await client.execute({
231
+ sql: `DELETE FROM ${table} WHERE key = ? AND value = ?`,
232
+ args: [key, JSON.stringify(expected)],
233
+ });
234
+ if (result.rowsAffected === 0)
235
+ return false;
236
+ requestSettingsCache()?.set(key, null);
237
+ invalidateRequestAllSettings();
238
+ settingsEmitter().emit("settings", {
239
+ source: "settings",
240
+ type: "delete",
241
+ key,
242
+ ...(options?.requestSource && { requestSource: options.requestSource }),
243
+ });
244
+ return true;
245
+ }
224
246
  /**
225
247
  * Delete every setting whose key starts with `prefix`. Returns the number of
226
248
  * rows removed. Used when an owning entity is deleted (e.g. an organization
@@ -12,5 +12,7 @@ import { type StoreWriteOptions } from "./store.js";
12
12
  export declare function getUserSetting(email: string, key: string): Promise<Record<string, unknown> | null>;
13
13
  /** Write a user-scoped setting. Always writes to the prefixed key. */
14
14
  export declare function putUserSetting(email: string, key: string, value: Record<string, unknown>, options?: StoreWriteOptions): Promise<void>;
15
+ /** Atomically derive and persist one user-scoped setting. */
16
+ export declare function mutateUserSetting(email: string, key: string, updater: (current: Record<string, unknown> | null) => Record<string, unknown> | Promise<Record<string, unknown>>, options?: StoreWriteOptions): Promise<Record<string, unknown>>;
15
17
  /** Delete a user-scoped setting. */
16
18
  export declare function deleteUserSetting(email: string, key: string, options?: StoreWriteOptions): Promise<boolean>;