@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.
- package/corpus/README.md +1 -1
- package/corpus/templates/brain/server/lib/capture-sanitization.ts +6 -2
- package/corpus/templates/clips/changelog/2026-08-07-full-screen-recordings-on-a-multi-monitor-mac-now-let-you-pi.md +6 -0
- package/corpus/templates/clips/chrome-extension/src/styles.css +13 -4
- package/corpus/templates/clips/desktop/src/app.tsx +38 -1
- package/corpus/templates/clips/desktop/src/components/MediaDeviceRow.tsx +123 -104
- package/corpus/templates/clips/desktop/src/lib/recorder.ts +165 -2
- package/corpus/templates/clips/desktop/src/main.tsx +3 -0
- package/corpus/templates/clips/desktop/src/overlays/monitor-picker.tsx +70 -0
- package/corpus/templates/clips/desktop/src/styles.css +70 -11
- package/corpus/templates/clips/desktop/src-tauri/capabilities/default.json +2 -1
- package/corpus/templates/clips/desktop/src-tauri/src/clips/mod.rs +163 -1
- package/corpus/templates/clips/desktop/src-tauri/src/lib.rs +6 -1
- package/corpus/templates/clips/desktop/src-tauri/src/native_screen.rs +141 -22
- package/corpus/templates/clips/desktop/src-tauri/src/state.rs +27 -1
- package/corpus/templates/clips/desktop/src-tauri/src/util.rs +12 -1
- package/dist/agent/engine/index.d.ts +1 -1
- package/dist/agent/engine/index.js +1 -1
- package/dist/agent/engine/registry.d.ts +15 -0
- package/dist/agent/engine/registry.js +39 -10
- package/dist/agent/production-agent.d.ts +40 -1
- package/dist/agent/production-agent.js +81 -40
- package/dist/agent/run-manager.d.ts +44 -1
- package/dist/agent/run-manager.js +70 -8
- package/dist/chat-threads/store.js +6 -4
- package/dist/client/use-db-sync.js +9 -1
- package/dist/db/ddl-guard.d.ts +8 -0
- package/dist/db/ddl-guard.js +148 -0
- package/dist/deploy/build.d.ts +36 -0
- package/dist/deploy/build.js +78 -5
- package/dist/integrations/webhook-handler.d.ts +3 -2
- package/dist/integrations/webhook-handler.js +8 -38
- package/dist/mcp/screen-memory-stdio.d.ts +7 -7
- package/dist/notifications/routes.d.ts +3 -3
- package/dist/observability/routes.d.ts +5 -5
- package/dist/org/accept-pending.js +2 -2
- package/dist/org/auth-policy.js +4 -0
- package/dist/org/auto-join-domain.d.ts +9 -0
- package/dist/org/auto-join-domain.js +53 -4
- package/dist/org/context.js +12 -3
- package/dist/org/handlers.js +17 -5
- package/dist/org/request-org-cache.d.ts +24 -2
- package/dist/org/request-org-cache.js +57 -2
- package/dist/provider-api/actions/custom-provider-registration.d.ts +4 -4
- package/dist/provider-api/actions/provider-api.d.ts +6 -6
- package/dist/resources/store.d.ts +1 -1
- package/dist/resources/store.js +96 -7
- package/dist/secrets/routes.d.ts +6 -6
- package/dist/server/agent-chat-plugin.js +83 -37
- package/dist/server/agent-engine-api-key-route.d.ts +1 -1
- package/dist/server/auth.d.ts +3 -0
- package/dist/server/auth.js +22 -1
- package/dist/server/collab-plugin.js +10 -4
- package/dist/server/complete-text.js +14 -7
- package/dist/server/core-routes-plugin.js +3 -0
- package/dist/server/index.d.ts +1 -1
- package/dist/server/index.js +1 -1
- package/dist/server/request-context.d.ts +7 -1
- package/dist/server/session-email-cache.d.ts +14 -0
- package/dist/server/session-email-cache.js +48 -0
- package/dist/server/sweep-runtime.d.ts +22 -0
- package/dist/server/sweep-runtime.js +28 -0
- package/dist/server/transcribe-voice.d.ts +1 -1
- package/dist/settings/store.js +63 -7
- package/dist/shared/ttl-cache.d.ts +59 -0
- package/dist/shared/ttl-cache.js +43 -0
- package/dist/sharing/actions/list-resource-shares.d.ts +3 -3
- package/dist/sharing/actions/list-resource-shares.js +4 -1
- package/dist/triggers/condition-evaluator.js +11 -27
- package/docs/content/deployment.mdx +48 -0
- package/docs/content/environment-variables.mdx +11 -0
- 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 {
|
|
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
|
-
|
|
66
|
+
invalidateMemberOrgCaches();
|
|
67
67
|
}
|
|
68
68
|
await db.execute({
|
|
69
69
|
sql: `UPDATE org_invitations SET status = 'accepted' WHERE id = ?`,
|
package/dist/org/auth-policy.js
CHANGED
|
@@ -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 {
|
|
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
|
-
|
|
118
|
+
invalidateMemberOrgCaches();
|
|
70
119
|
}
|
|
71
120
|
catch {
|
|
72
121
|
// Race with a parallel join (e.g. user accepted an invite to the
|
package/dist/org/context.js
CHANGED
|
@@ -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 {
|
|
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
|
-
|
|
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
|
-
|
|
570
|
+
invalidateMemberOrgCaches();
|
|
562
571
|
await setActiveOrgId(email, orgId, "auto-created default organization");
|
|
563
572
|
return { email, orgId, orgName, role: "owner" };
|
|
564
573
|
}
|
package/dist/org/handlers.js
CHANGED
|
@@ -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 {
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
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
|
|
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
|
-
*
|
|
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
|
|
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
|
|
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
|
/**
|