@agent-native/core 0.227.0 → 0.228.0-nightly-20261111004142
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/dist/action.d.ts +6 -2
- package/dist/agent/engine/builder-engine.js +6 -1
- package/dist/agent/engine/tool-call-journal-seed.d.ts +2 -0
- package/dist/agent/engine/tool-call-journal-seed.js +14 -0
- package/dist/agent/harness/store.js +14 -0
- package/dist/agent/production-agent.js +257 -17
- package/dist/agent/reaped-run-thread.js +6 -0
- package/dist/agent/run-manager.js +6 -0
- package/dist/agent/tool-call-journal.d.ts +4 -0
- package/dist/agent/tool-call-journal.js +12 -0
- package/dist/agent/types.d.ts +7 -1
- package/dist/app-config/agent.d.ts +3 -0
- package/dist/app-config/agent.js +7 -0
- package/dist/app-config/schema.d.ts +3 -0
- package/dist/application-state/handlers.d.ts +3 -3
- package/dist/application-state/handlers.js +6 -1
- package/dist/application-state/index.d.ts +3 -2
- package/dist/application-state/index.js +3 -2
- package/dist/application-state/last-used-tab.d.ts +11 -0
- package/dist/application-state/last-used-tab.js +43 -0
- package/dist/application-state/script-helpers.d.ts +26 -0
- package/dist/application-state/script-helpers.js +83 -15
- package/dist/application-state/store.d.ts +8 -0
- package/dist/application-state/store.js +16 -0
- package/dist/cli/agent.js +7 -3
- package/dist/cli/code-agent-executor.d.ts +1 -0
- package/dist/cli/code-agent-executor.js +37 -2
- package/dist/cli/code.js +3 -1
- package/dist/client/agent-sidebar-state.d.ts +2 -0
- package/dist/client/browser-tab-use.d.ts +9 -0
- package/dist/client/browser-tab-use.js +112 -0
- package/dist/client/chat/runtime.js +3 -1
- package/dist/client/error-capture.js +33 -1
- package/dist/client/navigation/index.d.ts +1 -0
- package/dist/client/navigation/index.js +1 -0
- package/dist/client/resources/mcp-integration-logos.js +16 -0
- package/dist/client/review/use-review.d.ts +28 -0
- package/dist/client/review/use-review.js +41 -12
- package/dist/client/route-state.js +2 -0
- package/dist/client/sse-event-processor.d.ts +1 -0
- package/dist/client/sse-event-processor.js +7 -1
- package/dist/client/use-db-sync.d.ts +8 -1
- package/dist/client/use-db-sync.js +6 -5
- package/dist/coding-tools/run-code.js +12 -2
- package/dist/coding-tools/sandbox/executions-store.d.ts +1 -1
- package/dist/coding-tools/sandbox/executions-store.js +9 -3
- package/dist/collab/routes.d.ts +1 -1
- package/dist/data-programs/execute.js +1 -1
- package/dist/db/schema-snapshot.generated.js +17 -3
- package/dist/extensions/url-safety.d.ts +1 -3
- package/dist/extensions/url-safety.js +23 -23
- package/dist/file-upload/actions/upload-image.d.ts +5 -5
- package/dist/file-upload/index.d.ts +1 -1
- package/dist/file-upload/index.js +1 -1
- package/dist/file-upload/s3-client.d.ts +1 -0
- package/dist/file-upload/s3-client.js +70 -13
- package/dist/file-upload/s3.d.ts +9 -0
- package/dist/file-upload/s3.js +14 -1
- package/dist/framework-package-versions.json +8 -8
- package/dist/jobs/run-history.js +10 -3
- package/dist/jobs/scheduler.js +5 -36
- package/dist/localization/core-messages/supplemental/ar-SA.js +35 -0
- package/dist/localization/core-messages/supplemental/de-DE.js +35 -0
- package/dist/localization/core-messages/supplemental/en-US.js +35 -0
- package/dist/localization/core-messages/supplemental/es-ES.js +35 -0
- package/dist/localization/core-messages/supplemental/fr-FR.js +35 -0
- package/dist/localization/core-messages/supplemental/hi-IN.js +35 -0
- package/dist/localization/core-messages/supplemental/ja-JP.js +35 -0
- package/dist/localization/core-messages/supplemental/ko-KR.js +35 -0
- package/dist/localization/core-messages/supplemental/pt-BR.js +35 -0
- package/dist/localization/core-messages/supplemental/zh-CN.js +35 -0
- package/dist/localization/core-messages/supplemental/zh-TW.js +35 -0
- package/dist/localization/default-messages.d.ts +35 -0
- package/dist/localization/default-messages.js +35 -0
- package/dist/localization/mcp-settings-messages.d.ts +35 -0
- package/dist/mcp/actions/list-mcp-client-connections.d.ts +8 -0
- package/dist/mcp/actions/list-mcp-client-connections.js +39 -0
- package/dist/mcp/browser-tab-target.d.ts +16 -0
- package/dist/mcp/browser-tab-target.js +58 -0
- package/dist/mcp/build-server.d.ts +1 -0
- package/dist/mcp/build-server.js +38 -7
- package/dist/mcp/call-attribution.d.ts +4 -1
- package/dist/mcp/call-attribution.js +3 -1
- package/dist/mcp/client-connections.d.ts +6 -0
- package/dist/mcp/client-connections.js +91 -0
- package/dist/mcp/connect-route.js +12 -2
- package/dist/mcp/connect-store.d.ts +3 -0
- package/dist/mcp/connect-store.js +10 -1
- package/dist/mcp/direct-action-call.js +1 -0
- package/dist/mcp/oauth-store.d.ts +6 -0
- package/dist/mcp/oauth-store.js +23 -0
- package/dist/mcp/oauth-token.d.ts +1 -0
- package/dist/mcp/oauth-token.js +1 -0
- package/dist/mcp/server.js +9 -20
- package/dist/mcp-client/config.d.ts +2 -0
- package/dist/mcp-client/manager.js +23 -0
- package/dist/mcp-client/workspace-servers.js +42 -10
- package/dist/notifications/routes.d.ts +3 -3
- package/dist/org/handlers.d.ts +1 -1
- package/dist/org/handlers.js +21 -17
- package/dist/provider-api/actions/custom-provider-registration.d.ts +12 -12
- package/dist/provider-api/actions/provider-api.d.ts +8 -8
- package/dist/provider-api/corpus-jobs.d.ts +2 -2
- package/dist/provider-api/index.js +17 -36
- package/dist/review/actions/create-review-comment.d.ts +7 -0
- package/dist/review/actions/create-review-comment.js +29 -3
- package/dist/review/actions/delete-review-comment.js +2 -2
- package/dist/review/actions/list-review-comments.d.ts +3 -0
- package/dist/review/actions/list-review-comments.js +41 -17
- package/dist/review/actions/react-to-review-comment.js +2 -2
- package/dist/review/actions/reply-review-comment.d.ts +4 -0
- package/dist/review/actions/reply-review-comment.js +15 -6
- package/dist/review/actions/update-review-comment-anchor.d.ts +6 -0
- package/dist/review/actions/update-review-comment-anchor.js +39 -10
- package/dist/review/actions/update-review-comment.d.ts +3 -0
- package/dist/review/actions/update-review-comment.js +28 -3
- package/dist/review/errors.d.ts +33 -0
- package/dist/review/errors.js +51 -0
- package/dist/review/index.d.ts +5 -4
- package/dist/review/index.js +3 -2
- package/dist/review/registry.d.ts +51 -1
- package/dist/review/registry.js +117 -0
- package/dist/review/store.d.ts +59 -14
- package/dist/review/store.js +622 -101
- package/dist/review/suggestions/actions.js +65 -10
- package/dist/review/suggestions/actor-kind.d.ts +5 -0
- package/dist/review/suggestions/actor-kind.js +20 -0
- package/dist/review/suggestions/store.js +16 -2
- package/dist/review/suggestions/types.d.ts +13 -0
- package/dist/review/types.d.ts +89 -0
- package/dist/scripts/db/dev-query-proxy.d.ts +2 -0
- package/dist/scripts/db/dev-query-proxy.js +4 -1
- package/dist/scripts/db/query.js +3 -0
- package/dist/scripts/dev/shell.js +4 -0
- package/dist/scripts/dev-session.d.ts +31 -0
- package/dist/scripts/dev-session.js +58 -0
- package/dist/scripts/resources/list.js +53 -4
- package/dist/scripts/runner.js +22 -4
- package/dist/search/query.d.ts +11 -5
- package/dist/search/query.js +52 -15
- package/dist/search/tokenize.d.ts +1 -0
- package/dist/search/tokenize.js +3 -0
- package/dist/secrets/api-keys.d.ts +4 -0
- package/dist/secrets/api-keys.js +5 -0
- package/dist/secrets/register.d.ts +2 -0
- package/dist/secrets/routes.d.ts +2 -2
- package/dist/secrets/routes.js +85 -6
- package/dist/secrets/storage.d.ts +2 -0
- package/dist/secrets/storage.js +34 -9
- package/dist/secrets/substitution.d.ts +3 -2
- package/dist/secrets/substitution.js +56 -7
- package/dist/server/action-discovery.js +5 -0
- package/dist/server/action-routes.js +8 -6
- package/dist/server/agent-chat/prompt-resources.js +117 -79
- package/dist/server/agent-chat/script-entries.js +15 -8
- package/dist/server/agent-chat-plugin.js +9 -4
- package/dist/server/agent-engine-api-key-route.d.ts +1 -1
- package/dist/server/agent-engine-ollama-models-route.d.ts +1 -1
- package/dist/server/agents-bundle.d.ts +3 -1
- package/dist/server/agents-bundle.js +4 -4
- package/dist/server/csrf.js +1 -0
- package/dist/server/dev-action-bridge.d.ts +2 -0
- package/dist/server/dev-action-bridge.js +34 -5
- package/dist/server/first-touch-route.d.ts +1 -1
- package/dist/server/http-response-telemetry.js +4 -0
- package/dist/server/loopback.js +3 -1
- package/dist/server/request-context.d.ts +26 -0
- package/dist/shared/app-state-routes.d.ts +7 -0
- package/dist/shared/app-state-routes.js +7 -0
- package/dist/shared/mcp-client-connections.d.ts +41 -0
- package/dist/shared/mcp-client-connections.js +18 -0
- package/dist/shared/mcp-connect-content.d.ts +18 -0
- package/dist/shared/mcp-connect-content.js +200 -172
- package/dist/shared/runtime-config.js +5 -1
- package/dist/templates/chat/.agents/skills/context-awareness/SKILL.md +13 -0
- package/dist/templates/default/.agents/skills/context-awareness/SKILL.md +13 -0
- package/dist/templates/workspace-core/.agents/skills/context-awareness/SKILL.md +13 -0
- package/dist/tracking/providers.d.ts +7 -0
- package/dist/tracking/providers.js +39 -17
- package/dist/triggers/condition-evaluator.d.ts +2 -0
- package/dist/triggers/condition-evaluator.js +93 -3
- package/dist/triggers/dispatcher.js +24 -6
- package/docs/AGENTS.md +16 -16
- package/docs/content/create-with-an-agent.mdx +10 -4
- package/docs/content/durable-resume.mdx +11 -2
- package/docs/content/toolkit-comments-review.mdx +54 -1
- package/package.json +5 -4
- package/src/templates/chat/.agents/skills/context-awareness/SKILL.md +13 -0
- package/src/templates/default/.agents/skills/context-awareness/SKILL.md +13 -0
- package/src/templates/workspace-core/.agents/skills/context-awareness/SKILL.md +13 -0
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { resourceList, resourceListAccessible, ensurePersonalDefaults, isWorkspaceResourceOwner, SHARED_OWNER, WORKSPACE_OWNER, sharedResourceOwner, } from "../../resources/store.js";
|
|
2
2
|
import { getAmbientUserEmail, getRequestOrgId, getRequestUserEmail, } from "../../server/request-context.js";
|
|
3
3
|
import { parseArgs, fail } from "../utils.js";
|
|
4
|
+
const DEFAULT_PAGE_SIZE = 50;
|
|
5
|
+
const MAX_PAGE_SIZE = 100;
|
|
4
6
|
export default async function resourceListScript(args) {
|
|
5
7
|
const parsed = parseArgs(args);
|
|
6
8
|
if (parsed.help === "true") {
|
|
@@ -12,9 +14,23 @@ Options:
|
|
|
12
14
|
Scope to list (default: all)
|
|
13
15
|
--format json|text Output format (default: text)
|
|
14
16
|
--include-agent-scratch true Include hidden agent scratch files
|
|
17
|
+
--limit <n> Resources per page, 1-${MAX_PAGE_SIZE} (default: ${DEFAULT_PAGE_SIZE}).
|
|
18
|
+
Text output always pages; json pages only
|
|
19
|
+
when --limit or --offset is set
|
|
20
|
+
--offset <n> Resources to skip; pass the previous page's
|
|
21
|
+
next offset
|
|
15
22
|
--help Show this help message`);
|
|
16
23
|
return;
|
|
17
24
|
}
|
|
25
|
+
const limitArg = parsed.limit;
|
|
26
|
+
const offsetArg = parsed.offset;
|
|
27
|
+
if (limitArg !== undefined &&
|
|
28
|
+
!(/^\d+$/.test(limitArg) && +limitArg >= 1 && +limitArg <= MAX_PAGE_SIZE)) {
|
|
29
|
+
fail(`--limit must be a whole number from 1 to ${MAX_PAGE_SIZE}`);
|
|
30
|
+
}
|
|
31
|
+
if (offsetArg !== undefined && !/^\d+$/.test(offsetArg)) {
|
|
32
|
+
fail("--offset must be a whole number, 0 or more");
|
|
33
|
+
}
|
|
18
34
|
const prefix = parsed.prefix;
|
|
19
35
|
const scope = parsed.scope ?? "all";
|
|
20
36
|
const format = parsed.format ?? "text";
|
|
@@ -72,16 +88,44 @@ Options:
|
|
|
72
88
|
})
|
|
73
89
|
: await resourceListAccessible(owner, prefix, { orgId });
|
|
74
90
|
}
|
|
91
|
+
// Agents read this through tool-result caps (12,000 chars when replayed), so
|
|
92
|
+
// text always pages and json pages when asked. Sorting the merged list keeps
|
|
93
|
+
// pages stable across calls.
|
|
94
|
+
resources.sort((a, b) => (a.path < b.path ? -1 : a.path > b.path ? 1 : 0));
|
|
95
|
+
const paged = format !== "json" || limitArg !== undefined || offsetArg !== undefined;
|
|
96
|
+
const total = resources.length;
|
|
97
|
+
const offset = Number(offsetArg ?? 0);
|
|
98
|
+
const limit = Number(limitArg ?? DEFAULT_PAGE_SIZE);
|
|
99
|
+
const page = paged ? resources.slice(offset, offset + limit) : resources;
|
|
100
|
+
const nextOffset = offset + page.length < total ? offset + page.length : null;
|
|
75
101
|
if (format === "json") {
|
|
76
|
-
console.log(JSON.stringify(
|
|
102
|
+
console.log(JSON.stringify(paged
|
|
103
|
+
? {
|
|
104
|
+
total,
|
|
105
|
+
offset,
|
|
106
|
+
limit,
|
|
107
|
+
hasMore: nextOffset !== null,
|
|
108
|
+
nextOffset,
|
|
109
|
+
resources: page,
|
|
110
|
+
}
|
|
111
|
+
: resources, null, 2));
|
|
77
112
|
return;
|
|
78
113
|
}
|
|
79
|
-
if (
|
|
114
|
+
if (total === 0) {
|
|
80
115
|
console.log("No resources found.");
|
|
81
116
|
return;
|
|
82
117
|
}
|
|
83
|
-
|
|
84
|
-
|
|
118
|
+
if (page.length === 0) {
|
|
119
|
+
console.log(`No resources at offset ${offset} (${total} total).`);
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
// The next offset leads the header too: truncation cuts from the end.
|
|
123
|
+
const whole = page.length === total;
|
|
124
|
+
const range = whole
|
|
125
|
+
? ""
|
|
126
|
+
: ` (showing ${offset + 1}-${offset + page.length}${nextOffset === null ? "" : `; next offset=${nextOffset}`})`;
|
|
127
|
+
console.log(`Resources: ${total}${range}\n`);
|
|
128
|
+
for (const r of page) {
|
|
85
129
|
const ownerLabel = isWorkspaceResourceOwner(r.owner)
|
|
86
130
|
? "[workspace]"
|
|
87
131
|
: r.owner === SHARED_OWNER
|
|
@@ -90,4 +134,9 @@ Options:
|
|
|
90
134
|
const sizeLabel = r.size != null ? ` (${r.size} bytes)` : "";
|
|
91
135
|
console.log(` ${r.path} ${ownerLabel}${sizeLabel} ${r.mimeType}`);
|
|
92
136
|
}
|
|
137
|
+
if (whole)
|
|
138
|
+
return;
|
|
139
|
+
console.log(nextOffset === null
|
|
140
|
+
? "\nEnd of list."
|
|
141
|
+
: `\n${total - nextOffset} more not shown. Next page: repeat this call with offset=${nextOffset}.`);
|
|
93
142
|
}
|
package/dist/scripts/runner.js
CHANGED
|
@@ -11,11 +11,11 @@ import { requestCliApproval, runApprovedCliAction, takeCliApprovalFlag, } from "
|
|
|
11
11
|
import { getUrlOpenerCommand } from "../cli/open-url.js";
|
|
12
12
|
import { closeDbExec, getRuntimeDatabaseUrl, isProcessAlive, } from "../db/client.js";
|
|
13
13
|
import { notifyActionChange } from "../server/action-change.js";
|
|
14
|
-
import { DEV_ACTION_ORG_HEADER, DEV_ACTION_ROUTE, DEV_ACTION_TOKEN_HEADER, DEV_ACTION_USER_HEADER, devActionHandoffUrl, hashDatabaseKey, isLoopbackDevActionOrigin, isValidDevActionHandoffUrl, readDevActionDiscoveryFile, } from "../server/dev-action-bridge.js";
|
|
14
|
+
import { DEV_ACTION_ORG_HEADER, DEV_ACTION_ORG_SCOPE_HEADER, DEV_ACTION_ROUTE, DEV_ACTION_TOKEN_HEADER, DEV_ACTION_USER_HEADER, devActionHandoffUrl, hashDatabaseKey, isLoopbackDevActionOrigin, isValidDevActionHandoffUrl, readDevActionDiscoveryFile, } from "../server/dev-action-bridge.js";
|
|
15
15
|
import { runWithRequestContext, getRequestOrgId, getRequestUserEmail, } from "../server/request-context.js";
|
|
16
16
|
import { loadCliBootstrap } from "./cli-bootstrap.js";
|
|
17
17
|
import { coreScripts, getCoreScriptNames } from "./core-scripts.js";
|
|
18
|
-
import { resolveDevUserEmail } from "./dev-session.js";
|
|
18
|
+
import { devOrgChoiceFromEnv, resolveDevOrgScope, resolveDevUserEmail, } from "./dev-session.js";
|
|
19
19
|
import { loadEnv } from "./utils.js";
|
|
20
20
|
loadEnv();
|
|
21
21
|
const CLI_HANDOFF_KEYS = new Set(["embedStartUrl", "startUrl"]);
|
|
@@ -192,8 +192,24 @@ export async function runScript(options = {}) {
|
|
|
192
192
|
await tryForwardToDevServer(actionName, args);
|
|
193
193
|
await loadCliBootstrap();
|
|
194
194
|
const userEmail = await resolveDevUserEmail();
|
|
195
|
-
|
|
196
|
-
|
|
195
|
+
let orgScope;
|
|
196
|
+
try {
|
|
197
|
+
orgScope = await resolveDevOrgScope(userEmail, devOrgChoiceFromEnv());
|
|
198
|
+
}
|
|
199
|
+
catch (err) {
|
|
200
|
+
// The lookup can open the database, so a failure closes it and reports
|
|
201
|
+
// like a failed action rather than leaving an unhandled rejection.
|
|
202
|
+
await closeDbExec().catch(() => { });
|
|
203
|
+
console.error(`Action "${actionName}" failed:`, err instanceof Error ? err.message : String(err));
|
|
204
|
+
process.exit(1);
|
|
205
|
+
}
|
|
206
|
+
return runWithRequestContext({ ...(userEmail ? { userEmail } : {}), ...orgScope }, () => dispatchAction(actionName, args, options));
|
|
207
|
+
}
|
|
208
|
+
/** Tells the dev server this call acts in no org, so it looks none up. */
|
|
209
|
+
function devOrgScopeHeader() {
|
|
210
|
+
return devOrgChoiceFromEnv().personal
|
|
211
|
+
? { [DEV_ACTION_ORG_SCOPE_HEADER]: "personal" }
|
|
212
|
+
: {};
|
|
197
213
|
}
|
|
198
214
|
export async function tryForwardToDevServer(actionName, args) {
|
|
199
215
|
const discovery = readDevActionDiscoveryFile(process.cwd());
|
|
@@ -233,6 +249,7 @@ export async function tryForwardToDevServer(actionName, args) {
|
|
|
233
249
|
...(process.env.AGENT_ORG_ID
|
|
234
250
|
? { [DEV_ACTION_ORG_HEADER]: process.env.AGENT_ORG_ID }
|
|
235
251
|
: {}),
|
|
252
|
+
...devOrgScopeHeader(),
|
|
236
253
|
},
|
|
237
254
|
body: JSON.stringify({
|
|
238
255
|
name: actionName,
|
|
@@ -263,6 +280,7 @@ export async function tryForwardToDevServer(actionName, args) {
|
|
|
263
280
|
...(process.env.AGENT_ORG_ID
|
|
264
281
|
? { [DEV_ACTION_ORG_HEADER]: process.env.AGENT_ORG_ID }
|
|
265
282
|
: {}),
|
|
283
|
+
...devOrgScopeHeader(),
|
|
266
284
|
},
|
|
267
285
|
body: JSON.stringify({
|
|
268
286
|
name: actionName,
|
package/dist/search/query.d.ts
CHANGED
|
@@ -6,11 +6,17 @@
|
|
|
6
6
|
* Matching, for each term:
|
|
7
7
|
* - titles and summaries match anywhere, including mid-word ("prio" finds
|
|
8
8
|
* "Task Priorities"), because they're short;
|
|
9
|
-
* - bodies match whole words, every word as a prefix
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
9
|
+
* - bodies match whole words, every word as a prefix; mid-word body matches
|
|
10
|
+
* are deliberately not supported. A phrase needs its words adjacent, except
|
|
11
|
+
* in a document too long or repetitive for Postgres to keep every word
|
|
12
|
+
* position, where every word being in one field is enough.
|
|
13
|
+
*
|
|
14
|
+
* The index is read one row per document the app's query allows, not through
|
|
15
|
+
* its GIN index across the table. The table holds every user's documents, so a
|
|
16
|
+
* common word would otherwise read most of them, and Postgres will: it doesn't
|
|
17
|
+
* count what fetching a stored vector costs, so it picks that scan however many
|
|
18
|
+
* words the query has. Reading only the caller's rows keeps the cost to the
|
|
19
|
+
* size of their library, and to that of whatever else narrows the query.
|
|
14
20
|
*
|
|
15
21
|
* Ranking reproduces the tiers the browser lane uses
|
|
16
22
|
* (`TITLE_MATCH_TIER` in Content): exact title 5, title prefix 4, title word
|
package/dist/search/query.js
CHANGED
|
@@ -6,11 +6,17 @@
|
|
|
6
6
|
* Matching, for each term:
|
|
7
7
|
* - titles and summaries match anywhere, including mid-word ("prio" finds
|
|
8
8
|
* "Task Priorities"), because they're short;
|
|
9
|
-
* - bodies match whole words, every word as a prefix
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
9
|
+
* - bodies match whole words, every word as a prefix; mid-word body matches
|
|
10
|
+
* are deliberately not supported. A phrase needs its words adjacent, except
|
|
11
|
+
* in a document too long or repetitive for Postgres to keep every word
|
|
12
|
+
* position, where every word being in one field is enough.
|
|
13
|
+
*
|
|
14
|
+
* The index is read one row per document the app's query allows, not through
|
|
15
|
+
* its GIN index across the table. The table holds every user's documents, so a
|
|
16
|
+
* common word would otherwise read most of them, and Postgres will: it doesn't
|
|
17
|
+
* count what fetching a stored vector costs, so it picks that scan however many
|
|
18
|
+
* words the query has. Reading only the caller's rows keeps the cost to the
|
|
19
|
+
* size of their library, and to that of whatever else narrows the query.
|
|
14
20
|
*
|
|
15
21
|
* Ranking reproduces the tiers the browser lane uses
|
|
16
22
|
* (`TITLE_MATCH_TIER` in Content): exact title 5, title prefix 4, title word
|
|
@@ -23,7 +29,7 @@
|
|
|
23
29
|
*/
|
|
24
30
|
import { and, not, or, sql } from "drizzle-orm";
|
|
25
31
|
import { SEARCH_RESOURCES_TABLE } from "./index-store.js";
|
|
26
|
-
import { anyOfTsquery, isPhraseTerm, normalizeSearchText, termTsquery, } from "./tokenize.js";
|
|
32
|
+
import { allOfTsquery, anyOfTsquery, isPhraseTerm, normalizeSearchText, termTsquery, } from "./tokenize.js";
|
|
27
33
|
const ALIAS = "search_index";
|
|
28
34
|
/** The indexer's weights for title, summary, and body. */
|
|
29
35
|
const FIELD_WEIGHTS = ["A", "B", "C"];
|
|
@@ -61,8 +67,11 @@ export function indexedSearchSql(options) {
|
|
|
61
67
|
const titleOnlyField = options.fields === "title";
|
|
62
68
|
const needle = (term) => normalizeSearchText(term.text);
|
|
63
69
|
const bodyTerms = (terms) => titleOnlyField ? [] : terms.filter((term) => !term.titleOnly);
|
|
70
|
+
const docVector = sql.raw(`${ALIAS}.doc_vector`);
|
|
71
|
+
const positionsComplete = sql.raw(`${ALIAS}.positions_complete`);
|
|
72
|
+
const termsTsquery = (terms) => anyOfTsquery(terms.map((term) => termTsquery(term.text, { prefix: true })));
|
|
64
73
|
const bodyMatch = (terms) => {
|
|
65
|
-
const tsquery =
|
|
74
|
+
const tsquery = termsTsquery(terms);
|
|
66
75
|
if (!tsquery)
|
|
67
76
|
return undefined;
|
|
68
77
|
// A document whose vector lost positions can't be phrase-matched
|
|
@@ -70,10 +79,9 @@ export function indexedSearchSql(options) {
|
|
|
70
79
|
const anyOrder = anyOfTsquery(terms
|
|
71
80
|
.filter((term) => isPhraseTerm(term.text))
|
|
72
81
|
.flatMap((term) => FIELD_WEIGHTS.map((weights) => termTsquery(term.text, { prefix: true, anyOrder: true, weights }))));
|
|
73
|
-
|
|
74
|
-
? sql `(
|
|
75
|
-
: sql
|
|
76
|
-
return sql `${sql.raw(`${ALIAS}.resource_id`)} IN (SELECT resource_id FROM ${sql.raw(SEARCH_RESOURCES_TABLE)} WHERE app = ${registration.app} AND resource_type = ${registration.type} AND ${vectorMatch})`;
|
|
82
|
+
return anyOrder
|
|
83
|
+
? sql `(${docVector} @@ ${tsquery}::tsquery OR (NOT ${positionsComplete} AND ${docVector} @@ ${anyOrder}::tsquery))`
|
|
84
|
+
: sql `${docVector} @@ ${tsquery}::tsquery`;
|
|
77
85
|
};
|
|
78
86
|
const titleAny = (terms) => or(...terms.map((term) => contains(titleNorm, needle(term))));
|
|
79
87
|
const summaryAny = (terms) => {
|
|
@@ -84,8 +92,35 @@ export function indexedSearchSql(options) {
|
|
|
84
92
|
};
|
|
85
93
|
const anyField = (terms) => or(titleAny(terms), summaryAny(terms), bodyMatch(bodyTerms(terms)));
|
|
86
94
|
const groups = query.groups.filter((group) => group.terms.length > 0);
|
|
95
|
+
/**
|
|
96
|
+
* Every group matching in any field, for groups of plain words. Each test of
|
|
97
|
+
* a stored vector reads it again, so asking for all the groups in one test
|
|
98
|
+
* reads each row's vector once. What that misses is a document where some
|
|
99
|
+
* group matches only in its title or summary, and only those few are tested
|
|
100
|
+
* group by group.
|
|
101
|
+
*/
|
|
102
|
+
const everyPlainWordMatch = () => {
|
|
103
|
+
if (titleOnlyField || groups.length < 2)
|
|
104
|
+
return undefined;
|
|
105
|
+
const tested = [];
|
|
106
|
+
for (const group of groups) {
|
|
107
|
+
const terms = bodyTerms(group.terms);
|
|
108
|
+
const tsquery = termsTsquery(terms);
|
|
109
|
+
if (!tsquery || terms.some((term) => isPhraseTerm(term.text))) {
|
|
110
|
+
return undefined;
|
|
111
|
+
}
|
|
112
|
+
tested.push({ terms: group.terms, tsquery });
|
|
113
|
+
}
|
|
114
|
+
return or(sql `${docVector} @@ ${allOfTsquery(tested.map((group) => group.tsquery))}::tsquery`, and(or(...tested.flatMap(({ terms }) => [
|
|
115
|
+
titleAny(terms),
|
|
116
|
+
summaryAny(terms),
|
|
117
|
+
])), ...tested.map(({ terms, tsquery }) => or(titleAny(terms), summaryAny(terms), sql `${docVector} @@ ${tsquery}::tsquery`))));
|
|
118
|
+
};
|
|
119
|
+
const plainWordMatch = everyPlainWordMatch();
|
|
87
120
|
const match = groups.length || query.negatives.length
|
|
88
|
-
? and(...
|
|
121
|
+
? and(...(plainWordMatch
|
|
122
|
+
? [plainWordMatch]
|
|
123
|
+
: groups.map((group) => anyField(group.terms))), ...query.negatives.map((negative) => not(anyField([negative]))))
|
|
89
124
|
: sql `false`;
|
|
90
125
|
const simpleQueries = groups.length > 0 && groups.every((group) => group.terms.length === 1)
|
|
91
126
|
? [groups.map((group) => group.terms[0].text.trim()).join(" ")]
|
|
@@ -132,11 +167,13 @@ export function indexedSearchSql(options) {
|
|
|
132
167
|
? termTsquery(phraseTerms, { prefix: true, weights: "C" })
|
|
133
168
|
: null;
|
|
134
169
|
const bodyPhrase = phraseQuery
|
|
135
|
-
? sql `case when ${
|
|
170
|
+
? sql `case when ${docVector} @@ ${phraseQuery}::tsquery then 1 else 0 end`
|
|
136
171
|
: null;
|
|
137
172
|
return {
|
|
138
|
-
|
|
139
|
-
|
|
173
|
+
// LATERAL looks up each row the app's query keeps, and OFFSET keeps Postgres
|
|
174
|
+
// from folding the lookup back into a join it would start from this table.
|
|
175
|
+
join: sql `LATERAL (SELECT * FROM ${sql.raw(SEARCH_RESOURCES_TABLE)} WHERE app = ${registration.app} AND resource_type = ${registration.type} AND resource_id = ${registration.idColumn}::text OFFSET 0) AS ${sql.raw(ALIAS)}`,
|
|
176
|
+
on: sql `true`,
|
|
140
177
|
match,
|
|
141
178
|
matchTier,
|
|
142
179
|
titleCoverage: titleCoverage ?? zero,
|
|
@@ -100,4 +100,5 @@ export interface QueryPhraseOptions {
|
|
|
100
100
|
export declare function termTsquery(text: string, options?: QueryPhraseOptions): string | null;
|
|
101
101
|
/** Whether a term is more than one lexeme, so it matches as a phrase. */
|
|
102
102
|
export declare function isPhraseTerm(text: string): boolean;
|
|
103
|
+
export declare function allOfTsquery(parts: readonly string[]): string;
|
|
103
104
|
export declare function anyOfTsquery(parts: readonly (string | null)[]): string | null;
|
package/dist/search/tokenize.js
CHANGED
|
@@ -357,6 +357,9 @@ export function termTsquery(text, options = {}) {
|
|
|
357
357
|
export function isPhraseTerm(text) {
|
|
358
358
|
return queryLexemes(text).length > 1;
|
|
359
359
|
}
|
|
360
|
+
export function allOfTsquery(parts) {
|
|
361
|
+
return parts.map((part) => `(${part})`).join(" & ");
|
|
362
|
+
}
|
|
360
363
|
export function anyOfTsquery(parts) {
|
|
361
364
|
const present = parts.filter((part) => !!part);
|
|
362
365
|
if (!present.length)
|
|
@@ -19,6 +19,8 @@ export interface ApiKeyEntry {
|
|
|
19
19
|
* shared row (owner `org`) or, for owner `user`, the caller's solo row.
|
|
20
20
|
*/
|
|
21
21
|
storedScope: SecretScope;
|
|
22
|
+
/** Exact outbound origins approved for this saved row. Empty or absent denies forwarding. */
|
|
23
|
+
urlAllowlist?: string[] | null;
|
|
22
24
|
/** "••••1234". Absent on managed keys, which only say they exist. */
|
|
23
25
|
masked?: string;
|
|
24
26
|
updatedAt: number | null;
|
|
@@ -34,6 +36,8 @@ export interface ApiKeyEntry {
|
|
|
34
36
|
managedBy?: SecretManagedBy;
|
|
35
37
|
/** Saving a new value writes this same row. */
|
|
36
38
|
canReplace: boolean;
|
|
39
|
+
/** The caller may edit this custom row's outbound origin policy. */
|
|
40
|
+
canEditOrigins?: boolean;
|
|
37
41
|
canDelete: boolean;
|
|
38
42
|
/** A registered validator can check the stored value. */
|
|
39
43
|
canTest: boolean;
|
package/dist/secrets/api-keys.js
CHANGED
|
@@ -73,11 +73,15 @@ function toEntry(caller, meta, owner, manages) {
|
|
|
73
73
|
target.scopeId === meta.scopeId &&
|
|
74
74
|
registered?.kind !== "oauth";
|
|
75
75
|
const canReplace = mayChange && !provider && writesThisRow;
|
|
76
|
+
const canEditOrigins = mayChange && !registered && !provider;
|
|
76
77
|
return {
|
|
77
78
|
name: meta.key,
|
|
78
79
|
...(registered ? { label: registered.label } : {}),
|
|
79
80
|
scope: owner,
|
|
80
81
|
storedScope: meta.scope,
|
|
82
|
+
...(canReplace || canEditOrigins
|
|
83
|
+
? { urlAllowlist: meta.urlAllowlist }
|
|
84
|
+
: {}),
|
|
81
85
|
...(managedBy || !meta.last4 ? {} : { masked: meta.last4 }),
|
|
82
86
|
updatedAt: meta.updatedAt || null,
|
|
83
87
|
...(vault ? { vault: true } : {}),
|
|
@@ -87,6 +91,7 @@ function toEntry(caller, meta, owner, manages) {
|
|
|
87
91
|
usedFor: describeSecretUsage(meta.key),
|
|
88
92
|
...(managedBy ? { managedBy } : {}),
|
|
89
93
|
canReplace,
|
|
94
|
+
canEditOrigins,
|
|
90
95
|
canDelete: mayChange,
|
|
91
96
|
canTest: canReplace && !!registered?.validator,
|
|
92
97
|
};
|
|
@@ -46,6 +46,8 @@ export interface RegisteredSecret {
|
|
|
46
46
|
kind: SecretKind;
|
|
47
47
|
required?: boolean;
|
|
48
48
|
validator?: SecretValidator;
|
|
49
|
+
/** Approved origins for outbound `${keys.NAME}` requests. */
|
|
50
|
+
urlAllowlist?: readonly string[];
|
|
49
51
|
oauthProvider?: string;
|
|
50
52
|
oauthConnectUrl?: string;
|
|
51
53
|
/** What this app uses the key for. Framework uses merge in at read time. */
|
package/dist/secrets/routes.d.ts
CHANGED
|
@@ -55,14 +55,14 @@ export declare function createWriteSecretHandler(): import("h3").EventHandlerWit
|
|
|
55
55
|
errorCode: "secret_managed_elsewhere";
|
|
56
56
|
managedBy: SecretManagedBy;
|
|
57
57
|
} | {
|
|
58
|
-
ok?: undefined;
|
|
59
58
|
error: string;
|
|
60
59
|
errorCode?: undefined;
|
|
60
|
+
ok?: undefined;
|
|
61
61
|
status?: undefined;
|
|
62
62
|
} | {
|
|
63
|
-
ok?: undefined;
|
|
64
63
|
error: string;
|
|
65
64
|
errorCode: string;
|
|
65
|
+
ok?: undefined;
|
|
66
66
|
status?: undefined;
|
|
67
67
|
} | {
|
|
68
68
|
error?: undefined;
|
package/dist/secrets/routes.js
CHANGED
|
@@ -7,14 +7,15 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import { defineEventHandler, getMethod, getQuery, setResponseStatus, } from "h3";
|
|
9
9
|
import { readBody } from "../server/h3-helpers.js";
|
|
10
|
+
import { isLoopbackHost } from "../server/origin-allowlist.js";
|
|
10
11
|
import { runWithRequestContext } from "../server/request-context.js";
|
|
11
12
|
async function canMutateWorkspaceScope(event, scopeId) {
|
|
12
13
|
if (scopeId.startsWith("solo:"))
|
|
13
14
|
return true;
|
|
14
15
|
const { getOrgContext } = await import("../org/context.js");
|
|
15
16
|
const ctx = await getOrgContext(event).catch(() => null);
|
|
16
|
-
if (!ctx?.orgId)
|
|
17
|
-
return
|
|
17
|
+
if (!ctx?.orgId || ctx.orgId !== scopeId)
|
|
18
|
+
return false;
|
|
18
19
|
return ctx.role === "owner" || ctx.role === "admin";
|
|
19
20
|
}
|
|
20
21
|
/**
|
|
@@ -35,7 +36,8 @@ import { PERSONAL_PROVIDER_KEYS_RESTRICTED_ERROR_CODE, resolvePersonalProviderKe
|
|
|
35
36
|
import { canonicalSecretKey, secretKeyNames } from "./key-aliases.js";
|
|
36
37
|
import { isManagedDeleteAllowed, managedDeleteRefusal, resolveSecretManagedBy, } from "./managed-keys.js";
|
|
37
38
|
import { listRequiredSecrets, getRequiredSecret, } from "./register.js";
|
|
38
|
-
import { writeAppSecret, deleteAppSecret, last4, listAppSecretsForScope, readAppSecretMeta, VAULT_SYNC_DESCRIPTION_PREFIX, } from "./storage.js";
|
|
39
|
+
import { writeAppSecret, updateAppSecretUrlAllowlist, deleteAppSecret, last4, listAppSecretsForScope, readAppSecretMeta, VAULT_SYNC_DESCRIPTION_PREFIX, } from "./storage.js";
|
|
40
|
+
import { registeredUrlAllowlist, validateUrlAllowlist, } from "./substitution.js";
|
|
39
41
|
import { describeSecretUsage } from "./usage.js";
|
|
40
42
|
export const SECRET_REJECTED_ERROR_CODE = "secret_rejected";
|
|
41
43
|
export const SECRET_VALIDATION_UNAVAILABLE_ERROR_CODE = "secret_validation_unavailable";
|
|
@@ -267,6 +269,15 @@ async function handleWrite(event, secret) {
|
|
|
267
269
|
setResponseStatus(event, 400);
|
|
268
270
|
return { error: "value is required" };
|
|
269
271
|
}
|
|
272
|
+
let urlAllowlist;
|
|
273
|
+
if (body.urlAllowlist !== undefined) {
|
|
274
|
+
const normalized = normalizeUrlAllowlist(body.urlAllowlist, registeredUrlAllowlist(secret.key));
|
|
275
|
+
if (normalized.ok === false) {
|
|
276
|
+
setResponseStatus(event, 400);
|
|
277
|
+
return { error: normalized.error };
|
|
278
|
+
}
|
|
279
|
+
urlAllowlist = JSON.stringify(normalized.origins);
|
|
280
|
+
}
|
|
270
281
|
const { scopeId, reason } = await resolveScopeId(event, secret.scope);
|
|
271
282
|
if (!scopeId) {
|
|
272
283
|
setResponseStatus(event, 401);
|
|
@@ -339,6 +350,7 @@ async function handleWrite(event, secret) {
|
|
|
339
350
|
value,
|
|
340
351
|
scope: secret.scope,
|
|
341
352
|
scopeId,
|
|
353
|
+
...(urlAllowlist === undefined ? {} : { urlAllowlist }),
|
|
342
354
|
});
|
|
343
355
|
}
|
|
344
356
|
catch (err) {
|
|
@@ -510,6 +522,9 @@ export function createAdHocSecretHandler() {
|
|
|
510
522
|
if (method === "POST" && !name) {
|
|
511
523
|
return handleAdHocWrite(event);
|
|
512
524
|
}
|
|
525
|
+
if (method === "PATCH" && name) {
|
|
526
|
+
return handleAdHocPolicyUpdate(event, name);
|
|
527
|
+
}
|
|
513
528
|
if (method === "DELETE" && name) {
|
|
514
529
|
return handleAdHocDelete(event, name);
|
|
515
530
|
}
|
|
@@ -615,6 +630,56 @@ async function handleAdHocWrite(event) {
|
|
|
615
630
|
}
|
|
616
631
|
return { ok: true, key: name };
|
|
617
632
|
}
|
|
633
|
+
async function handleAdHocPolicyUpdate(event, name) {
|
|
634
|
+
if (getRequiredSecret(name) || resolveSecretManagedBy(name)) {
|
|
635
|
+
setResponseStatus(event, 403);
|
|
636
|
+
return { error: "This key is managed by another settings surface" };
|
|
637
|
+
}
|
|
638
|
+
const rawBody = await readBody(event);
|
|
639
|
+
if (!rawBody || typeof rawBody !== "object" || Array.isArray(rawBody)) {
|
|
640
|
+
setResponseStatus(event, 400);
|
|
641
|
+
return { error: "A policy update body is required" };
|
|
642
|
+
}
|
|
643
|
+
const body = rawBody;
|
|
644
|
+
if (body.scope !== "user" &&
|
|
645
|
+
body.scope !== "org" &&
|
|
646
|
+
body.scope !== "workspace") {
|
|
647
|
+
setResponseStatus(event, 400);
|
|
648
|
+
return { error: "A saved key scope is required" };
|
|
649
|
+
}
|
|
650
|
+
if ((body.owner !== "user" && body.owner !== "org") ||
|
|
651
|
+
(body.owner === "user" && body.scope === "org") ||
|
|
652
|
+
(body.owner === "org" && body.scope === "user")) {
|
|
653
|
+
setResponseStatus(event, 400);
|
|
654
|
+
return { error: "A saved key owner is required" };
|
|
655
|
+
}
|
|
656
|
+
const normalized = normalizeUrlAllowlist(body.urlAllowlist);
|
|
657
|
+
if (normalized.ok === false) {
|
|
658
|
+
setResponseStatus(event, 400);
|
|
659
|
+
return { error: normalized.error };
|
|
660
|
+
}
|
|
661
|
+
const scope = body.scope;
|
|
662
|
+
const resolved = await resolveScopeId(event, scope === "workspace" && body.owner === "user" ? "user" : scope);
|
|
663
|
+
const scopeId = resolved.scopeId && scope === "workspace" && body.owner === "user"
|
|
664
|
+
? `solo:${resolved.scopeId}`
|
|
665
|
+
: resolved.scopeId;
|
|
666
|
+
if (!scopeId) {
|
|
667
|
+
setResponseStatus(event, 401);
|
|
668
|
+
return { error: resolved.reason ?? "Unable to resolve scope" };
|
|
669
|
+
}
|
|
670
|
+
if (body.owner === "org" && !(await canMutateOrgScope(event, scopeId))) {
|
|
671
|
+
setResponseStatus(event, 403);
|
|
672
|
+
return {
|
|
673
|
+
error: "Only organization owners and admins can edit shared keys",
|
|
674
|
+
};
|
|
675
|
+
}
|
|
676
|
+
const updated = await updateAppSecretUrlAllowlist({ key: name, scope, scopeId }, normalized.origins);
|
|
677
|
+
if (!updated) {
|
|
678
|
+
setResponseStatus(event, 404);
|
|
679
|
+
return { error: "Saved key not found" };
|
|
680
|
+
}
|
|
681
|
+
return { ok: true, key: name, urlAllowlist: normalized.origins };
|
|
682
|
+
}
|
|
618
683
|
async function handleAdHocDelete(event, name) {
|
|
619
684
|
if (getRequiredSecret(name)) {
|
|
620
685
|
setResponseStatus(event, 400);
|
|
@@ -693,7 +758,7 @@ function extractAdHocName(event) {
|
|
|
693
758
|
return null;
|
|
694
759
|
return AD_HOC_NAME_REGEX.test(candidate) ? candidate : null;
|
|
695
760
|
}
|
|
696
|
-
function normalizeUrlAllowlist(input) {
|
|
761
|
+
function normalizeUrlAllowlist(input, maximum = null) {
|
|
697
762
|
if (!Array.isArray(input) || !input.every((v) => typeof v === "string")) {
|
|
698
763
|
return { ok: false, error: "urlAllowlist must be an array of strings" };
|
|
699
764
|
}
|
|
@@ -712,15 +777,29 @@ function normalizeUrlAllowlist(input) {
|
|
|
712
777
|
error: `urlAllowlist entry "${value}" is not a valid URL`,
|
|
713
778
|
};
|
|
714
779
|
}
|
|
715
|
-
if (url.protocol !== "https:" &&
|
|
780
|
+
if (url.protocol !== "https:" &&
|
|
781
|
+
!(url.protocol === "http:" && isLoopbackHost(url.host))) {
|
|
782
|
+
return {
|
|
783
|
+
ok: false,
|
|
784
|
+
error: `urlAllowlist entry "${value}" must use https (or local loopback http)`,
|
|
785
|
+
};
|
|
786
|
+
}
|
|
787
|
+
if (url.username || url.password) {
|
|
716
788
|
return {
|
|
717
789
|
ok: false,
|
|
718
|
-
error:
|
|
790
|
+
error: "urlAllowlist entries cannot contain credentials",
|
|
719
791
|
};
|
|
720
792
|
}
|
|
721
793
|
if (!origins.includes(url.origin))
|
|
722
794
|
origins.push(url.origin);
|
|
723
795
|
}
|
|
796
|
+
if (maximum &&
|
|
797
|
+
origins.some((origin) => !validateUrlAllowlist(origin, maximum))) {
|
|
798
|
+
return {
|
|
799
|
+
ok: false,
|
|
800
|
+
error: "urlAllowlist exceeds the registered provider policy",
|
|
801
|
+
};
|
|
802
|
+
}
|
|
724
803
|
return { ok: true, origins };
|
|
725
804
|
}
|
|
726
805
|
function extractKeyFromEvent(event, opts = {}) {
|
|
@@ -41,6 +41,8 @@ export declare class AppSecretKeyMismatchError extends Error {
|
|
|
41
41
|
* record's id.
|
|
42
42
|
*/
|
|
43
43
|
export declare function writeAppSecret(args: WriteSecretArgs): Promise<string>;
|
|
44
|
+
/** Change an existing key's outbound policy without reading or replacing its value. */
|
|
45
|
+
export declare function updateAppSecretUrlAllowlist(ref: SecretRef, origins: readonly string[]): Promise<boolean>;
|
|
44
46
|
export interface ReadSecretResult {
|
|
45
47
|
value: string;
|
|
46
48
|
last4: string;
|
package/dist/secrets/storage.js
CHANGED
|
@@ -144,7 +144,7 @@ export async function writeAppSecret(args) {
|
|
|
144
144
|
encrypted_value = excluded.encrypted_value,
|
|
145
145
|
shared_encrypted_value = excluded.shared_encrypted_value,
|
|
146
146
|
description = excluded.description,
|
|
147
|
-
url_allowlist = excluded.url_allowlist,
|
|
147
|
+
url_allowlist = COALESCE(excluded.url_allowlist, app_secrets.url_allowlist),
|
|
148
148
|
updated_at = excluded.updated_at`;
|
|
149
149
|
const upsertArgs = [
|
|
150
150
|
id,
|
|
@@ -167,6 +167,29 @@ export async function writeAppSecret(args) {
|
|
|
167
167
|
invalidateAgentEngineStatusCache();
|
|
168
168
|
return String(rows[0]?.id ?? id);
|
|
169
169
|
}
|
|
170
|
+
/** Change an existing key's outbound policy without reading or replacing its value. */
|
|
171
|
+
export async function updateAppSecretUrlAllowlist(ref, origins) {
|
|
172
|
+
await ensureTable();
|
|
173
|
+
const { rows } = await getDbExec().execute({
|
|
174
|
+
sql: `UPDATE app_secrets SET url_allowlist = ?, updated_at = ?
|
|
175
|
+
WHERE scope = ? AND scope_id = ? AND key = ?
|
|
176
|
+
AND (description IS NULL OR description NOT LIKE ?)
|
|
177
|
+
RETURNING id`,
|
|
178
|
+
args: [
|
|
179
|
+
JSON.stringify(origins),
|
|
180
|
+
Date.now(),
|
|
181
|
+
ref.scope,
|
|
182
|
+
ref.scopeId,
|
|
183
|
+
ref.key,
|
|
184
|
+
`${VAULT_SYNC_DESCRIPTION_PREFIX}%`,
|
|
185
|
+
],
|
|
186
|
+
});
|
|
187
|
+
if (rows.length === 0)
|
|
188
|
+
return false;
|
|
189
|
+
invalidateRequestSecret(ref);
|
|
190
|
+
invalidateOptionalKeyCache();
|
|
191
|
+
return true;
|
|
192
|
+
}
|
|
170
193
|
function decryptAppSecretValue(encrypted, sharedEncrypted) {
|
|
171
194
|
if (sharedEncrypted) {
|
|
172
195
|
try {
|
|
@@ -540,18 +563,20 @@ export async function listAppSecretsForScope(scope, scopeId) {
|
|
|
540
563
|
return results;
|
|
541
564
|
}
|
|
542
565
|
function parseAllowlist(raw) {
|
|
543
|
-
if (
|
|
566
|
+
if (raw === null)
|
|
544
567
|
return null;
|
|
568
|
+
let parsed;
|
|
545
569
|
try {
|
|
546
|
-
|
|
547
|
-
if (Array.isArray(parsed) && parsed.every((v) => typeof v === "string")) {
|
|
548
|
-
return parsed;
|
|
549
|
-
}
|
|
550
|
-
return null;
|
|
570
|
+
parsed = JSON.parse(raw);
|
|
551
571
|
}
|
|
552
|
-
catch {
|
|
553
|
-
|
|
572
|
+
catch (error) {
|
|
573
|
+
throw new Error("Stored secret URL policy is unreadable", { cause: error });
|
|
574
|
+
}
|
|
575
|
+
if (!Array.isArray(parsed) ||
|
|
576
|
+
!parsed.every((value) => typeof value === "string")) {
|
|
577
|
+
throw new Error("Stored secret URL policy is invalid");
|
|
554
578
|
}
|
|
579
|
+
return parsed;
|
|
555
580
|
}
|
|
556
581
|
export async function deleteAppSecret(ref) {
|
|
557
582
|
await ensureTable();
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
* installs benefit from the stricter default — opt in only after the
|
|
25
25
|
* org-admin write-gate is verified to be active.
|
|
26
26
|
*/
|
|
27
|
-
import type
|
|
27
|
+
import { type SecretScope } from "./register.js";
|
|
28
28
|
export interface ResolveKeyReferencesResult {
|
|
29
29
|
resolved: string;
|
|
30
30
|
usedKeys: string[];
|
|
@@ -38,7 +38,8 @@ export interface ResolvedKeyReference {
|
|
|
38
38
|
}
|
|
39
39
|
export declare function resolveKeyReferences(text: string, scope: SecretScope, scopeId: string): Promise<ResolveKeyReferencesResult>;
|
|
40
40
|
export declare function resolveKeyReferencesWithRequestScopes(text: string, userScopeId: string): Promise<ResolveKeyReferencesResult>;
|
|
41
|
-
export declare function
|
|
41
|
+
export declare function registeredUrlAllowlist(name: string): string[] | null;
|
|
42
|
+
export declare function validateUrlAllowlist(url: string, allowlist: readonly string[] | null): boolean;
|
|
42
43
|
/**
|
|
43
44
|
* Convenience helper: look up a key's allowlist by name+scope. Returns null
|
|
44
45
|
* when the key doesn't exist or has no allowlist configured.
|