@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
package/dist/resources/store.js
CHANGED
|
@@ -4,6 +4,7 @@ import { ensureColumnExists, ensureIndexExists, ensureTableExists, } from "../db
|
|
|
4
4
|
import { widenIntColumnsToBigInt } from "../db/widen-columns.js";
|
|
5
5
|
import { canUseLocalWorkspaceResourcePath, deleteLocalWorkspaceResource, isLocalWorkspaceResourceId, isLocalWorkspaceResourcesEnabled, listLocalWorkspaceResources, localWorkspaceResourcePathFromId, readLocalWorkspaceResource, writeLocalWorkspaceResource, } from "../local-artifacts/index.js";
|
|
6
6
|
import { getRequestOrgId, getRequestUserEmail, } from "../server/request-context.js";
|
|
7
|
+
import { getSetting, putSetting, } from "../settings/store.js";
|
|
7
8
|
import { emitResourceChange, emitResourceDelete } from "./emitter.js";
|
|
8
9
|
export const SHARED_OWNER = "__shared__";
|
|
9
10
|
export const WORKSPACE_OWNER = "__workspace__";
|
|
@@ -570,14 +571,34 @@ async function grantedWorkspaceResourceByPath(path, options) {
|
|
|
570
571
|
return null;
|
|
571
572
|
}
|
|
572
573
|
}
|
|
573
|
-
|
|
574
|
+
/**
|
|
575
|
+
* Expire agent scratch resources, WITHOUT blocking the read that triggered it.
|
|
576
|
+
*
|
|
577
|
+
* This is garbage collection, not part of any read's answer: a caller asking for
|
|
578
|
+
* one resource does not need yesterday's expired scratch rows gone first. Awaiting
|
|
579
|
+
* it made every read path issue a `DELETE` before its own `SELECT` — double the
|
|
580
|
+
* round trips, unservable by a replica, taking row locks inside a request a user
|
|
581
|
+
* is waiting on, and (since nothing caught it) able to fail the read outright.
|
|
582
|
+
*
|
|
583
|
+
* The throttle is a module-scope timestamp, so it starts at 0 in a fresh isolate
|
|
584
|
+
* and the first resource read after every cold start still triggers one sweep.
|
|
585
|
+
* That is why the index below exists, and why this must not be awaited.
|
|
586
|
+
*/
|
|
587
|
+
function scheduleExpiredAgentScratchCleanup(client) {
|
|
574
588
|
const now = Date.now();
|
|
575
589
|
if (now - _lastScratchCleanupAt < SCRATCH_CLEANUP_INTERVAL_MS)
|
|
576
590
|
return;
|
|
577
591
|
_lastScratchCleanupAt = now;
|
|
578
|
-
|
|
592
|
+
void client
|
|
593
|
+
.execute({
|
|
579
594
|
sql: `DELETE FROM resources WHERE visibility = ? AND expires_at IS NOT NULL AND expires_at <= ?`,
|
|
580
595
|
args: ["agent_scratch", now],
|
|
596
|
+
})
|
|
597
|
+
.catch((err) => {
|
|
598
|
+
// Say so rather than swallowing: scratch rows accumulating forever is a
|
|
599
|
+
// slow leak nobody would otherwise notice.
|
|
600
|
+
// coercion-ok: failed GC degrades storage over time, never read correctness
|
|
601
|
+
console.warn("[resources] expired agent-scratch cleanup failed; will retry on a later read:", err?.message ?? err);
|
|
581
602
|
});
|
|
582
603
|
}
|
|
583
604
|
async function ensureTable() {
|
|
@@ -655,7 +676,7 @@ async function _doEnsureTable() {
|
|
|
655
676
|
"updated_at",
|
|
656
677
|
"expires_at",
|
|
657
678
|
]);
|
|
658
|
-
// `
|
|
679
|
+
// `scheduleExpiredAgentScratchCleanup` filters on exactly these two columns
|
|
659
680
|
// and, without this, full-scans `resources` — a table shared by every
|
|
660
681
|
// template and read from agent discovery, docs, chat scratch and remote-agent
|
|
661
682
|
// manifests. Its 60s throttle is a module-scope timestamp that starts at 0 in
|
|
@@ -669,7 +690,20 @@ async function _doEnsureTable() {
|
|
|
669
690
|
// coercion-ok: absence of an index degrades latency, never correctness
|
|
670
691
|
console.warn("[resources] could not ensure resources_visibility_expires_idx; scratch cleanup will full-scan:", err?.message ?? err);
|
|
671
692
|
});
|
|
672
|
-
// Seed default shared resources if they don't exist (INSERT OR IGNORE to avoid
|
|
693
|
+
// Seed default shared resources if they don't exist (INSERT OR IGNORE to avoid
|
|
694
|
+
// race conditions).
|
|
695
|
+
//
|
|
696
|
+
// Guarded by a durable marker: `_doEnsureTable` runs once per PROCESS, which on
|
|
697
|
+
// serverless is once per cold start, so this block was issuing ~10 writes and
|
|
698
|
+
// 2 migration scans per container to insert rows that had existed since day
|
|
699
|
+
// one (53,785 of them in one production sample). The marker makes it once per
|
|
700
|
+
// database, matching what the comments here already assumed.
|
|
701
|
+
//
|
|
702
|
+
// Consequence worth knowing: a default resource the user DELETES is no longer
|
|
703
|
+
// recreated on the next cold start. That silent resurrection was never
|
|
704
|
+
// intended — see `RESOURCE_SEED_VERSION` to force a re-seed.
|
|
705
|
+
if (await alreadySeeded(SHARED_SEED_KEY))
|
|
706
|
+
return;
|
|
673
707
|
const now = Date.now();
|
|
674
708
|
const seedSql = isPostgres()
|
|
675
709
|
? `INSERT INTO resources (id, path, owner, content, mime_type, size, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?) ON CONFLICT (path, owner) DO NOTHING`
|
|
@@ -797,8 +831,54 @@ async function _doEnsureTable() {
|
|
|
797
831
|
catch {
|
|
798
832
|
// Migration best-effort
|
|
799
833
|
}
|
|
834
|
+
await markSeeded(SHARED_SEED_KEY);
|
|
800
835
|
}
|
|
836
|
+
/**
|
|
837
|
+
* Bump when a default resource is ADDED, or when existing seed content must
|
|
838
|
+
* reach databases that already ran an earlier seed.
|
|
839
|
+
*
|
|
840
|
+
* The marker below records which version a database has been seeded at, so the
|
|
841
|
+
* seed runs once per database instead of once per process. Without a version, a
|
|
842
|
+
* newly added default would never reach an already-seeded deployment.
|
|
843
|
+
*/
|
|
844
|
+
const RESOURCE_SEED_VERSION = 1;
|
|
845
|
+
/**
|
|
846
|
+
* Per-process memo ON TOP of the durable marker, so a warm process doesn't even
|
|
847
|
+
* pay the marker read. The durable marker is what makes this correct across cold
|
|
848
|
+
* starts; this only avoids a repeat lookup within one process.
|
|
849
|
+
*/
|
|
801
850
|
const _personalSeeded = new Set();
|
|
851
|
+
function personalSeedKey(owner) {
|
|
852
|
+
return `resources-seeded:personal:v${RESOURCE_SEED_VERSION}:${owner.toLowerCase()}`;
|
|
853
|
+
}
|
|
854
|
+
const SHARED_SEED_KEY = `resources-seeded:shared:v${RESOURCE_SEED_VERSION}`;
|
|
855
|
+
/**
|
|
856
|
+
* Has this database already been seeded at the current version?
|
|
857
|
+
*
|
|
858
|
+
* A failed marker read returns `false` — "seed again" — because the seeds are
|
|
859
|
+
* `ON CONFLICT DO NOTHING` and therefore idempotent. Guessing "already seeded"
|
|
860
|
+
* on an unreadable marker would skip seeding a fresh database and leave it
|
|
861
|
+
* without its default AGENTS.md / LEARNINGS.md, which is not recoverable by
|
|
862
|
+
* retrying a read.
|
|
863
|
+
*/
|
|
864
|
+
async function alreadySeeded(key) {
|
|
865
|
+
try {
|
|
866
|
+
const row = await getSetting(key);
|
|
867
|
+
return row?.at !== undefined;
|
|
868
|
+
}
|
|
869
|
+
catch {
|
|
870
|
+
// coercion-ok: "not seeded" is the SAFE answer, and it is not indistinguishable from success — it re-runs an idempotent ON CONFLICT DO NOTHING seed, exactly the pre-marker behaviour. Guessing "already seeded" would leave a fresh database permanently without its default AGENTS.md / LEARNINGS.md, which no retry recovers.
|
|
871
|
+
return false;
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
async function markSeeded(key) {
|
|
875
|
+
try {
|
|
876
|
+
await putSetting(key, { at: Date.now() });
|
|
877
|
+
}
|
|
878
|
+
catch {
|
|
879
|
+
// coercion-ok: the marker is an optimization, not state anything reads for correctness. Failing to write it costs one repeat idempotent seed on the next cold start — the pre-marker behaviour — and the seed itself already succeeded, so there is no failure to report to the caller.
|
|
880
|
+
}
|
|
881
|
+
}
|
|
802
882
|
/**
|
|
803
883
|
* Seed personal AGENTS.md and LEARNINGS.md for a user if they don't exist.
|
|
804
884
|
* Called when listing resources or from the agent chat plugin.
|
|
@@ -810,6 +890,14 @@ export async function ensurePersonalDefaults(owner) {
|
|
|
810
890
|
return;
|
|
811
891
|
}
|
|
812
892
|
await ensureTable();
|
|
893
|
+
// The in-memory Set resets on every cold start, so on serverless this seed ran
|
|
894
|
+
// its 5 writes per container per owner, forever. The durable marker is what
|
|
895
|
+
// actually makes it once-per-owner.
|
|
896
|
+
const seedKey = personalSeedKey(owner);
|
|
897
|
+
if (await alreadySeeded(seedKey)) {
|
|
898
|
+
_personalSeeded.add(owner);
|
|
899
|
+
return;
|
|
900
|
+
}
|
|
813
901
|
const client = getDbExec();
|
|
814
902
|
const now = Date.now();
|
|
815
903
|
const seedSql = isPostgres()
|
|
@@ -886,6 +974,7 @@ export async function ensurePersonalDefaults(owner) {
|
|
|
886
974
|
// retries instead of permanently skipping seeding. Seeds use INSERT OR IGNORE
|
|
887
975
|
// / ON CONFLICT DO NOTHING, so a concurrent re-run is harmless.
|
|
888
976
|
_personalSeeded.add(owner);
|
|
977
|
+
await markSeeded(seedKey);
|
|
889
978
|
}
|
|
890
979
|
function rowToResource(row) {
|
|
891
980
|
return {
|
|
@@ -932,7 +1021,7 @@ export async function resourceGet(id, options) {
|
|
|
932
1021
|
return localWorkspaceResourceById(id);
|
|
933
1022
|
}
|
|
934
1023
|
const client = getDbExec();
|
|
935
|
-
|
|
1024
|
+
scheduleExpiredAgentScratchCleanup(client);
|
|
936
1025
|
const { rows } = await client.execute({
|
|
937
1026
|
sql: `SELECT * FROM resources WHERE id = ?`,
|
|
938
1027
|
args: [id],
|
|
@@ -949,7 +1038,7 @@ export async function resourceGetByPath(owner, path, options) {
|
|
|
949
1038
|
return local;
|
|
950
1039
|
}
|
|
951
1040
|
const client = getDbExec();
|
|
952
|
-
|
|
1041
|
+
scheduleExpiredAgentScratchCleanup(client);
|
|
953
1042
|
const { rows } = await client.execute({
|
|
954
1043
|
sql: `SELECT * FROM resources WHERE owner = ? AND path = ?`,
|
|
955
1044
|
args: [owner, path],
|
|
@@ -1165,7 +1254,7 @@ export async function resourceDeleteByPath(owner, path) {
|
|
|
1165
1254
|
export async function resourceList(owner, pathPrefix, options) {
|
|
1166
1255
|
await ensureTable();
|
|
1167
1256
|
const client = getDbExec();
|
|
1168
|
-
|
|
1257
|
+
scheduleExpiredAgentScratchCleanup(client);
|
|
1169
1258
|
const visibilitySql = scratchFilterSql(options);
|
|
1170
1259
|
if (pathPrefix) {
|
|
1171
1260
|
const { rows } = await client.execute({
|
package/dist/secrets/routes.d.ts
CHANGED
|
@@ -37,17 +37,17 @@ export declare function createWriteSecretHandler(): import("h3").EventHandlerWit
|
|
|
37
37
|
ok?: undefined;
|
|
38
38
|
status?: undefined;
|
|
39
39
|
} | {
|
|
40
|
+
error?: undefined;
|
|
40
41
|
ok: boolean;
|
|
41
42
|
status: string;
|
|
42
|
-
error?: undefined;
|
|
43
43
|
} | {
|
|
44
44
|
ok?: undefined;
|
|
45
45
|
error: string;
|
|
46
46
|
removed?: undefined;
|
|
47
47
|
} | {
|
|
48
|
+
error?: undefined;
|
|
48
49
|
ok: boolean;
|
|
49
50
|
removed: boolean;
|
|
50
|
-
error?: undefined;
|
|
51
51
|
}>>;
|
|
52
52
|
/**
|
|
53
53
|
* POST /_agent-native/secrets/:key/test — validate an optional candidate value
|
|
@@ -58,13 +58,13 @@ export declare function createTestSecretHandler(): import("h3").EventHandlerWith
|
|
|
58
58
|
error: string;
|
|
59
59
|
note?: undefined;
|
|
60
60
|
} | {
|
|
61
|
+
error?: undefined;
|
|
61
62
|
ok: boolean;
|
|
62
63
|
note?: undefined;
|
|
63
|
-
error?: undefined;
|
|
64
64
|
} | {
|
|
65
|
+
error?: undefined;
|
|
65
66
|
ok: boolean;
|
|
66
67
|
note: string;
|
|
67
|
-
error?: undefined;
|
|
68
68
|
} | {
|
|
69
69
|
note?: undefined;
|
|
70
70
|
ok: boolean;
|
|
@@ -95,11 +95,11 @@ export interface AdHocSecretPayload {
|
|
|
95
95
|
export declare function createAdHocSecretHandler(): import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<AdHocSecretPayload[] | {
|
|
96
96
|
error: string;
|
|
97
97
|
} | {
|
|
98
|
+
error?: undefined;
|
|
98
99
|
ok: boolean;
|
|
99
100
|
key: string;
|
|
100
|
-
error?: undefined;
|
|
101
101
|
} | {
|
|
102
|
+
error?: undefined;
|
|
102
103
|
ok: boolean;
|
|
103
104
|
removed: boolean;
|
|
104
|
-
error?: undefined;
|
|
105
105
|
}>>;
|
|
@@ -11,6 +11,7 @@ import { DEFAULT_ANTHROPIC_MODEL } from "../agent/default-model.js";
|
|
|
11
11
|
import { AGENT_CHAT_BACKGROUND_RUN_FIELD, AGENT_CHAT_PROCESS_RUN_PATH, backgroundRunMarkerExpectsBackgroundRuntime, isInBackgroundFunctionRuntime, prepareProcessRunRequest, } from "../agent/durable-background.js";
|
|
12
12
|
import { resolveEngine, createAnthropicEngine, getStoredModelForEngine, normalizeModelForEngine, resolveDelegatedRunModel, getAgentEngineEntry, isAgentEnginePackageInstalled, isStoredEngineUsableForRequest, listAgentEngines, registerBuiltinEngines, } from "../agent/engine/index.js";
|
|
13
13
|
import { SYSTEM_PROMPT_CACHE_SPLIT } from "../agent/engine/prompt-cache.js";
|
|
14
|
+
import { PROVIDER_TO_ENV } from "../agent/engine/provider-env-vars.js";
|
|
14
15
|
import { createProductionAgentHandler, actionsToEngineTools, executeAgentToolCall, toolCallCacheKey, getActiveRunForThreadAsync, abortRunDurably, abortTurnDurably, subscribeToRun, } from "../agent/production-agent.js";
|
|
15
16
|
import { callerHasRunAccess, callerHasThreadAccess, } from "../agent/run-ownership.js";
|
|
16
17
|
import { markTurnAborted, readBackgroundRunClaim } from "../agent/run-store.js";
|
|
@@ -117,6 +118,7 @@ import { isLocalhost, shouldBlockInProductCodeEditingSurface, } from "./agent-ch
|
|
|
117
118
|
import { loadRunCodeToolEntries } from "./agent-chat/run-code-tools.js";
|
|
118
119
|
import { createAgentEngineScriptEntries, createAgentLoopSettingsScriptEntries, createCallAgentScriptEntry, createChatScriptEntries, createDbScriptEntries, createDocsScriptEntries, createResourceScriptEntries, } from "./agent-chat/script-entries.js";
|
|
119
120
|
import { parseSkillFrontmatter } from "./agent-chat/skill-frontmatter.js";
|
|
121
|
+
import { shouldDisableInProcessSweeps } from "./sweep-runtime.js";
|
|
120
122
|
export { loadResourcesForPrompt };
|
|
121
123
|
export { _agentChatPromptSectionsForTests };
|
|
122
124
|
export { buildPublicAgentA2ASkills };
|
|
@@ -1239,8 +1241,12 @@ export function createAgentChatPlugin(options) {
|
|
|
1239
1241
|
}
|
|
1240
1242
|
// Use the SAME agent setup as the interactive chat — identical tools,
|
|
1241
1243
|
// prompt, and capabilities. The A2A agent IS the app's agent.
|
|
1242
|
-
const {
|
|
1243
|
-
const ownerApiKey = await
|
|
1244
|
+
const { resolveOwnerEngineApiKey } = await import("../agent/production-agent.js");
|
|
1245
|
+
const { apiKey: ownerApiKey, apiKeyEnvVar: ownerApiKeyEnvVar } = await resolveOwnerEngineApiKey({
|
|
1246
|
+
engineOption: options?.engine,
|
|
1247
|
+
ownerEmail: userEmail,
|
|
1248
|
+
anthropicFallback: options?.apiKey,
|
|
1249
|
+
});
|
|
1244
1250
|
// A2A runs are reconstructed in a fresh processor request, so they
|
|
1245
1251
|
// do not pass through the interactive handler's prepareRun hook.
|
|
1246
1252
|
// Seed the same mutable run context before resolving the engine and
|
|
@@ -1253,6 +1259,7 @@ export function createAgentChatPlugin(options) {
|
|
|
1253
1259
|
if (a2aRunContext) {
|
|
1254
1260
|
a2aRunContext.owner = userEmail;
|
|
1255
1261
|
a2aRunContext.userApiKey = ownerApiKey;
|
|
1262
|
+
a2aRunContext.userApiKeyEnvVar = ownerApiKeyEnvVar;
|
|
1256
1263
|
// The async processor restores the original request origin from
|
|
1257
1264
|
// task metadata. Only derive a fallback for synchronous A2A calls
|
|
1258
1265
|
// where the inbound event is still the caller request.
|
|
@@ -1273,7 +1280,8 @@ export function createAgentChatPlugin(options) {
|
|
|
1273
1280
|
}
|
|
1274
1281
|
const a2aEngine = await resolveEngine({
|
|
1275
1282
|
engineOption: options?.engine,
|
|
1276
|
-
apiKey: ownerApiKey
|
|
1283
|
+
apiKey: ownerApiKey,
|
|
1284
|
+
apiKeyEnvVar: ownerApiKeyEnvVar,
|
|
1277
1285
|
appId: options?.appId,
|
|
1278
1286
|
});
|
|
1279
1287
|
const devActive = isDevMode();
|
|
@@ -1683,13 +1691,27 @@ export function createAgentChatPlugin(options) {
|
|
|
1683
1691
|
askAgent: async (message) => {
|
|
1684
1692
|
const ownerEmail = getRequestUserEmail();
|
|
1685
1693
|
const mcpRunId = crypto.randomUUID();
|
|
1686
|
-
const {
|
|
1687
|
-
const ownerApiKey =
|
|
1688
|
-
|
|
1689
|
-
|
|
1694
|
+
const { resolveOwnerEngineApiKey } = await import("../agent/production-agent.js");
|
|
1695
|
+
const ownerApiKey = await resolveOwnerEngineApiKey({
|
|
1696
|
+
engineOption: options?.engine,
|
|
1697
|
+
ownerEmail,
|
|
1698
|
+
anthropicFallback: options?.apiKey,
|
|
1699
|
+
});
|
|
1700
|
+
// `ask_app` runs outside the interactive handler, so nothing seeds
|
|
1701
|
+
// the run context for it — and `onEngineResolved` never fires on
|
|
1702
|
+
// this path either. Without the resolved key and its provenance
|
|
1703
|
+
// here, an agent-team sub-agent spawned from an MCP run falls back
|
|
1704
|
+
// to the plugin host key while the parent bills the owner's BYO
|
|
1705
|
+
// credential. Same reason the A2A branch above seeds it.
|
|
1706
|
+
const mcpRunContext = ensureRequestRunContext();
|
|
1707
|
+
if (mcpRunContext) {
|
|
1708
|
+
mcpRunContext.userApiKey = ownerApiKey.apiKey;
|
|
1709
|
+
mcpRunContext.userApiKeyEnvVar = ownerApiKey.apiKeyEnvVar;
|
|
1710
|
+
}
|
|
1690
1711
|
const mcpEngine = await resolveEngine({
|
|
1691
1712
|
engineOption: options?.engine,
|
|
1692
|
-
apiKey: ownerApiKey
|
|
1713
|
+
apiKey: ownerApiKey.apiKey,
|
|
1714
|
+
apiKeyEnvVar: ownerApiKey.apiKeyEnvVar,
|
|
1693
1715
|
appId: options?.appId,
|
|
1694
1716
|
});
|
|
1695
1717
|
const mcpModelCandidate = options?.model ??
|
|
@@ -2179,12 +2201,19 @@ export function createAgentChatPlugin(options) {
|
|
|
2179
2201
|
getActions: buildSubAgentActions,
|
|
2180
2202
|
getEngine: () => {
|
|
2181
2203
|
const runCtx = getRequestRunContext();
|
|
2204
|
+
// Sub-agents must inherit the parent run's resolved key so
|
|
2205
|
+
// delegations spawned by agent-teams don't silently fall back to
|
|
2206
|
+
// the platform key while the parent uses BYO credentials. This
|
|
2207
|
+
// fallback engine is Anthropic, so a key the parent resolved for
|
|
2208
|
+
// another provider is not inheritable — passing it anyway sends a
|
|
2209
|
+
// live OpenAI/Gemini secret to Anthropic's endpoint.
|
|
2210
|
+
const inheritableKey = runCtx?.userApiKeyEnvVar === undefined ||
|
|
2211
|
+
runCtx.userApiKeyEnvVar === "ANTHROPIC_API_KEY"
|
|
2212
|
+
? runCtx?.userApiKey
|
|
2213
|
+
: undefined;
|
|
2182
2214
|
return (runCtx?.engine ??
|
|
2183
2215
|
createAnthropicEngine({
|
|
2184
|
-
|
|
2185
|
-
// delegations spawned by agent-teams don't silently fall back
|
|
2186
|
-
// to the platform key while the parent uses BYO credentials.
|
|
2187
|
-
apiKey: runCtx?.userApiKey ?? options?.apiKey,
|
|
2216
|
+
apiKey: inheritableKey ?? options?.apiKey,
|
|
2188
2217
|
}));
|
|
2189
2218
|
},
|
|
2190
2219
|
getModel: () => getRequestRunContext()?.model ?? resolvedModel,
|
|
@@ -2341,13 +2370,17 @@ export function createAgentChatPlugin(options) {
|
|
|
2341
2370
|
// content is what the token-saving modes strip.
|
|
2342
2371
|
const prepareRun = async (event) => {
|
|
2343
2372
|
const owner = await getOwnerFromEvent(event);
|
|
2344
|
-
const {
|
|
2345
|
-
const userApiKey = await
|
|
2373
|
+
const { resolveOwnerEngineApiKey } = await import("../agent/production-agent.js");
|
|
2374
|
+
const userApiKey = await resolveOwnerEngineApiKey({
|
|
2375
|
+
engineOption: options?.engine,
|
|
2376
|
+
ownerEmail: owner,
|
|
2377
|
+
});
|
|
2346
2378
|
const runCtx = ensureRequestRunContext();
|
|
2347
2379
|
if (runCtx) {
|
|
2348
2380
|
runCtx.requestOrigin = getOrigin(event);
|
|
2349
2381
|
runCtx.owner = owner;
|
|
2350
|
-
runCtx.userApiKey = userApiKey;
|
|
2382
|
+
runCtx.userApiKey = userApiKey.apiKey;
|
|
2383
|
+
runCtx.userApiKeyEnvVar = userApiKey.apiKeyEnvVar;
|
|
2351
2384
|
}
|
|
2352
2385
|
const extra = await resolveExtraContext(event, owner);
|
|
2353
2386
|
return { owner, extra };
|
|
@@ -2937,21 +2970,28 @@ Non-code requests are still fine on this surface: read data, navigate the UI, su
|
|
|
2937
2970
|
resolveConfig: async ({ payload, ownerEmail, orgId: _orgId }) => {
|
|
2938
2971
|
// Resolve the owner's API key so BYO-key sub-agents use the
|
|
2939
2972
|
// same credentials as the parent chat.
|
|
2940
|
-
let
|
|
2973
|
+
let resolvedKey = {
|
|
2974
|
+
apiKey: undefined,
|
|
2975
|
+
apiKeyEnvVar: undefined,
|
|
2976
|
+
};
|
|
2941
2977
|
try {
|
|
2942
|
-
const {
|
|
2943
|
-
|
|
2944
|
-
|
|
2978
|
+
const { resolveOwnerEngineApiKey } = await import("../agent/production-agent.js");
|
|
2979
|
+
resolvedKey = await resolveOwnerEngineApiKey({
|
|
2980
|
+
engineOption: options?.engine,
|
|
2981
|
+
ownerEmail,
|
|
2982
|
+
anthropicFallback: options?.apiKey,
|
|
2983
|
+
});
|
|
2945
2984
|
}
|
|
2946
2985
|
catch {
|
|
2947
|
-
|
|
2986
|
+
resolvedKey = { apiKey: undefined, apiKeyEnvVar: undefined };
|
|
2948
2987
|
}
|
|
2949
2988
|
// Use the same resolveEngine path as the A2A and MCP
|
|
2950
2989
|
// processors so Builder-gateway/OpenAI users get their
|
|
2951
2990
|
// configured engine instead of always hitting the Anthropic SDK.
|
|
2952
2991
|
const engine = await resolveEngine({
|
|
2953
2992
|
engineOption: options?.engine,
|
|
2954
|
-
apiKey: apiKey
|
|
2993
|
+
apiKey: resolvedKey.apiKey,
|
|
2994
|
+
apiKeyEnvVar: resolvedKey.apiKeyEnvVar,
|
|
2955
2995
|
appId: options?.appId,
|
|
2956
2996
|
});
|
|
2957
2997
|
const modelCandidate = payload.model ??
|
|
@@ -3147,15 +3187,7 @@ Non-code requests are still fine on this surface: read data, navigate the UI, su
|
|
|
3147
3187
|
setResponseStatus(event, 401);
|
|
3148
3188
|
return { error: "Authentication required" };
|
|
3149
3189
|
}
|
|
3150
|
-
const
|
|
3151
|
-
anthropic: "ANTHROPIC_API_KEY",
|
|
3152
|
-
openai: "OPENAI_API_KEY",
|
|
3153
|
-
google: "GOOGLE_GENERATIVE_AI_API_KEY",
|
|
3154
|
-
groq: "GROQ_API_KEY",
|
|
3155
|
-
mistral: "MISTRAL_API_KEY",
|
|
3156
|
-
cohere: "COHERE_API_KEY",
|
|
3157
|
-
};
|
|
3158
|
-
const secretKey = providerToEnv[provider] ?? `${provider.toUpperCase()}_API_KEY`;
|
|
3190
|
+
const secretKey = PROVIDER_TO_ENV[provider] ?? `${provider.toUpperCase()}_API_KEY`;
|
|
3159
3191
|
try {
|
|
3160
3192
|
const { writeAppSecret } = await import("../secrets/storage.js");
|
|
3161
3193
|
await writeAppSecret({
|
|
@@ -3685,9 +3717,12 @@ Non-code requests are still fine on this surface: read data, navigate the UI, su
|
|
|
3685
3717
|
.trim();
|
|
3686
3718
|
// Mirror the chat-run resolution so BYO-key users have title
|
|
3687
3719
|
// generation billed to their own key instead of the platform key.
|
|
3688
|
-
|
|
3689
|
-
|
|
3690
|
-
|
|
3720
|
+
// This request goes straight to Anthropic, so it needs the owner's
|
|
3721
|
+
// Anthropic key specifically — the active engine may be another
|
|
3722
|
+
// provider, whose key must never be sent here. Owners without one
|
|
3723
|
+
// get the truncated title.
|
|
3724
|
+
const { getOwnerApiKeyForEngine } = await import("../agent/production-agent.js");
|
|
3725
|
+
const { apiKey } = await getOwnerApiKeyForEngine("anthropic", ownerEmail);
|
|
3691
3726
|
if (!apiKey) {
|
|
3692
3727
|
// Fallback: truncate the message
|
|
3693
3728
|
return { title: cleanMessage.trim().slice(0, 60) };
|
|
@@ -4735,6 +4770,17 @@ Non-code requests are still fine on this surface: read data, navigate the UI, su
|
|
|
4735
4770
|
}));
|
|
4736
4771
|
const isBackgroundRuntime = isInBackgroundFunctionRuntime();
|
|
4737
4772
|
const disableRecurringJobsRuntime = isBackgroundRuntime || shouldDisableRecurringJobsRuntime();
|
|
4773
|
+
// Opt-in kill switch for the backstop sweep timers below. On serverless
|
|
4774
|
+
// these are billed per warm container, so their query rate scales with
|
|
4775
|
+
// instance count rather than with anything happening. See
|
|
4776
|
+
// `shouldDisableInProcessSweeps`.
|
|
4777
|
+
const sweepsDisabled = shouldDisableInProcessSweeps();
|
|
4778
|
+
if (sweepsDisabled) {
|
|
4779
|
+
console.log("[agent-native] In-process backstop sweeps disabled " +
|
|
4780
|
+
"(AGENT_NATIVE_DISABLE_INPROCESS_SWEEPS). A durable scheduler must " +
|
|
4781
|
+
"drive automation redispatch, agent-teams reconciliation, and " +
|
|
4782
|
+
"sandbox-execution recovery, or queued work will not be retried.");
|
|
4783
|
+
}
|
|
4738
4784
|
// ─── Recurring Jobs Scheduler ──────────────────────────────────────
|
|
4739
4785
|
// Poll every 60 seconds for due recurring jobs and execute them.
|
|
4740
4786
|
// Uses setInterval so it works in all deployment environments without
|
|
@@ -4832,7 +4878,7 @@ Non-code requests are still fine on this surface: read data, navigate the UI, su
|
|
|
4832
4878
|
// A non-Netlify self-dispatch only waits for the request to leave the
|
|
4833
4879
|
// current invocation. Keep the durable history row as a retryable queue
|
|
4834
4880
|
// so a frozen serverless handoff is recovered on the next sweep.
|
|
4835
|
-
if (!isBackgroundRuntime) {
|
|
4881
|
+
if (!isBackgroundRuntime && !sweepsDisabled) {
|
|
4836
4882
|
(() => {
|
|
4837
4883
|
let inFlight = false;
|
|
4838
4884
|
const sweep = async () => {
|
|
@@ -4864,7 +4910,7 @@ Non-code requests are still fine on this surface: read data, navigate the UI, su
|
|
|
4864
4910
|
// per instance; cheap (one indexed query when no active tasks are found).
|
|
4865
4911
|
// Throttled by the same per-owner interval guard inside reconcileAgentTeamRunsForOwner.
|
|
4866
4912
|
(() => {
|
|
4867
|
-
if (isBackgroundRuntime)
|
|
4913
|
+
if (isBackgroundRuntime || sweepsDisabled)
|
|
4868
4914
|
return;
|
|
4869
4915
|
// Track when this instance last ran the sweep so only one sweep fires
|
|
4870
4916
|
// per 2-min window even if multiple timers fire in overlapping invocations.
|
|
@@ -5013,7 +5059,7 @@ Non-code requests are still fine on this surface: read data, navigate the UI, su
|
|
|
5013
5059
|
// comment above for why this exists and the timing budget in
|
|
5014
5060
|
// run-store.ts's `UNCLAIMED_BACKGROUND_RUN_FAST_SWEEP_MS` doc comment.
|
|
5015
5061
|
(() => {
|
|
5016
|
-
if (isBackgroundRuntime)
|
|
5062
|
+
if (isBackgroundRuntime || sweepsDisabled)
|
|
5017
5063
|
return;
|
|
5018
5064
|
setTimeout(() => {
|
|
5019
5065
|
(async () => {
|
|
@@ -5056,7 +5102,7 @@ Non-code requests are still fine on this surface: read data, navigate the UI, su
|
|
|
5056
5102
|
// still fails loud, and it survives the fast sweep having missed a row
|
|
5057
5103
|
// entirely (e.g. a restart landed between fast-sweep ticks).
|
|
5058
5104
|
(() => {
|
|
5059
|
-
if (isBackgroundRuntime)
|
|
5105
|
+
if (isBackgroundRuntime || sweepsDisabled)
|
|
5060
5106
|
return;
|
|
5061
5107
|
let lastSweep = 0;
|
|
5062
5108
|
const SWEEP_INTERVAL_MS = 2 * 60 * 1000;
|
|
@@ -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 {};
|
package/dist/server/auth.d.ts
CHANGED
|
@@ -226,6 +226,9 @@ export declare function removeSession(token: string): Promise<void>;
|
|
|
226
226
|
/**
|
|
227
227
|
* Look up the email associated with a legacy session token.
|
|
228
228
|
* Returns null if the session doesn't exist, is expired, or has no email.
|
|
229
|
+
*
|
|
230
|
+
* Resolutions are cached across requests — see `session-email-cache.ts` for the
|
|
231
|
+
* TTL and the only-cache-successes rule.
|
|
229
232
|
*/
|
|
230
233
|
export declare function getSessionEmail(token: string): Promise<string | null>;
|
|
231
234
|
export interface DesktopExchangeErrorPayload {
|
package/dist/server/auth.js
CHANGED
|
@@ -71,6 +71,7 @@ import { isIdentitySsoEnabled } from "./identity-sso-store.js";
|
|
|
71
71
|
import { safeOAuthReturnUrl } from "./oauth-return-url.js";
|
|
72
72
|
import { getOnboardingHtml, getResetPasswordHtml, } from "./onboarding-html.js";
|
|
73
73
|
import { captureAuthError } from "./sentry.js";
|
|
74
|
+
import { forgetCachedSessionEmail, getCachedSessionEmail, invalidateSessionEmailCache, setCachedSessionEmail, } from "./session-email-cache.js";
|
|
74
75
|
import { isWorkspaceOAuthCallbackRelayEnabled } from "./workspace-oauth.js";
|
|
75
76
|
/**
|
|
76
77
|
* Get the configured session max age. Desktop SSO broker writes from
|
|
@@ -741,6 +742,9 @@ export async function addSession(token, email) {
|
|
|
741
742
|
: `INSERT OR REPLACE INTO sessions (token, email, created_at) VALUES (?, ?, ?)`,
|
|
742
743
|
args: [token, email ?? null, Date.now()],
|
|
743
744
|
}));
|
|
745
|
+
// The upsert can REBIND an existing token to a different email, so a cached
|
|
746
|
+
// resolution for it is now wrong.
|
|
747
|
+
invalidateSessionEmailCache();
|
|
744
748
|
}
|
|
745
749
|
export async function hasLegacySessionForEmail(email) {
|
|
746
750
|
await ensureSessionTable();
|
|
@@ -759,12 +763,20 @@ export async function removeSession(token) {
|
|
|
759
763
|
sql: `DELETE FROM sessions WHERE token = ?`,
|
|
760
764
|
args: [token],
|
|
761
765
|
}));
|
|
766
|
+
// Sign-out must take effect immediately, not after the cache TTL.
|
|
767
|
+
invalidateSessionEmailCache();
|
|
762
768
|
}
|
|
763
769
|
/**
|
|
764
770
|
* Look up the email associated with a legacy session token.
|
|
765
771
|
* Returns null if the session doesn't exist, is expired, or has no email.
|
|
772
|
+
*
|
|
773
|
+
* Resolutions are cached across requests — see `session-email-cache.ts` for the
|
|
774
|
+
* TTL and the only-cache-successes rule.
|
|
766
775
|
*/
|
|
767
776
|
export async function getSessionEmail(token) {
|
|
777
|
+
const cached = getCachedSessionEmail(token);
|
|
778
|
+
if (cached !== undefined)
|
|
779
|
+
return cached;
|
|
768
780
|
await ensureSessionTable();
|
|
769
781
|
const client = getDbExec();
|
|
770
782
|
const { rows } = await retryIfSessionsMissing(() => client.execute({
|
|
@@ -779,9 +791,13 @@ export async function getSessionEmail(token) {
|
|
|
779
791
|
sql: `DELETE FROM sessions WHERE token = ?`,
|
|
780
792
|
args: [token],
|
|
781
793
|
});
|
|
794
|
+
forgetCachedSessionEmail(token);
|
|
782
795
|
return null;
|
|
783
796
|
}
|
|
784
|
-
|
|
797
|
+
const email = rows[0].email ?? null;
|
|
798
|
+
if (email)
|
|
799
|
+
setCachedSessionEmail(token, email);
|
|
800
|
+
return email;
|
|
785
801
|
}
|
|
786
802
|
// ---------------------------------------------------------------------------
|
|
787
803
|
// getSession — the auth contract
|
|
@@ -937,6 +953,7 @@ async function consumeDesktopExchangeFromDB(flowId) {
|
|
|
937
953
|
sql: `DELETE FROM sessions WHERE token = ? AND created_at > ? RETURNING email`,
|
|
938
954
|
args: [`dex:${flowId}`, Date.now() - DESKTOP_EXCHANGE_TTL_MS],
|
|
939
955
|
});
|
|
956
|
+
forgetCachedSessionEmail(`dex:${flowId}`);
|
|
940
957
|
if (rows.length === 0)
|
|
941
958
|
return null;
|
|
942
959
|
const packed = (rows[0].email ?? rows[0][0]);
|
|
@@ -2706,6 +2723,9 @@ async function mountBetterAuthRoutes(app, options) {
|
|
|
2706
2723
|
args: [userEmail],
|
|
2707
2724
|
});
|
|
2708
2725
|
}
|
|
2726
|
+
// Deleted by email, so the removed tokens are unknown here — the
|
|
2727
|
+
// whole cache goes.
|
|
2728
|
+
invalidateSessionEmailCache();
|
|
2709
2729
|
}
|
|
2710
2730
|
}
|
|
2711
2731
|
catch {
|
|
@@ -2939,6 +2959,7 @@ async function mountBetterAuthRoutes(app, options) {
|
|
|
2939
2959
|
catch {
|
|
2940
2960
|
// Best-effort.
|
|
2941
2961
|
}
|
|
2962
|
+
invalidateSessionEmailCache();
|
|
2942
2963
|
// 3. Drop the current request's cookie and best-effort sign out
|
|
2943
2964
|
// of Better Auth (so the response sets the proper expiry header).
|
|
2944
2965
|
clearFrameworkSessionCookies(event);
|
|
@@ -211,8 +211,12 @@ export function createCollabPlugin(options = {}) {
|
|
|
211
211
|
(action === "json" && method === "POST") ||
|
|
212
212
|
(action === "patch" && method === "POST");
|
|
213
213
|
if (isWrite) {
|
|
214
|
-
// assertAccess throws ForbiddenError (→ 403) if no editor access
|
|
215
|
-
|
|
214
|
+
// assertAccess throws ForbiddenError (→ 403) if no editor access.
|
|
215
|
+
// Projected: only ownerEmail/orgId are read below, and a collab
|
|
216
|
+
// resource's body is the largest column it has — loading it here
|
|
217
|
+
// means every keystroke-driven update read the whole document
|
|
218
|
+
// twice, once for the ACL and once for the edit itself.
|
|
219
|
+
const access = await assertAccess(resourceType, resourceId, "editor", undefined, { skipResourceBody: true });
|
|
216
220
|
const resource = access.resource;
|
|
217
221
|
const awarenessScope = {
|
|
218
222
|
resourceType,
|
|
@@ -229,8 +233,10 @@ export function createCollabPlugin(options = {}) {
|
|
|
229
233
|
}
|
|
230
234
|
}
|
|
231
235
|
else {
|
|
232
|
-
// resolveAccess returns null when no access; return 404 to avoid leaking existence
|
|
233
|
-
|
|
236
|
+
// resolveAccess returns null when no access; return 404 to avoid leaking existence.
|
|
237
|
+
// Projected: only ownerEmail/orgId are read below, and a collab
|
|
238
|
+
// resource's body is the largest column it has.
|
|
239
|
+
const access = await resolveAccess(resourceType, resourceId, undefined, { skipResourceBody: true });
|
|
234
240
|
if (!access) {
|
|
235
241
|
setResponseStatus(event, 404);
|
|
236
242
|
return { error: "Not found" };
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { getStoredModelForEngine, normalizeModelForEngine, registerBuiltinEngines, resolveEngine, } from "../agent/engine/index.js";
|
|
2
2
|
import { EngineError } from "../agent/engine/types.js";
|
|
3
|
-
import {
|
|
3
|
+
import { resolveOwnerEngineApiKey, } from "../agent/production-agent.js";
|
|
4
4
|
import { getRequestUserEmail } from "./request-context.js";
|
|
5
5
|
function normalizeCompleteTextMessages(messages, input) {
|
|
6
6
|
const normalized = [];
|
|
@@ -56,14 +56,20 @@ function createCompletionAbortSignal(signal, timeoutMs) {
|
|
|
56
56
|
},
|
|
57
57
|
};
|
|
58
58
|
}
|
|
59
|
-
async function resolveCompletionApiKey(
|
|
60
|
-
|
|
61
|
-
|
|
59
|
+
async function resolveCompletionApiKey(options) {
|
|
60
|
+
// A caller-supplied key is opaque: `CompleteTextOptions.apiKey` names no
|
|
61
|
+
// provider, so it keeps the caller's contract and is passed through untagged.
|
|
62
|
+
if (options.apiKey) {
|
|
63
|
+
return { apiKey: options.apiKey, apiKeyEnvVar: undefined };
|
|
64
|
+
}
|
|
62
65
|
try {
|
|
63
|
-
return await
|
|
66
|
+
return await resolveOwnerEngineApiKey({
|
|
67
|
+
engineOption: options.engine,
|
|
68
|
+
ownerEmail: getRequestUserEmail(),
|
|
69
|
+
});
|
|
64
70
|
}
|
|
65
71
|
catch {
|
|
66
|
-
return undefined;
|
|
72
|
+
return { apiKey: undefined, apiKeyEnvVar: undefined };
|
|
67
73
|
}
|
|
68
74
|
}
|
|
69
75
|
/**
|
|
@@ -75,10 +81,11 @@ async function resolveCompletionApiKey(explicitApiKey) {
|
|
|
75
81
|
*/
|
|
76
82
|
export async function completeText(options) {
|
|
77
83
|
registerBuiltinEngines();
|
|
78
|
-
const apiKey = await resolveCompletionApiKey(options
|
|
84
|
+
const { apiKey, apiKeyEnvVar } = await resolveCompletionApiKey(options);
|
|
79
85
|
const engine = await resolveEngine({
|
|
80
86
|
engineOption: options.engine,
|
|
81
87
|
apiKey,
|
|
88
|
+
apiKeyEnvVar,
|
|
82
89
|
model: options.model,
|
|
83
90
|
appId: options.appId,
|
|
84
91
|
});
|