@agent-native/core 0.145.0 → 0.145.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/templates/brain/server/lib/capture-sanitization.ts +6 -2
  3. package/corpus/templates/clips/changelog/2026-08-07-full-screen-recordings-on-a-multi-monitor-mac-now-let-you-pi.md +6 -0
  4. package/corpus/templates/clips/chrome-extension/src/styles.css +13 -4
  5. package/corpus/templates/clips/desktop/src/app.tsx +38 -1
  6. package/corpus/templates/clips/desktop/src/components/MediaDeviceRow.tsx +123 -104
  7. package/corpus/templates/clips/desktop/src/lib/recorder.ts +165 -2
  8. package/corpus/templates/clips/desktop/src/main.tsx +3 -0
  9. package/corpus/templates/clips/desktop/src/overlays/monitor-picker.tsx +70 -0
  10. package/corpus/templates/clips/desktop/src/styles.css +70 -11
  11. package/corpus/templates/clips/desktop/src-tauri/capabilities/default.json +2 -1
  12. package/corpus/templates/clips/desktop/src-tauri/src/clips/mod.rs +163 -1
  13. package/corpus/templates/clips/desktop/src-tauri/src/lib.rs +6 -1
  14. package/corpus/templates/clips/desktop/src-tauri/src/native_screen.rs +141 -22
  15. package/corpus/templates/clips/desktop/src-tauri/src/state.rs +27 -1
  16. package/corpus/templates/clips/desktop/src-tauri/src/util.rs +12 -1
  17. package/dist/agent/engine/index.d.ts +1 -1
  18. package/dist/agent/engine/index.js +1 -1
  19. package/dist/agent/engine/registry.d.ts +15 -0
  20. package/dist/agent/engine/registry.js +39 -10
  21. package/dist/agent/production-agent.d.ts +40 -1
  22. package/dist/agent/production-agent.js +81 -40
  23. package/dist/agent/run-manager.d.ts +44 -1
  24. package/dist/agent/run-manager.js +70 -8
  25. package/dist/chat-threads/store.js +6 -4
  26. package/dist/client/use-db-sync.js +9 -1
  27. package/dist/db/ddl-guard.d.ts +8 -0
  28. package/dist/db/ddl-guard.js +148 -0
  29. package/dist/deploy/build.d.ts +36 -0
  30. package/dist/deploy/build.js +78 -5
  31. package/dist/integrations/webhook-handler.d.ts +3 -2
  32. package/dist/integrations/webhook-handler.js +8 -38
  33. package/dist/mcp/screen-memory-stdio.d.ts +7 -7
  34. package/dist/notifications/routes.d.ts +3 -3
  35. package/dist/observability/routes.d.ts +5 -5
  36. package/dist/org/accept-pending.js +2 -2
  37. package/dist/org/auth-policy.js +4 -0
  38. package/dist/org/auto-join-domain.d.ts +9 -0
  39. package/dist/org/auto-join-domain.js +53 -4
  40. package/dist/org/context.js +12 -3
  41. package/dist/org/handlers.js +17 -5
  42. package/dist/org/request-org-cache.d.ts +24 -2
  43. package/dist/org/request-org-cache.js +57 -2
  44. package/dist/provider-api/actions/custom-provider-registration.d.ts +4 -4
  45. package/dist/provider-api/actions/provider-api.d.ts +6 -6
  46. package/dist/resources/store.d.ts +1 -1
  47. package/dist/resources/store.js +96 -7
  48. package/dist/secrets/routes.d.ts +6 -6
  49. package/dist/server/agent-chat-plugin.js +83 -37
  50. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  51. package/dist/server/auth.d.ts +3 -0
  52. package/dist/server/auth.js +22 -1
  53. package/dist/server/collab-plugin.js +10 -4
  54. package/dist/server/complete-text.js +14 -7
  55. package/dist/server/core-routes-plugin.js +3 -0
  56. package/dist/server/index.d.ts +1 -1
  57. package/dist/server/index.js +1 -1
  58. package/dist/server/request-context.d.ts +7 -1
  59. package/dist/server/session-email-cache.d.ts +14 -0
  60. package/dist/server/session-email-cache.js +48 -0
  61. package/dist/server/sweep-runtime.d.ts +22 -0
  62. package/dist/server/sweep-runtime.js +28 -0
  63. package/dist/server/transcribe-voice.d.ts +1 -1
  64. package/dist/settings/store.js +63 -7
  65. package/dist/shared/ttl-cache.d.ts +59 -0
  66. package/dist/shared/ttl-cache.js +43 -0
  67. package/dist/sharing/actions/list-resource-shares.d.ts +3 -3
  68. package/dist/sharing/actions/list-resource-shares.js +4 -1
  69. package/dist/triggers/condition-evaluator.js +11 -27
  70. package/docs/content/deployment.mdx +48 -0
  71. package/docs/content/environment-variables.mdx +11 -0
  72. 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;
141
140
  query?: undefined;
142
141
  minutes?: undefined;
143
142
  limit?: undefined;
144
143
  clientHint?: undefined;
145
144
  timestamp?: undefined;
146
145
  chapterId?: undefined;
146
+ count?: undefined;
147
147
  startAt?: undefined;
148
148
  endAt?: undefined;
149
149
  reason?: undefined;
@@ -159,7 +159,6 @@ export declare function screenMemoryMcpToolDefinitions(): ({
159
159
  inputSchema: {
160
160
  type: string;
161
161
  properties: {
162
- count?: undefined;
163
162
  query: {
164
163
  type: string;
165
164
  description: string;
@@ -175,6 +174,7 @@ export declare function screenMemoryMcpToolDefinitions(): ({
175
174
  clientHint?: undefined;
176
175
  timestamp?: undefined;
177
176
  chapterId?: undefined;
177
+ count?: undefined;
178
178
  startAt?: undefined;
179
179
  endAt?: undefined;
180
180
  reason?: undefined;
@@ -190,7 +190,6 @@ export declare function screenMemoryMcpToolDefinitions(): ({
190
190
  inputSchema: {
191
191
  type: string;
192
192
  properties: {
193
- count?: undefined;
194
193
  minutes: {
195
194
  type: string;
196
195
  description: string;
@@ -200,6 +199,7 @@ export declare function screenMemoryMcpToolDefinitions(): ({
200
199
  clientHint?: undefined;
201
200
  timestamp?: undefined;
202
201
  chapterId?: undefined;
202
+ count?: undefined;
203
203
  startAt?: undefined;
204
204
  endAt?: undefined;
205
205
  reason?: undefined;
@@ -216,7 +216,6 @@ export declare function screenMemoryMcpToolDefinitions(): ({
216
216
  type: string;
217
217
  required: string[];
218
218
  properties: {
219
- count?: undefined;
220
219
  query: {
221
220
  type: string;
222
221
  description: string;
@@ -235,6 +234,7 @@ export declare function screenMemoryMcpToolDefinitions(): ({
235
234
  };
236
235
  timestamp?: undefined;
237
236
  chapterId?: undefined;
237
+ count?: undefined;
238
238
  startAt?: undefined;
239
239
  endAt?: undefined;
240
240
  reason?: undefined;
@@ -250,7 +250,6 @@ export declare function screenMemoryMcpToolDefinitions(): ({
250
250
  type: string;
251
251
  required: string[];
252
252
  properties: {
253
- count?: undefined;
254
253
  query?: undefined;
255
254
  minutes?: undefined;
256
255
  limit?: undefined;
@@ -264,6 +263,7 @@ export declare function screenMemoryMcpToolDefinitions(): ({
264
263
  description: string;
265
264
  };
266
265
  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;
318
317
  query?: undefined;
319
318
  minutes?: undefined;
320
319
  limit?: undefined;
321
320
  clientHint?: undefined;
322
321
  timestamp?: undefined;
323
322
  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;
353
352
  query?: undefined;
354
353
  minutes?: undefined;
355
354
  limit?: undefined;
356
355
  clientHint?: undefined;
357
356
  timestamp?: undefined;
358
357
  chapterId?: undefined;
358
+ count?: undefined;
359
359
  startAt?: undefined;
360
360
  endAt?: undefined;
361
361
  reason?: undefined;
@@ -13,13 +13,13 @@
13
13
  export declare function createNotificationsHandler(): import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<"" | import("./types.js").Notification[] | {
14
14
  count: number;
15
15
  updated?: undefined;
16
- error?: undefined;
17
16
  ok?: undefined;
17
+ error?: undefined;
18
18
  } | {
19
19
  count?: undefined;
20
20
  updated: number;
21
- error?: undefined;
22
21
  ok?: undefined;
22
+ error?: undefined;
23
23
  } | {
24
24
  count?: undefined;
25
25
  updated?: undefined;
@@ -28,6 +28,6 @@ export declare function createNotificationsHandler(): import("h3").EventHandlerW
28
28
  } | {
29
29
  count?: undefined;
30
30
  updated?: undefined;
31
- error?: undefined;
32
31
  ok: boolean;
32
+ error?: 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
  }>>;
@@ -1,6 +1,6 @@
1
1
  import { getDbExec } from "../db/client.js";
2
2
  import { setActiveOrgId } from "./active-org.js";
3
- import { invalidateRequestMemberOrgIds } from "./request-org-cache.js";
3
+ import { invalidateMemberOrgCaches } from "./request-org-cache.js";
4
4
  const nanoid = () => globalThis.crypto?.randomUUID?.().replace(/-/g, "") ??
5
5
  Math.random().toString(36).slice(2) + Date.now().toString(36);
6
6
  /**
@@ -63,7 +63,7 @@ export async function acceptPendingInvitationsForEmail(rawEmail) {
63
63
  ON CONFLICT (org_id, LOWER(email)) DO NOTHING`,
64
64
  args: [nanoid(), inv.orgId, email, role, Date.now()],
65
65
  });
66
- invalidateRequestMemberOrgIds();
66
+ invalidateMemberOrgCaches();
67
67
  }
68
68
  await db.execute({
69
69
  sql: `UPDATE org_invitations SET status = 'accepted' WHERE id = ?`,
@@ -1,4 +1,5 @@
1
1
  import { getDbExec } from "../db/client.js";
2
+ import { invalidateSessionEmailCache } from "../server/session-email-cache.js";
2
3
  export const GOOGLE_AUTH_REQUIRED_MESSAGE = "This organization requires Google sign-in.";
3
4
  function normalizeEmail(email) {
4
5
  return email.trim().toLowerCase();
@@ -152,6 +153,9 @@ export async function setRequiredAuthProvider(orgId, provider) {
152
153
  if (!isMissingLegacySessionTable(error))
153
154
  throw error;
154
155
  }
156
+ // Revoking a whole org's sessions must land immediately, not after the
157
+ // resolution cache's TTL.
158
+ invalidateSessionEmailCache();
155
159
  return {
156
160
  revokedBetterAuthSessions: Number(betterAuthResult.rowsAffected ?? 0),
157
161
  revokedLegacySessions: Number(legacyResult.rowsAffected ?? 0),
@@ -4,6 +4,15 @@ export interface AutoJoinDomainResult {
4
4
  }>;
5
5
  activeOrgId: string | null;
6
6
  }
7
+ /**
8
+ * Call after any write that could make a domain start matching an org —
9
+ * `organizations.allowed_domain` updates and org creation. Clears every domain
10
+ * rather than one: the caller knows which org changed, not which domains a
11
+ * stale negative was recorded for.
12
+ */
13
+ export declare function invalidateDomainMatchCache(): void;
14
+ /** Test seam — the cache is module state, so suites must be able to clear it. */
15
+ export declare function __resetDomainMatchCacheForTests(): void;
7
16
  export interface AutoJoinDomainOptions {
8
17
  /**
9
18
  * The signup hook should not clobber an org selected by an invite flow, but
@@ -1,9 +1,46 @@
1
1
  import { getDbExec } from "../db/client.js";
2
2
  import { getUserSetting } from "../settings/user-settings.js";
3
+ import { createTtlCache } from "../shared/ttl-cache.js";
3
4
  import { setActiveOrgId } from "./active-org.js";
4
- import { invalidateRequestMemberOrgIds } from "./request-org-cache.js";
5
+ import { isFreeEmailProvider } from "./free-email-providers.js";
6
+ import { invalidateMemberOrgCaches } from "./request-org-cache.js";
5
7
  const nanoid = () => globalThis.crypto?.randomUUID?.().replace(/-/g, "") ??
6
8
  Math.random().toString(36).slice(2) + Date.now().toString(36);
9
+ /**
10
+ * Negative cache for "no org has this email domain as its `allowed_domain`".
11
+ *
12
+ * `resolveOrgContext` calls this function on EVERY authenticated request for
13
+ * any account that holds no membership in a domain-matched org — which is every
14
+ * solo user and every user in a team org that never configured domain matching.
15
+ * The probe finds nothing, changes no state, and runs again on the next
16
+ * request: production showed 318k of these, ~64% of all authenticated requests,
17
+ * with no fixed point because not-joining an org is not a state the user can
18
+ * leave.
19
+ *
20
+ * Keyed on the DOMAIN, not the email, so one entry covers every account at that
21
+ * domain. Only a SUCCESSFUL zero-row read is cached — a failed probe stays on
22
+ * the uncached path so an unreadable `organizations` table never masquerades as
23
+ * "no matching org". Invalidated whenever `allowed_domain` is written, so
24
+ * correctness does not rest on the TTL.
25
+ */
26
+ const NO_DOMAIN_MATCH_TTL_MS = 60_000;
27
+ const noDomainMatchCache = createTtlCache({
28
+ ttlMs: NO_DOMAIN_MATCH_TTL_MS,
29
+ maxEntries: 512,
30
+ });
31
+ /**
32
+ * Call after any write that could make a domain start matching an org —
33
+ * `organizations.allowed_domain` updates and org creation. Clears every domain
34
+ * rather than one: the caller knows which org changed, not which domains a
35
+ * stale negative was recorded for.
36
+ */
37
+ export function invalidateDomainMatchCache() {
38
+ noDomainMatchCache.clear();
39
+ }
40
+ /** Test seam — the cache is module state, so suites must be able to clear it. */
41
+ export function __resetDomainMatchCacheForTests() {
42
+ noDomainMatchCache.clear();
43
+ }
7
44
  /**
8
45
  * Auto-join a newly-signed-up user into every org whose `allowed_domain`
9
46
  * matches their email domain.
@@ -31,6 +68,14 @@ export async function autoJoinDomainMatchingOrgs(rawEmail, options = {}) {
31
68
  const domain = email.split("@")[1]?.toLowerCase();
32
69
  if (!domain)
33
70
  return { joined: [], activeOrgId: null };
71
+ // `org/handlers.ts` REFUSES to set `allowed_domain` to a free provider, so no
72
+ // org can ever match `gmail.com` — every consumer-email account was probing
73
+ // for a row that is structurally impossible to exist.
74
+ if (isFreeEmailProvider(domain))
75
+ return { joined: [], activeOrgId: null };
76
+ if (noDomainMatchCache.get(domain)) {
77
+ return { joined: [], activeOrgId: null };
78
+ }
34
79
  const db = getDbExec();
35
80
  let matches = [];
36
81
  try {
@@ -53,11 +98,15 @@ export async function autoJoinDomainMatchingOrgs(rawEmail, options = {}) {
53
98
  }
54
99
  catch {
55
100
  // Template without org tables (or `allowed_domain` column not yet
56
- // migrated). Not fatal — return empty.
101
+ // migrated). Not fatal — return empty. Deliberately NOT cached: this branch
102
+ // cannot tell "no org matches" from "the table was unreadable", and caching
103
+ // it would let one blip answer every later request for a minute.
57
104
  return { joined: [], activeOrgId: null };
58
105
  }
59
- if (matches.length === 0)
106
+ if (matches.length === 0) {
107
+ noDomainMatchCache.set(domain, true);
60
108
  return { joined: [], activeOrgId: null };
109
+ }
61
110
  const joined = [];
62
111
  for (const m of matches) {
63
112
  try {
@@ -66,7 +115,7 @@ export async function autoJoinDomainMatchingOrgs(rawEmail, options = {}) {
66
115
  args: [nanoid(), m.orgId, email, Date.now()],
67
116
  });
68
117
  joined.push({ orgId: m.orgId });
69
- invalidateRequestMemberOrgIds();
118
+ invalidateMemberOrgCaches();
70
119
  }
71
120
  catch {
72
121
  // Race with a parallel join (e.g. user accepted an invite to the
@@ -5,7 +5,8 @@ import { getSetting } from "../settings/store.js";
5
5
  import { getUserSetting } from "../settings/user-settings.js";
6
6
  import { setActiveOrgId } from "./active-org.js";
7
7
  import { autoJoinDomainMatchingOrgs } from "./auto-join-domain.js";
8
- import { invalidateRequestMemberOrgIds, requestMemberOrgIds, } from "./request-org-cache.js";
8
+ import { isFreeEmailProvider } from "./free-email-providers.js";
9
+ import { cachedMemberships, invalidateMemberOrgCaches, requestMemberOrgIds, } from "./request-org-cache.js";
9
10
  const EMPTY_CONTEXT = {
10
11
  email: "",
11
12
  orgId: null,
@@ -197,8 +198,13 @@ async function resolveOrgContextUncached(event) {
197
198
  // signal. Recognizing the personal workspace by its *name* instead breaks the
198
199
  // moment the provider display name or the workspace name changes, which
199
200
  // strands an existing user in Personal with no way into their company org.
201
+ // `isFreeEmailProvider` short-circuits before the round trip rather than
202
+ // relying on the negative cache inside `autoJoinDomainMatchingOrgs`: a
203
+ // consumer-email domain can NEVER match (see `org/handlers.ts`, which refuses
204
+ // to set `allowed_domain` to a free provider), so it needs no TTL at all.
200
205
  const shouldTryDomainAutoJoin = !explicitPersonal &&
201
206
  emailDomain !== null &&
207
+ !isFreeEmailProvider(emailDomain) &&
202
208
  !memberships.some((m) => m.allowedDomain?.toLowerCase() === emailDomain);
203
209
  if (!explicitPersonal &&
204
210
  activeOrgSetting?.orgId &&
@@ -307,6 +313,9 @@ async function resolveOrgContextUncached(event) {
307
313
  */
308
314
  const MEMBERSHIP_FALLBACK_ORDER_BY = `ORDER BY joined_at ASC, org_id ASC`;
309
315
  async function loadMemberships(email) {
316
+ return cachedMemberships(email, () => loadMembershipsUncached(email));
317
+ }
318
+ async function loadMembershipsUncached(email) {
310
319
  const rows = await queryOrgMembers({
311
320
  sql: `SELECT m.org_id AS "orgId", m.role AS role, o.name AS "orgName",
312
321
  o.allowed_domain AS "allowedDomain"
@@ -403,7 +412,7 @@ export async function createOrganization(name, email, role = "owner") {
403
412
  sql: `INSERT INTO org_members (id, org_id, email, role, joined_at) VALUES (?, ?, ?, ?, ?)`,
404
413
  args: [nanoid(), id, email, role, createdAt],
405
414
  });
406
- invalidateRequestMemberOrgIds();
415
+ invalidateMemberOrgCaches();
407
416
  await warnOnAdditionalOrganization(exec, email, id, trimmedName);
408
417
  await setActiveOrgId(email, id, `created organization "${trimmedName}"`);
409
418
  return { id, name: trimmedName, role, a2aSecret, createdAt };
@@ -558,7 +567,7 @@ async function tryCreateDefaultOrg(exec, email, session) {
558
567
  sql: `INSERT INTO org_members (id, org_id, email, role, joined_at) VALUES (?, ?, ?, ?, ?)`,
559
568
  args: [nanoid(), orgId, email, "owner", now],
560
569
  });
561
- invalidateRequestMemberOrgIds();
570
+ invalidateMemberOrgCaches();
562
571
  await setActiveOrgId(email, orgId, "auto-created default organization");
563
572
  return { email, orgId, orgName, role: "owner" };
564
573
  }
@@ -37,9 +37,10 @@ import { sendEmail, isEmailConfigured } from "../server/email.js";
37
37
  import { readBody } from "../server/h3-helpers.js";
38
38
  import { setActiveOrgId } from "./active-org.js";
39
39
  import { setRequiredAuthProvider } from "./auth-policy.js";
40
+ import { invalidateDomainMatchCache } from "./auto-join-domain.js";
40
41
  import { getOrgContext, createOrganization } from "./context.js";
41
42
  import { isFreeEmailProvider } from "./free-email-providers.js";
42
- import { invalidateRequestMemberOrgIds } from "./request-org-cache.js";
43
+ import { invalidateMemberOrgCaches } from "./request-org-cache.js";
43
44
  import { parseWorkspaceUrl } from "./workspace-url.js";
44
45
  function getInviteAppUrl(event) {
45
46
  return getAppProductionUrl(event);
@@ -426,7 +427,7 @@ export const acceptInvitationHandler = defineEventHandler(async (event) => {
426
427
  sql: `INSERT INTO org_members (id, org_id, email, role, joined_at) VALUES (?, ?, ?, ?, ?)`,
427
428
  args: [nanoid(), invOrgId, email, inviteRole, Date.now()],
428
429
  });
429
- invalidateRequestMemberOrgIds();
430
+ invalidateMemberOrgCaches();
430
431
  await e.execute({
431
432
  sql: `UPDATE org_invitations SET status = 'accepted' WHERE id = ?`,
432
433
  args: [invitationId],
@@ -487,7 +488,7 @@ export const removeMemberHandler = defineEventHandler(async (event) => {
487
488
  sql: `DELETE FROM org_members WHERE org_id = ? AND LOWER(email) = ?`,
488
489
  args: [ctx.orgId, memberEmailLower],
489
490
  });
490
- invalidateRequestMemberOrgIds();
491
+ invalidateMemberOrgCaches();
491
492
  return { success: true };
492
493
  });
493
494
  /**
@@ -555,6 +556,7 @@ export const changeMemberRoleHandler = defineEventHandler(async (event) => {
555
556
  sql: `UPDATE org_members SET role = ? WHERE org_id = ? AND LOWER(email) = ?`,
556
557
  args: [role, ctx.orgId, memberEmailLower],
557
558
  });
559
+ invalidateMemberOrgCaches();
558
560
  return { email: memberEmailLower, role };
559
561
  });
560
562
  /** PATCH /_agent-native/org — rename the current organization (owner/admin only) */
@@ -582,6 +584,9 @@ export const updateOrgHandler = defineEventHandler(async (event) => {
582
584
  sql: `UPDATE organizations SET name = ? WHERE id = ?`,
583
585
  args: [name, ctx.orgId],
584
586
  });
587
+ // `orgName` is joined into the cached membership rows, so a rename that skips
588
+ // this leaves every member's org context showing the old name for the TTL.
589
+ invalidateMemberOrgCaches();
585
590
  return { orgId: ctx.orgId, name };
586
591
  });
587
592
  /**
@@ -661,7 +666,7 @@ export const deleteOrgHandler = defineEventHandler(async (event) => {
661
666
  message: "Organization deletion requires atomic database support",
662
667
  });
663
668
  }
664
- invalidateRequestMemberOrgIds();
669
+ invalidateMemberOrgCaches();
665
670
  const nextRes = await e.execute({
666
671
  sql: `SELECT org_id AS "orgId" FROM org_members WHERE LOWER(email) = ? LIMIT 1`,
667
672
  args: [ctx.email.toLowerCase()],
@@ -744,7 +749,7 @@ export const joinByDomainHandler = defineEventHandler(async (event) => {
744
749
  sql: `INSERT INTO org_members (id, org_id, email, role, joined_at) VALUES (?, ?, ?, 'member', ?)`,
745
750
  args: [nanoid(), orgId, email, Date.now()],
746
751
  });
747
- invalidateRequestMemberOrgIds();
752
+ invalidateMemberOrgCaches();
748
753
  await setActiveOrgId(email, orgId, "joined domain-matched organization");
749
754
  return {
750
755
  orgId,
@@ -813,6 +818,13 @@ export const setDomainHandler = defineEventHandler(async (event) => {
813
818
  sql: `UPDATE organizations SET allowed_domain = ? WHERE id = ?`,
814
819
  args: [raw, ctx.orgId],
815
820
  });
821
+ // A domain that previously matched nothing now matches this org. Without this
822
+ // the negative cache keeps every account at that domain out of it for the
823
+ // rest of the TTL, right after an owner deliberately turned domain-join on.
824
+ invalidateDomainMatchCache();
825
+ // `allowedDomain` is joined into the cached membership rows too, and existing
826
+ // members read it to decide whether a domain auto-join is still needed.
827
+ invalidateMemberOrgCaches();
816
828
  return { domain: raw };
817
829
  });
818
830
  /**
@@ -6,10 +6,32 @@
6
6
  */
7
7
  export declare function requestMemberOrgIds(email: string, load: () => Promise<string[] | null>): Promise<string[] | null>;
8
8
  /**
9
- * Drop this request's memoized memberships after a write to `org_members`.
9
+ * Read `email`'s memberships through the process cache, falling back to `load`.
10
+ *
11
+ * `load` returning `null`/empty-on-failure is the caller's contract to signal
12
+ * "unreadable"; pass `cacheable: false` for such a result and it is not stored.
13
+ */
14
+ export declare function cachedMemberships<T>(email: string, load: () => Promise<T[] | null>): Promise<T[] | null>;
15
+ /**
16
+ * Drop the memoized memberships after a write to `org_members`, in BOTH the
17
+ * per-request memo and the cross-request cache.
18
+ *
10
19
  * Clears every email rather than one: deleting an organization or removing a
11
20
  * member changes the answer for accounts other than the one being written.
12
21
  * Requests already in flight elsewhere keep their own snapshot for the rest of
13
22
  * their (short) lifetime, the same tradeoff the settings cache documents.
23
+ *
24
+ * Every membership write must route through here. A caller that clears only the
25
+ * request memo leaves the process cache serving the pre-write answer for the
26
+ * rest of the TTL — the user joins an org and the app keeps insisting they
27
+ * haven't.
28
+ *
29
+ * "Membership write" is wider than `INSERT`/`DELETE org_members`: the cached
30
+ * rows are a JOIN, so `org_members.role` and the `organizations.name` /
31
+ * `organizations.allowed_domain` columns it selects are cached too. Demoting an
32
+ * admin, renaming an org, or turning domain-join on without calling this keeps
33
+ * the pre-write answer authorizing and rendering requests until the TTL lapses.
14
34
  */
15
- export declare function invalidateRequestMemberOrgIds(): void;
35
+ export declare function invalidateMemberOrgCaches(): void;
36
+ /** Test seam — the process cache is module state, so suites must clear it. */
37
+ export declare function __resetProcessMemberOrgCacheForTests(): void;
@@ -1,4 +1,5 @@
1
1
  import { getRequestContext } from "../server/request-context.js";
2
+ import { createTtlCache } from "../shared/ttl-cache.js";
2
3
  /**
3
4
  * Per-request memo of the `org_members` read behind `resolveOrgIdForEmail`,
4
5
  * keyed on the active AsyncLocalStorage `RequestContext` (WeakMap → freed with
@@ -50,12 +51,66 @@ export function requestMemberOrgIds(email, load) {
50
51
  return pending;
51
52
  }
52
53
  /**
53
- * Drop this request's memoized memberships after a write to `org_members`.
54
+ * Cross-request cache for the full membership rows behind `getOrgContext`.
55
+ *
56
+ * The per-request memo above only collapses repeated reads inside ONE request.
57
+ * Every authenticated request still paid its own `org_members` round trip, and
58
+ * production showed 494,785 of them — one per request, 1:1 with the session
59
+ * lookup, for memberships that change on the order of days.
60
+ *
61
+ * Only a SUCCESSFUL read is stored. `loadMemberships` returns `null` for an
62
+ * unreadable `org_members` (no such table on a template that skips the org
63
+ * module, a role without SELECT); caching that would turn a permissions blip
64
+ * into a minute of silently org-less requests, which drops org scope and hides
65
+ * every org-scoped credential behind a permanent-sounding "not configured".
66
+ */
67
+ const MEMBER_ORGS_TTL_MS = 15_000;
68
+ const processMemberships = createTtlCache({
69
+ ttlMs: MEMBER_ORGS_TTL_MS,
70
+ maxEntries: 2_048,
71
+ });
72
+ /**
73
+ * Read `email`'s memberships through the process cache, falling back to `load`.
74
+ *
75
+ * `load` returning `null`/empty-on-failure is the caller's contract to signal
76
+ * "unreadable"; pass `cacheable: false` for such a result and it is not stored.
77
+ */
78
+ export async function cachedMemberships(email, load) {
79
+ const key = email.trim().toLowerCase();
80
+ const hit = processMemberships.get(key);
81
+ if (hit)
82
+ return hit;
83
+ const rows = await load();
84
+ // `null` is "unreadable", never "no memberships" — see the doc comment above.
85
+ if (rows !== null)
86
+ processMemberships.set(key, rows);
87
+ return rows;
88
+ }
89
+ /**
90
+ * Drop the memoized memberships after a write to `org_members`, in BOTH the
91
+ * per-request memo and the cross-request cache.
92
+ *
54
93
  * Clears every email rather than one: deleting an organization or removing a
55
94
  * member changes the answer for accounts other than the one being written.
56
95
  * Requests already in flight elsewhere keep their own snapshot for the rest of
57
96
  * their (short) lifetime, the same tradeoff the settings cache documents.
97
+ *
98
+ * Every membership write must route through here. A caller that clears only the
99
+ * request memo leaves the process cache serving the pre-write answer for the
100
+ * rest of the TTL — the user joins an org and the app keeps insisting they
101
+ * haven't.
102
+ *
103
+ * "Membership write" is wider than `INSERT`/`DELETE org_members`: the cached
104
+ * rows are a JOIN, so `org_members.role` and the `organizations.name` /
105
+ * `organizations.allowed_domain` columns it selects are cached too. Demoting an
106
+ * admin, renaming an org, or turning domain-join on without calling this keeps
107
+ * the pre-write answer authorizing and rendering requests until the TTL lapses.
58
108
  */
59
- export function invalidateRequestMemberOrgIds() {
109
+ export function invalidateMemberOrgCaches() {
60
110
  cacheForRequest(false)?.clear();
111
+ processMemberships.clear();
112
+ }
113
+ /** Test seam — the process cache is module state, so suites must clear it. */
114
+ export function __resetProcessMemberOrgCacheForTests() {
115
+ processMemberships.clear();
61
116
  }
@@ -75,6 +75,7 @@ export declare function createCustomProviderRegistrationAction<TSchema extends Z
75
75
  user: "user";
76
76
  }>>;
77
77
  }, z.core.$strip>>, {
78
+ message?: undefined;
78
79
  deleted?: undefined;
79
80
  found?: undefined;
80
81
  providers: {
@@ -91,8 +92,8 @@ export declare function createCustomProviderRegistrationAction<TSchema extends Z
91
92
  registered?: undefined;
92
93
  id?: undefined;
93
94
  label?: undefined;
94
- message?: undefined;
95
95
  } | {
96
+ message?: undefined;
96
97
  deleted?: undefined;
97
98
  providers?: undefined;
98
99
  count?: undefined;
@@ -101,8 +102,8 @@ export declare function createCustomProviderRegistrationAction<TSchema extends Z
101
102
  registered?: undefined;
102
103
  id?: undefined;
103
104
  label?: undefined;
104
- message?: undefined;
105
105
  } | {
106
+ message?: undefined;
106
107
  deleted?: undefined;
107
108
  providers?: undefined;
108
109
  count?: undefined;
@@ -111,8 +112,8 @@ export declare function createCustomProviderRegistrationAction<TSchema extends Z
111
112
  id: string;
112
113
  registered?: undefined;
113
114
  label?: undefined;
114
- message?: undefined;
115
115
  } | {
116
+ message?: undefined;
116
117
  found?: undefined;
117
118
  providers?: undefined;
118
119
  count?: undefined;
@@ -121,7 +122,6 @@ export declare function createCustomProviderRegistrationAction<TSchema extends Z
121
122
  id: string;
122
123
  registered?: undefined;
123
124
  label?: undefined;
124
- message?: undefined;
125
125
  } | {
126
126
  deleted?: undefined;
127
127
  found?: undefined;
@@ -311,9 +311,9 @@ export declare function createProviderApiActions(runtime: Pick<ProviderApiRuntim
311
311
  notes?: string | undefined;
312
312
  scope?: "org" | "user" | undefined;
313
313
  }, {
314
+ deleted?: undefined;
314
315
  id?: undefined;
315
316
  provider?: undefined;
316
- deleted?: undefined;
317
317
  found?: undefined;
318
318
  providers: {
319
319
  id: string;
@@ -329,9 +329,9 @@ export declare function createProviderApiActions(runtime: Pick<ProviderApiRuntim
329
329
  label?: undefined;
330
330
  message?: undefined;
331
331
  } | {
332
- id?: undefined;
333
332
  deleted?: undefined;
334
333
  count?: undefined;
334
+ id?: undefined;
335
335
  providers?: undefined;
336
336
  found: boolean;
337
337
  provider: import("../custom-registry.js").CustomProviderConfig;
@@ -339,9 +339,9 @@ export declare function createProviderApiActions(runtime: Pick<ProviderApiRuntim
339
339
  label?: undefined;
340
340
  message?: undefined;
341
341
  } | {
342
- provider?: undefined;
343
342
  deleted?: undefined;
344
343
  count?: undefined;
344
+ provider?: undefined;
345
345
  providers?: undefined;
346
346
  found: boolean;
347
347
  id: string;
@@ -349,9 +349,9 @@ export declare function createProviderApiActions(runtime: Pick<ProviderApiRuntim
349
349
  label?: undefined;
350
350
  message?: undefined;
351
351
  } | {
352
+ count?: undefined;
352
353
  provider?: undefined;
353
354
  found?: undefined;
354
- count?: undefined;
355
355
  providers?: undefined;
356
356
  deleted: boolean;
357
357
  id: string;
@@ -359,10 +359,10 @@ export declare function createProviderApiActions(runtime: Pick<ProviderApiRuntim
359
359
  label?: undefined;
360
360
  message?: undefined;
361
361
  } | {
362
- provider?: undefined;
363
362
  deleted?: undefined;
364
- found?: undefined;
365
363
  count?: undefined;
364
+ provider?: undefined;
365
+ found?: undefined;
366
366
  providers?: undefined;
367
367
  registered: boolean;
368
368
  id: string;
@@ -1,5 +1,5 @@
1
1
  import { isLocalWorkspaceResourceId } from "../local-artifacts/index.js";
2
- import type { StoreWriteOptions } from "../settings/store.js";
2
+ import { type StoreWriteOptions } from "../settings/store.js";
3
3
  export declare const SHARED_OWNER = "__shared__";
4
4
  export declare const WORKSPACE_OWNER = "__workspace__";
5
5
  /**