@agent-native/core 0.203.2-nightly-20261007213109 → 0.204.0
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/a2a/handlers.js +57 -1
- package/dist/a2a/server.js +37 -1
- package/dist/action.d.ts +1 -0
- package/dist/action.js +26 -3
- package/dist/agent/contain-principal.d.ts +9 -0
- package/dist/agent/contain-principal.js +53 -0
- package/dist/agent/production-agent.js +32 -2
- package/dist/agent/run-manager.js +14 -2
- package/dist/agent/run-store.js +66 -12
- package/dist/audit/actions/export-audit-events.d.ts +1 -1
- package/dist/audit/actions/export-audit-events.js +2 -2
- package/dist/audit/actions/export-audit-ocsf.d.ts +11 -0
- package/dist/audit/actions/export-audit-ocsf.js +183 -0
- package/dist/audit/actions/list-audit-events.d.ts +1 -1
- package/dist/audit/actions/list-audit-events.js +3 -3
- package/dist/audit/config.d.ts +1 -1
- package/dist/audit/config.js +5 -1
- package/dist/audit/index.d.ts +1 -0
- package/dist/audit/index.js +1 -0
- package/dist/audit/ocsf.d.ts +70 -0
- package/dist/audit/ocsf.js +228 -0
- package/dist/audit/record.js +1 -1
- package/dist/audit/store.js +9 -2
- package/dist/audit/types.d.ts +12 -1
- package/dist/cli/agent.js +2 -11
- package/dist/cli/connect.js +7 -2
- package/dist/client/use-db-sync.d.ts +13 -0
- package/dist/client/use-db-sync.js +22 -1
- package/dist/coding-tools/run-code.js +22 -1
- package/dist/coding-tools/sandbox/local-child-process-adapter.js +20 -0
- package/dist/collab/client.js +36 -3
- package/dist/collab/routes.d.ts +9 -4
- package/dist/collab/routes.js +15 -1
- package/dist/collab/struct-routes.d.ts +1 -1
- package/dist/deploy/build.js +15 -8
- package/dist/deploy/workspace-deploy.js +88 -13
- package/dist/file-upload/actions/upload-image.d.ts +1 -1
- package/dist/file-upload/builder.js +18 -4
- package/dist/framework-tools.js +3 -0
- package/dist/identity/rekey.js +6 -0
- package/dist/jobs/actions/manage-recurring-job.d.ts +1 -1
- package/dist/localization/core-messages/supplemental/ar-SA.js +15 -0
- package/dist/localization/core-messages/supplemental/de-DE.js +15 -0
- package/dist/localization/core-messages/supplemental/en-US.js +15 -0
- package/dist/localization/core-messages/supplemental/es-ES.js +15 -0
- package/dist/localization/core-messages/supplemental/fr-FR.js +15 -0
- package/dist/localization/core-messages/supplemental/hi-IN.js +15 -0
- package/dist/localization/core-messages/supplemental/ja-JP.js +15 -0
- package/dist/localization/core-messages/supplemental/ko-KR.js +15 -0
- package/dist/localization/core-messages/supplemental/pt-BR.js +15 -0
- package/dist/localization/core-messages/supplemental/zh-CN.js +15 -0
- package/dist/localization/core-messages/supplemental/zh-TW.js +15 -0
- package/dist/localization/mcp-settings-messages.d.ts +15 -0
- package/dist/mcp/actions/create-org-service-token.d.ts +8 -0
- package/dist/mcp/actions/create-org-service-token.js +93 -4
- package/dist/mcp/actions/list-org-service-tokens.d.ts +2 -0
- package/dist/mcp/actions/list-org-service-tokens.js +36 -8
- package/dist/mcp/actions/service-principal-input.d.ts +48 -0
- package/dist/mcp/actions/service-principal-input.js +102 -0
- package/dist/mcp/actions/set-service-principal-lifecycle.d.ts +15 -0
- package/dist/mcp/actions/set-service-principal-lifecycle.js +150 -0
- package/dist/mcp/actions/set-service-principal-policy.d.ts +14 -0
- package/dist/mcp/actions/set-service-principal-policy.js +105 -0
- package/dist/mcp/build-server.js +117 -13
- package/dist/mcp/connect-route.js +116 -4
- package/dist/mcp/connect-store.d.ts +7 -0
- package/dist/mcp/connect-store.js +21 -14
- package/dist/mcp-client/oauth-routes.js +2 -1
- package/dist/notifications/routes.d.ts +3 -3
- package/dist/observability/routes.d.ts +7 -7
- package/dist/org/index.d.ts +2 -2
- package/dist/org/service-principal-guard.d.ts +64 -0
- package/dist/org/service-principal-guard.js +140 -0
- package/dist/org/service-principal-policy.d.ts +104 -0
- package/dist/org/service-principal-policy.js +272 -0
- package/dist/provider-api/actions/custom-provider-registration.d.ts +5 -5
- package/dist/provider-api/actions/provider-api.d.ts +13 -13
- package/dist/scripts/dev/index.js +2 -8
- package/dist/scripts/parse-args.d.ts +7 -0
- package/dist/scripts/parse-args.js +76 -0
- package/dist/scripts/utils.d.ts +1 -1
- package/dist/scripts/utils.js +1 -1
- package/dist/secrets/routes.d.ts +11 -11
- package/dist/server/action-discovery.js +16 -42
- package/dist/server/agent-chat/script-entries.js +3 -8
- package/dist/server/agent-chat-plugin.js +8 -42
- package/dist/server/agent-engine-default-model-route.d.ts +2 -2
- package/dist/server/agent-engine-ollama-models-route.d.ts +1 -1
- package/dist/server/auth.js +5 -2
- package/dist/server/core-routes-plugin.js +25 -6
- package/dist/server/google-oauth.js +8 -4
- package/dist/server/identity-sso.js +2 -1
- package/dist/server/index.d.ts +1 -0
- package/dist/server/index.js +1 -0
- package/dist/server/poll.d.ts +9 -0
- package/dist/server/poll.js +86 -7
- package/dist/server/query-echo-safe-redirect.d.ts +10 -0
- package/dist/server/query-echo-safe-redirect.js +102 -0
- package/dist/server/release-schema.js +4 -0
- package/dist/server/workspace-provider-oauth.js +2 -1
- package/dist/templates/chat/.agents/skills/actions/references/action-fields.md +1 -0
- package/dist/templates/default/.agents/skills/actions/references/action-fields.md +1 -0
- package/dist/templates/factory/.agents/skills/review-latest-feedback/SKILL.md +24 -25
- package/dist/templates/factory/.agents/skills/review-latest-feedback/references/slack-reaction-replies.md +33 -0
- package/dist/templates/headless/.agents/skills/actions/references/action-fields.md +1 -0
- package/dist/templates/workspace-core/.agents/skills/actions/references/action-fields.md +1 -0
- package/dist/triggers/actions/manage-automation.d.ts +7 -7
- package/dist/vite/client.js +1 -0
- package/docs/content/audit-log.mdx +15 -0
- package/docs/content/external-agents-catalog.mdx +19 -0
- package/docs/content/http-api.mdx +21 -2
- package/docs/content/pr-visual-recap.mdx +3 -1
- package/docs/content/template-clips-embed.mdx +1 -1
- package/docs/content/turn-into-app.mdx +118 -0
- package/package.json +4 -4
- package/src/templates/chat/.agents/skills/actions/references/action-fields.md +1 -0
- package/src/templates/default/.agents/skills/actions/references/action-fields.md +1 -0
- package/src/templates/factory/.agents/skills/review-latest-feedback/SKILL.md +24 -25
- package/src/templates/factory/.agents/skills/review-latest-feedback/references/slack-reaction-replies.md +33 -0
- package/src/templates/headless/.agents/skills/actions/references/action-fields.md +1 -0
- package/src/templates/workspace-core/.agents/skills/actions/references/action-fields.md +1 -0
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { renameThread, resolveThreadAccess, setThreadArchived, setThreadPinned, } from "../../chat-threads/store.js";
|
|
2
2
|
import { dbExecToolParameters } from "../../scripts/db/tool-schemas.js";
|
|
3
|
+
import { serializeCliArgs } from "../../scripts/parse-args.js";
|
|
3
4
|
import { captureCliOutput } from "../cli-capture.js";
|
|
4
5
|
import { getAmbientUserEmail, getRequestOrgId, getRequestRunContext, getRequestUserEmail, } from "../request-context.js";
|
|
5
6
|
function wrapCliScript(tool, cliDefault, opts) {
|
|
@@ -7,18 +8,12 @@ function wrapCliScript(tool, cliDefault, opts) {
|
|
|
7
8
|
tool,
|
|
8
9
|
...(opts?.readOnly ? { readOnly: true } : {}),
|
|
9
10
|
run: async (args) => {
|
|
10
|
-
const
|
|
11
|
-
for (const [k, v] of Object.entries(args)) {
|
|
11
|
+
for (const k of Object.keys(args)) {
|
|
12
12
|
if (opts?.allowedArgs && !opts.allowedArgs.includes(k)) {
|
|
13
13
|
throw new Error(`Unknown argument: ${k}`);
|
|
14
14
|
}
|
|
15
|
-
const raw = v;
|
|
16
|
-
const value = raw != null && typeof raw === "object"
|
|
17
|
-
? JSON.stringify(raw)
|
|
18
|
-
: String(raw);
|
|
19
|
-
cliArgs.push(`--${k}`, value);
|
|
20
15
|
}
|
|
21
|
-
return captureCliOutput(() => cliDefault(
|
|
16
|
+
return captureCliOutput(() => cliDefault(serializeCliArgs(args)));
|
|
22
17
|
},
|
|
23
18
|
};
|
|
24
19
|
}
|
|
@@ -46,6 +46,7 @@ import { setProgressPreListHook } from "../progress/store.js";
|
|
|
46
46
|
import { getSkillNameFromPath } from "../resources/metadata.js";
|
|
47
47
|
import { resourceList, resourceListAccessible, resourceGet, ensurePersonalDefaults, isWorkspaceResourceOwner, SHARED_OWNER, WORKSPACE_OWNER, } from "../resources/store.js";
|
|
48
48
|
import { normalizeDatabaseToolsMode } from "../scripts/db/tool-mode.js";
|
|
49
|
+
import { normalizeShellArgs, serializeCliArgs } from "../scripts/parse-args.js";
|
|
49
50
|
import { getSetting, putSetting } from "../settings/store.js";
|
|
50
51
|
import { retryContextFromRequest, } from "../shared/agent-chat-run-not-started.js";
|
|
51
52
|
import { ANALYTICS_CLIENT_PLATFORM_BODY_FIELD, normalizeAnalyticsClientPlatform, } from "../shared/analytics-platform.js";
|
|
@@ -1043,54 +1044,19 @@ export function createAgentChatPlugin(options) {
|
|
|
1043
1044
|
}
|
|
1044
1045
|
const tokens = [];
|
|
1045
1046
|
if (typeof input?.args === "string" && input.args.trim()) {
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
for (let i = 0; i < input.args.length; i++) {
|
|
1051
|
-
const char = input.args[i];
|
|
1052
|
-
if (escape) {
|
|
1053
|
-
current += char;
|
|
1054
|
-
escape = false;
|
|
1055
|
-
continue;
|
|
1056
|
-
}
|
|
1057
|
-
if (char === "\\") {
|
|
1058
|
-
if (inSingle) {
|
|
1059
|
-
current += char;
|
|
1060
|
-
}
|
|
1061
|
-
else {
|
|
1062
|
-
escape = true;
|
|
1063
|
-
}
|
|
1064
|
-
continue;
|
|
1065
|
-
}
|
|
1066
|
-
if (char === "'" && !inDouble) {
|
|
1067
|
-
inSingle = !inSingle;
|
|
1068
|
-
continue;
|
|
1069
|
-
}
|
|
1070
|
-
if (char === '"' && !inSingle) {
|
|
1071
|
-
inDouble = !inDouble;
|
|
1072
|
-
continue;
|
|
1073
|
-
}
|
|
1074
|
-
if (/\s/.test(char) && !inSingle && !inDouble) {
|
|
1075
|
-
if (current.length > 0) {
|
|
1076
|
-
tokens.push(current);
|
|
1077
|
-
current = "";
|
|
1078
|
-
}
|
|
1079
|
-
continue;
|
|
1080
|
-
}
|
|
1081
|
-
current += char;
|
|
1082
|
-
}
|
|
1083
|
-
if (current.length > 0) {
|
|
1084
|
-
tokens.push(current);
|
|
1085
|
-
}
|
|
1047
|
+
tokens.push(...normalizeShellArgs(input.args, {
|
|
1048
|
+
backslashEscapes: true,
|
|
1049
|
+
splitAllWhitespace: true,
|
|
1050
|
+
}));
|
|
1086
1051
|
}
|
|
1087
1052
|
else if (input && typeof input === "object") {
|
|
1053
|
+
const actionArgs = {};
|
|
1088
1054
|
for (const [k, v] of Object.entries(input)) {
|
|
1089
1055
|
if (k === "args" || v === undefined || v === null)
|
|
1090
1056
|
continue;
|
|
1091
|
-
|
|
1092
|
-
tokens.push(`--${k}`, strVal);
|
|
1057
|
+
actionArgs[k] = v;
|
|
1093
1058
|
}
|
|
1059
|
+
tokens.push(...serializeCliArgs(actionArgs));
|
|
1094
1060
|
}
|
|
1095
1061
|
const BLOCKED_OPERATORS = new Set([
|
|
1096
1062
|
";",
|
|
@@ -9,16 +9,16 @@ export declare const AGENT_ENGINE_API_KEY_ACTION_NAME = "agent-engine-api-key";
|
|
|
9
9
|
*/
|
|
10
10
|
export declare function createAgentEngineDisconnectHandler(): import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<{
|
|
11
11
|
error: string;
|
|
12
|
-
ok?: undefined;
|
|
13
12
|
scope?: undefined;
|
|
13
|
+
ok?: undefined;
|
|
14
14
|
} | {
|
|
15
15
|
ok: boolean;
|
|
16
16
|
error: any;
|
|
17
17
|
scope?: undefined;
|
|
18
18
|
} | {
|
|
19
|
-
error?: undefined;
|
|
20
19
|
ok: boolean;
|
|
21
20
|
scope: "org" | "user";
|
|
21
|
+
error?: undefined;
|
|
22
22
|
}>>;
|
|
23
23
|
export interface DefaultModelSelectionRequest {
|
|
24
24
|
engine: string;
|
package/dist/server/auth.js
CHANGED
|
@@ -85,6 +85,7 @@ import * as loopback from "./loopback.js";
|
|
|
85
85
|
import { encodeMagicLinkSignupAttribution, MAGIC_LINK_ATTRIBUTION_PARAM, } from "./magic-link-attribution.js";
|
|
86
86
|
import { safeOAuthReturnUrl } from "./oauth-return-url.js";
|
|
87
87
|
import { getDeploySettingsRequiredHtml, getOnboardingHtml, getResetPasswordHtml, } from "./onboarding-html.js";
|
|
88
|
+
import { queryEchoSafeRedirect } from "./query-echo-safe-redirect.js";
|
|
88
89
|
import { getRequestContext, markRequestIdentityAuthenticatedAtMs, hasContinuationLocalRequestContext, hasExplicitPersonalOrgScope, markExplicitPersonalOrgScope, runWithRequestContext, } from "./request-context.js";
|
|
89
90
|
import { captureAuthError } from "./sentry.js";
|
|
90
91
|
import { isTestIdentity } from "./test-identity.js";
|
|
@@ -4901,7 +4902,9 @@ async function mountBetterAuthRoutes(app, options) {
|
|
|
4901
4902
|
response.status < 300) {
|
|
4902
4903
|
setFirstRunOnboardingCookie(event);
|
|
4903
4904
|
}
|
|
4904
|
-
return
|
|
4905
|
+
return isResponse
|
|
4906
|
+
? queryEchoSafeRedirect(event, response)
|
|
4907
|
+
: response;
|
|
4905
4908
|
}));
|
|
4906
4909
|
app.use("/_agent-native/auth/login", defineEventHandler(async (event) => {
|
|
4907
4910
|
if (getMethod(event) !== "POST") {
|
|
@@ -4968,7 +4971,7 @@ async function mountBetterAuthRoutes(app, options) {
|
|
|
4968
4971
|
? query.return[0]
|
|
4969
4972
|
: query.return;
|
|
4970
4973
|
setFirstRunOnboardingCookie(event);
|
|
4971
|
-
return redirectWithStagedCookies(event, safeReturnPath(rawReturn), 302);
|
|
4974
|
+
return queryEchoSafeRedirect(event, redirectWithStagedCookies(event, safeReturnPath(rawReturn), 302));
|
|
4972
4975
|
}));
|
|
4973
4976
|
app.use("/_agent-native/auth/magic-link", defineEventHandler(async (event) => {
|
|
4974
4977
|
if (getMethod(event) === "POST" &&
|
|
@@ -4152,12 +4152,31 @@ export function createCoreRoutesPlugin(options = {}) {
|
|
|
4152
4152
|
return { error: "Unauthorized" };
|
|
4153
4153
|
}
|
|
4154
4154
|
const userEmail = session.email;
|
|
4155
|
-
|
|
4156
|
-
|
|
4157
|
-
|
|
4158
|
-
|
|
4159
|
-
|
|
4160
|
-
|
|
4155
|
+
let result;
|
|
4156
|
+
try {
|
|
4157
|
+
result = await runWithRequestContext({ userEmail, orgId: session.orgId }, () => uploadFile({
|
|
4158
|
+
data: filePart.data,
|
|
4159
|
+
filename: filePart.filename,
|
|
4160
|
+
mimeType: filePart.type,
|
|
4161
|
+
ownerEmail: userEmail,
|
|
4162
|
+
}));
|
|
4163
|
+
}
|
|
4164
|
+
catch (error) {
|
|
4165
|
+
// A thrown provider error (e.g. an upstream API rejecting the
|
|
4166
|
+
// request) carries its own `status`/`statusCode`, which h3 would
|
|
4167
|
+
// otherwise surface verbatim to the client — indistinguishable
|
|
4168
|
+
// from this route's own deliberate 4xx responses above and
|
|
4169
|
+
// useless for the composer's generic "could not upload" copy.
|
|
4170
|
+
// Normalize to one clear failure instead of leaking whatever
|
|
4171
|
+
// status the active provider happened to respond with. Use 503
|
|
4172
|
+
// (not 502/504) since gateway statuses get rewritten by the CDN
|
|
4173
|
+
// layer anyway — see cdnSafeOriginStatus.
|
|
4174
|
+
console.error("[file-upload] provider upload failed", error);
|
|
4175
|
+
setResponseStatus(event, 503);
|
|
4176
|
+
return {
|
|
4177
|
+
error: "The configured storage provider could not upload this file. Try again or check Settings → File uploads.",
|
|
4178
|
+
};
|
|
4179
|
+
}
|
|
4161
4180
|
if (result) {
|
|
4162
4181
|
setResponseStatus(event, 201);
|
|
4163
4182
|
return result;
|
|
@@ -14,6 +14,7 @@ import { setIdentityGoogleAuthCookie } from "./identity-auth-provider.js";
|
|
|
14
14
|
import { isNetlifyDeployPermalinkGoogleOAuthClientOrigin, isNetlifyDeployPermalinkGoogleOAuthClientRequest, } from "./identity-sso-store.js";
|
|
15
15
|
import { appendSessionToOAuthReturnUrl } from "./oauth-return-url.js";
|
|
16
16
|
import { EXPLICIT_PUBLIC_ORIGIN_ENV_KEYS, firstOriginFromEnv, getConfiguredOriginAllowlist, isLoopbackHost, normalizeOrigin, WORKSPACE_GATEWAY_ORIGIN_ENV_KEYS, } from "./origin-allowlist.js";
|
|
17
|
+
import { queryEchoSafeRedirect } from "./query-echo-safe-redirect.js";
|
|
17
18
|
import { isWorkspaceOAuthCallbackRelayEnabled } from "./workspace-oauth.js";
|
|
18
19
|
function safeReturnPath(raw) {
|
|
19
20
|
return normalizeAppPath(raw) ?? "/";
|
|
@@ -669,9 +670,6 @@ export function oauthCallbackResponse(event, email, opts) {
|
|
|
669
670
|
</script></body></html>`);
|
|
670
671
|
}
|
|
671
672
|
const location = appendSessionToOAuthReturnUrl(opts.returnUrl, opts.sessionToken);
|
|
672
|
-
setResponseStatus(event, 302);
|
|
673
|
-
setResponseHeader(event, "Location", location);
|
|
674
|
-
setResponseHeader(event, "Referrer-Policy", "no-referrer");
|
|
675
673
|
const headers = new Headers({
|
|
676
674
|
Location: location,
|
|
677
675
|
"Referrer-Policy": "no-referrer",
|
|
@@ -679,7 +677,13 @@ export function oauthCallbackResponse(event, email, opts) {
|
|
|
679
677
|
for (const cookie of event.res?.headers?.getSetCookie?.() ?? []) {
|
|
680
678
|
headers.append("set-cookie", cookie);
|
|
681
679
|
}
|
|
682
|
-
|
|
680
|
+
const response = queryEchoSafeRedirect(event, new Response(null, { status: 302, headers }));
|
|
681
|
+
if (response.status === 302) {
|
|
682
|
+
setResponseStatus(event, 302);
|
|
683
|
+
setResponseHeader(event, "Location", location);
|
|
684
|
+
}
|
|
685
|
+
setResponseHeader(event, "Referrer-Policy", "no-referrer");
|
|
686
|
+
return response;
|
|
683
687
|
}
|
|
684
688
|
export function oauthErrorPage(message, status = 400) {
|
|
685
689
|
const safe = escapeHtml(message);
|
|
@@ -16,6 +16,7 @@ import { createOAuthSession, getAppUrl, getOrigin } from "./google-oauth.js";
|
|
|
16
16
|
import { hasIdentityGoogleAuthCookie } from "./identity-auth-provider.js";
|
|
17
17
|
import { IDENTITY_SSO_PROVIDER_ID } from "./identity-sso-provider.js";
|
|
18
18
|
import { consumeSsoState, createSsoState, CANONICAL_IDENTITY_SSO_HUB_URL, NETLIFY_PREVIEW_IDENTITY_SSO_HUB_URL, getIdentityHubUrl, identitySsoLoginButtonHtml, isCanonicalIdentitySsoClientRequest, isDesktopSsoUserAgent, isIdentitySsoExplicitlyEnabled, isIdentitySsoEnabled, isNetlifyDeployPermalinkIdentitySsoClientRequest, isJtiReplayed, SSO_STATE_TTL_MS, } from "./identity-sso-store.js";
|
|
19
|
+
import { queryEchoSafeRedirect } from "./query-echo-safe-redirect.js";
|
|
19
20
|
import { getRequestContext, hasContinuationLocalRequestContext, runWithRequestContext, } from "./request-context.js";
|
|
20
21
|
export { getIdentityHubUrl, identitySsoLoginButtonHtml, isIdentitySsoEnabled };
|
|
21
22
|
export { IDENTITY_SSO_PROVIDER_ID };
|
|
@@ -63,7 +64,7 @@ function redirect(event, location) {
|
|
|
63
64
|
const staged = event.res?.headers?.getSetCookie?.() ?? [];
|
|
64
65
|
for (const cookie of staged)
|
|
65
66
|
headers.append("set-cookie", cookie);
|
|
66
|
-
return new Response("", { status: 302, headers });
|
|
67
|
+
return queryEchoSafeRedirect(event, new Response("", { status: 302, headers }));
|
|
67
68
|
}
|
|
68
69
|
function errorPage(message, loginPath) {
|
|
69
70
|
const safe = message
|
package/dist/server/index.d.ts
CHANGED
|
@@ -86,6 +86,7 @@ export { createOnboardingPlugin, defaultOnboardingPlugin, } from "../onboarding/
|
|
|
86
86
|
export { registerFileUploadProvider, unregisterFileUploadProvider, listFileUploadProviders, getActiveFileUploadProvider, getActiveFileUploadProviderForRequest, uploadFile, builderFileUploadProvider, type FileUploadInput, type FileUploadProvider, type FileUploadResult, } from "../file-upload/index.js";
|
|
87
87
|
export { createIntegrationsPlugin, defaultIntegrationsPlugin, enqueueRemoteCommand, slackAdapter, telegramAdapter, whatsappAdapter, discordAdapter, microsoftTeamsAdapter, emailAdapter, assertPlatformCapability, type PlatformAdapter, type IncomingMessage, type OutgoingMessage, type PlatformAdapterCapabilities, type ImmediateWebhookResponse, type IntegrationStatus, type IntegrationsPluginOptions, type IntegrationExecutionContext, BUILT_IN_INTEGRATION_CATALOG, INTEGRATION_CATEGORIES, getIntegrationCatalogEntry, listBuiltInChannelIntegrations, listIntegrationCatalog, type BuiltInChannelId, type ChannelCapabilities, type IntegrationAvailability, type IntegrationCatalogEntry, type IntegrationCategory, type IntegrationCredentialRequirement, type IntegrationIconKey, type IntegrationSupportMaturity, } from "../integrations/index.js";
|
|
88
88
|
export { isElectron, isMobile, getOrigin, getAppBasePath, getAppUrl, resolveOAuthRedirectUri, isAllowedOAuthRedirectUri, encodeOAuthState, decodeOAuthState, encodeNetlifyPreviewGoogleOAuthRelayState, decodeNetlifyPreviewGoogleOAuthRelayState, wrapNetlifyPreviewGoogleOAuthState, getNetlifyPreviewGoogleOAuthCallbackUrl, isNetlifyPreviewGoogleOAuthCallbackUrl, isNetlifyPreviewGoogleOAuthRelayState, AGENT_NATIVE_GOOGLE_OAUTH_RELAY_SECRET_ENV, NETLIFY_PREVIEW_GOOGLE_OAUTH_CALLBACK_URL, logOAuthStateDecodeFailure, resolveOAuthOwner, createOAuthSession, oauthCallbackResponse, oauthErrorPage, oauthDesktopExchangePage, type OAuthStatePayload, type OAuthStateDecodeFailureReason, type DecodeOAuthStateResult, type OAuthOwnerResult, type OAuthSessionResult, } from "./google-oauth.js";
|
|
89
|
+
export { queryEchoSafeRedirect } from "./query-echo-safe-redirect.js";
|
|
89
90
|
export { buildWorkspaceProviderAuthorizationUrl, createWorkspaceProviderOAuthHandler, exchangeWorkspaceProviderOAuthCode, handleWorkspaceProviderOAuthCallback, handleWorkspaceProviderOAuthStart, hasWorkspaceProviderOAuthCredentials, isGoogleWorkspaceOAuthProvider, isWorkspaceProviderOAuthFlowValid, mergeWorkspaceOAuthValues, resolveWorkspaceProviderIdentity, workspaceProviderOAuthPath, type GenericWorkspaceOAuthProvider, type WorkspaceProviderOAuthFlow, } from "./workspace-provider-oauth.js";
|
|
90
91
|
export { CredentialStoreUnavailableError, FeatureNotConfiguredError, hasBuilderPrivateKey, isBuilderEnvManaged, getBuilderProxyOrigin, getBuilderImageGenerationBaseUrl, getBuilderEmbeddingsBaseUrl, getBuilderVideoGenerationBaseUrl, getBuilderWebSearchBaseUrl, getBuilderAuthHeader, resolveBuilderPrivateKey, resolveBuilderAuthHeader, resolveHasBuilderPrivateKey, resolveHasCompleteBuilderConnection, resolveBuilderCredentials, resolveBuilderCredentialsDetailed, resolveBuilderGatewayCredentialsDetailed, resolveBuilderGatewayAuth, resolveBuilderGatewayCredentials, resolveHasBuilderGatewayCredential, resolveBuilderCredentialSource, resolveBuilderCredential, readDeployCredentialEnv, resolveVercelDeploymentProtectionHeaders, writeBuilderCredentials, deleteBuilderCredentials, resolveSecret, resolveSecretDetailed, prefetchSecrets, BuilderCredentialLookupError, type BuilderCredentialsDetailed, type ResolvedSecretDetail, } from "./credential-provider.js";
|
|
91
92
|
export { GEMINI_API_KEY, LEGACY_GEMINI_API_KEY, canonicalSecretKey, readGeminiDeployCredentialEnv, resolveGeminiApiKey, resolveGeminiApiKeyDetailed, resolveSecretWithAliases, resolveSecretWithAliasesDetailed, secretKeyNames, type ResolvedAliasedSecret, } from "./secret-key-aliases.js";
|
package/dist/server/index.js
CHANGED
|
@@ -81,6 +81,7 @@ export { createOnboardingPlugin, defaultOnboardingPlugin, } from "../onboarding/
|
|
|
81
81
|
export { registerFileUploadProvider, unregisterFileUploadProvider, listFileUploadProviders, getActiveFileUploadProvider, getActiveFileUploadProviderForRequest, uploadFile, builderFileUploadProvider, } from "../file-upload/index.js";
|
|
82
82
|
export { createIntegrationsPlugin, defaultIntegrationsPlugin, enqueueRemoteCommand, slackAdapter, telegramAdapter, whatsappAdapter, discordAdapter, microsoftTeamsAdapter, emailAdapter, assertPlatformCapability, BUILT_IN_INTEGRATION_CATALOG, INTEGRATION_CATEGORIES, getIntegrationCatalogEntry, listBuiltInChannelIntegrations, listIntegrationCatalog, } from "../integrations/index.js";
|
|
83
83
|
export { isElectron, isMobile, getOrigin, getAppBasePath, getAppUrl, resolveOAuthRedirectUri, isAllowedOAuthRedirectUri, encodeOAuthState, decodeOAuthState, encodeNetlifyPreviewGoogleOAuthRelayState, decodeNetlifyPreviewGoogleOAuthRelayState, wrapNetlifyPreviewGoogleOAuthState, getNetlifyPreviewGoogleOAuthCallbackUrl, isNetlifyPreviewGoogleOAuthCallbackUrl, isNetlifyPreviewGoogleOAuthRelayState, AGENT_NATIVE_GOOGLE_OAUTH_RELAY_SECRET_ENV, NETLIFY_PREVIEW_GOOGLE_OAUTH_CALLBACK_URL, logOAuthStateDecodeFailure, resolveOAuthOwner, createOAuthSession, oauthCallbackResponse, oauthErrorPage, oauthDesktopExchangePage, } from "./google-oauth.js";
|
|
84
|
+
export { queryEchoSafeRedirect } from "./query-echo-safe-redirect.js";
|
|
84
85
|
export { buildWorkspaceProviderAuthorizationUrl, createWorkspaceProviderOAuthHandler, exchangeWorkspaceProviderOAuthCode, handleWorkspaceProviderOAuthCallback, handleWorkspaceProviderOAuthStart, hasWorkspaceProviderOAuthCredentials, isGoogleWorkspaceOAuthProvider, isWorkspaceProviderOAuthFlowValid, mergeWorkspaceOAuthValues, resolveWorkspaceProviderIdentity, workspaceProviderOAuthPath, } from "./workspace-provider-oauth.js";
|
|
85
86
|
export { CredentialStoreUnavailableError, FeatureNotConfiguredError, hasBuilderPrivateKey, isBuilderEnvManaged, getBuilderProxyOrigin, getBuilderImageGenerationBaseUrl, getBuilderEmbeddingsBaseUrl, getBuilderVideoGenerationBaseUrl, getBuilderWebSearchBaseUrl, getBuilderAuthHeader, resolveBuilderPrivateKey, resolveBuilderAuthHeader, resolveHasBuilderPrivateKey, resolveHasCompleteBuilderConnection, resolveBuilderCredentials, resolveBuilderCredentialsDetailed, resolveBuilderGatewayCredentialsDetailed, resolveBuilderGatewayAuth, resolveBuilderGatewayCredentials, resolveHasBuilderGatewayCredential, resolveBuilderCredentialSource, resolveBuilderCredential, readDeployCredentialEnv, resolveVercelDeploymentProtectionHeaders, writeBuilderCredentials, deleteBuilderCredentials, resolveSecret, resolveSecretDetailed, prefetchSecrets, BuilderCredentialLookupError, } from "./credential-provider.js";
|
|
86
87
|
export { GEMINI_API_KEY, LEGACY_GEMINI_API_KEY, canonicalSecretKey, readGeminiDeployCredentialEnv, resolveGeminiApiKey, resolveGeminiApiKeyDetailed, resolveSecretWithAliases, resolveSecretWithAliasesDetailed, secretKeyNames, } from "./secret-key-aliases.js";
|
package/dist/server/poll.d.ts
CHANGED
|
@@ -86,6 +86,10 @@ export declare class AppSyncState {
|
|
|
86
86
|
private accessCacheTtl;
|
|
87
87
|
private durableEventId;
|
|
88
88
|
getVersion(): number;
|
|
89
|
+
getPollBaseline(useDurableEvents: boolean): Promise<{
|
|
90
|
+
version: number;
|
|
91
|
+
cursor: string;
|
|
92
|
+
}>;
|
|
89
93
|
getPollEmitter(): EventEmitter;
|
|
90
94
|
wireLocalEmitters(): void;
|
|
91
95
|
ensureSyncEventsTable(): Promise<boolean>;
|
|
@@ -154,6 +158,7 @@ export declare class AppSyncState {
|
|
|
154
158
|
getChangesSinceForUser(since: number, userEmail: string, orgId: string | undefined, cursor?: SyncCursor): ChangeReadResult;
|
|
155
159
|
getDurableChangesSinceForUser(since: number, userEmail: string, orgId: string | undefined, cursor?: SyncCursor): Promise<ChangeReadResult>;
|
|
156
160
|
getCombinedChangesSinceForUser(since: number, userEmail: string, orgId: string | undefined, useDurableEvents: boolean, cursor?: SyncCursor): Promise<ChangeReadResult>;
|
|
161
|
+
private readCombinedChangesSinceForUser;
|
|
157
162
|
seedVersionFromDb(): Promise<void>;
|
|
158
163
|
checkExternalDbChanges(options: {
|
|
159
164
|
durableEvents: boolean;
|
|
@@ -164,6 +169,10 @@ declare function hostedRealtimeTransportEnabled(): boolean;
|
|
|
164
169
|
export declare const __hostedRealtimeTransportEnabledForTests: typeof hostedRealtimeTransportEnabled;
|
|
165
170
|
export declare function getDefaultAppSyncState(): AppSyncState;
|
|
166
171
|
export declare function getVersion(): number;
|
|
172
|
+
export declare function getCurrentPollBaseline(state?: AppSyncState): Promise<{
|
|
173
|
+
version: number;
|
|
174
|
+
cursor: string;
|
|
175
|
+
}>;
|
|
167
176
|
export declare function getPollEmitter(): EventEmitter;
|
|
168
177
|
export declare function invalidateCollabAccessCache(resourceType: string, resourceId: string): void;
|
|
169
178
|
export declare function __resetCollabAccessCacheForTests(): void;
|
package/dist/server/poll.js
CHANGED
|
@@ -43,6 +43,7 @@ export const POLL_CHANGE_EVENT = "poll-change";
|
|
|
43
43
|
const ACCESS_CACHE_TTL_MS = 30_000;
|
|
44
44
|
const ACCESS_CACHE_DENY_TTL_MS = 5_000;
|
|
45
45
|
const ACCESS_CACHE_MAX = 500;
|
|
46
|
+
const ACCESS_CHECK_WAIT_MS = 1_000;
|
|
46
47
|
const SCREEN_REFRESH_KEY = "__screen_refresh__";
|
|
47
48
|
const SCREEN_REFRESH_QUERY_LIMIT = 256;
|
|
48
49
|
const SEED_SYNC_VERSION_SQL = `
|
|
@@ -278,7 +279,7 @@ export class AppSyncState {
|
|
|
278
279
|
screenRefreshHasMore = false;
|
|
279
280
|
localEmittersWired = false;
|
|
280
281
|
accessCache = new Map();
|
|
281
|
-
accessInFlight = new
|
|
282
|
+
accessInFlight = new Map();
|
|
282
283
|
accessInvalidationEpoch = new Map();
|
|
283
284
|
accessAllowTtlMs;
|
|
284
285
|
constructor(options = {}) {
|
|
@@ -311,6 +312,40 @@ export class AppSyncState {
|
|
|
311
312
|
getVersion() {
|
|
312
313
|
return this.version;
|
|
313
314
|
}
|
|
315
|
+
async getPollBaseline(useDurableEvents) {
|
|
316
|
+
let cursor = { version: this.version, id: "" };
|
|
317
|
+
if (compareSyncCursors(this.latestCursor, cursor) > 0) {
|
|
318
|
+
cursor = this.latestCursor;
|
|
319
|
+
}
|
|
320
|
+
if (useDurableEvents && !syncEventsDisabled()) {
|
|
321
|
+
try {
|
|
322
|
+
const result = await this.getDb().execute("SELECT version, id FROM sync_events WHERE version = (SELECT MAX(version) FROM sync_events) ORDER BY id DESC LIMIT 1");
|
|
323
|
+
const row = result.rows[0];
|
|
324
|
+
if (row) {
|
|
325
|
+
const version = Number(row.version);
|
|
326
|
+
if (!Number.isSafeInteger(version) ||
|
|
327
|
+
version < 0 ||
|
|
328
|
+
typeof row.id !== "string") {
|
|
329
|
+
throw new Error("Durable sync event cursor is invalid");
|
|
330
|
+
}
|
|
331
|
+
const durableCursor = { version, id: row.id };
|
|
332
|
+
if (compareSyncCursors(durableCursor, cursor) > 0) {
|
|
333
|
+
cursor = durableCursor;
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
catch (error) {
|
|
338
|
+
if (isMissingRelationError(error)) {
|
|
339
|
+
throw new SyncEventsTableUnavailableError();
|
|
340
|
+
}
|
|
341
|
+
throw error;
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
return {
|
|
345
|
+
version: Math.max(this.version, cursor.version),
|
|
346
|
+
cursor: encodeSyncCursor(cursor),
|
|
347
|
+
};
|
|
348
|
+
}
|
|
314
349
|
getPollEmitter() {
|
|
315
350
|
return this.pollEmitter;
|
|
316
351
|
}
|
|
@@ -551,7 +586,7 @@ export class AppSyncState {
|
|
|
551
586
|
if (key.endsWith(suffix))
|
|
552
587
|
this.accessCache.delete(key);
|
|
553
588
|
}
|
|
554
|
-
for (const key of Array.from(this.accessInFlight)) {
|
|
589
|
+
for (const key of Array.from(this.accessInFlight.keys())) {
|
|
555
590
|
if (key.endsWith(suffix))
|
|
556
591
|
this.accessInFlight.delete(key);
|
|
557
592
|
}
|
|
@@ -572,10 +607,10 @@ export class AppSyncState {
|
|
|
572
607
|
scheduleAccessCheck(key, resourceType, resourceId, userEmail, orgId) {
|
|
573
608
|
if (this.accessInFlight.has(key))
|
|
574
609
|
return;
|
|
575
|
-
this.accessInFlight.add(key);
|
|
576
610
|
const resourceKey = accessResourceKey(resourceType, resourceId);
|
|
577
611
|
const epoch = this.accessInvalidationEpoch.get(resourceKey) ?? 0;
|
|
578
|
-
|
|
612
|
+
let settled = false;
|
|
613
|
+
const check = (async () => {
|
|
579
614
|
try {
|
|
580
615
|
const access = await this.resolveAccessFn(resourceType, resourceId, {
|
|
581
616
|
userEmail,
|
|
@@ -593,9 +628,12 @@ export class AppSyncState {
|
|
|
593
628
|
this.setAccessCache(key, false, Date.now());
|
|
594
629
|
}
|
|
595
630
|
finally {
|
|
631
|
+
settled = true;
|
|
596
632
|
this.accessInFlight.delete(key);
|
|
597
633
|
}
|
|
598
634
|
})();
|
|
635
|
+
if (!settled)
|
|
636
|
+
this.accessInFlight.set(key, check);
|
|
599
637
|
}
|
|
600
638
|
__resetAccessCacheForTests() {
|
|
601
639
|
this.accessCache.clear();
|
|
@@ -896,7 +934,7 @@ export class AppSyncState {
|
|
|
896
934
|
return { version: this.version, events: [] };
|
|
897
935
|
}
|
|
898
936
|
const readCursor = cursor ?? { version: since, id: "" };
|
|
899
|
-
if (readCursor.version <= 0 && readCursor.id === "") {
|
|
937
|
+
if (!cursor && readCursor.version <= 0 && readCursor.id === "") {
|
|
900
938
|
try {
|
|
901
939
|
const result = await this.getDb().execute("SELECT MAX(version) as max_version FROM sync_events");
|
|
902
940
|
return {
|
|
@@ -1044,6 +1082,23 @@ export class AppSyncState {
|
|
|
1044
1082
|
}
|
|
1045
1083
|
}
|
|
1046
1084
|
async getCombinedChangesSinceForUser(since, userEmail, orgId, useDurableEvents, cursor) {
|
|
1085
|
+
const result = await this.readCombinedChangesSinceForUser(since, userEmail, orgId, useDurableEvents, cursor);
|
|
1086
|
+
if (!result.cursorLimited || this.accessInFlight.size === 0)
|
|
1087
|
+
return result;
|
|
1088
|
+
// A read stopped at an event whose access check had not finished. The
|
|
1089
|
+
// check is already running, so waiting for it here (bounded) delivers the
|
|
1090
|
+
// event now instead of one poll interval later.
|
|
1091
|
+
let timer;
|
|
1092
|
+
await Promise.race([
|
|
1093
|
+
Promise.allSettled(this.accessInFlight.values()),
|
|
1094
|
+
new Promise((resolve) => {
|
|
1095
|
+
timer = setTimeout(resolve, ACCESS_CHECK_WAIT_MS);
|
|
1096
|
+
}),
|
|
1097
|
+
]);
|
|
1098
|
+
clearTimeout(timer);
|
|
1099
|
+
return this.readCombinedChangesSinceForUser(since, userEmail, orgId, useDurableEvents, cursor);
|
|
1100
|
+
}
|
|
1101
|
+
async readCombinedChangesSinceForUser(since, userEmail, orgId, useDurableEvents, cursor) {
|
|
1047
1102
|
const memory = this.getChangesSinceForUser(since, userEmail, orgId, cursor);
|
|
1048
1103
|
if (!useDurableEvents)
|
|
1049
1104
|
return memory;
|
|
@@ -1066,8 +1121,7 @@ export class AppSyncState {
|
|
|
1066
1121
|
if (cursor) {
|
|
1067
1122
|
const limitedCursors = [memory, durable]
|
|
1068
1123
|
.filter((result) => result.cursorLimited)
|
|
1069
|
-
.map((result) => decodeSyncCursor(result.cursor))
|
|
1070
|
-
.filter((value) => !!value);
|
|
1124
|
+
.map((result) => decodeSyncCursor(result.cursor) ?? cursor);
|
|
1071
1125
|
if (limitedCursors.length > 0) {
|
|
1072
1126
|
const boundary = limitedCursors.reduce((minimum, value) => compareSyncCursors(value, minimum) < 0 ? value : minimum);
|
|
1073
1127
|
return {
|
|
@@ -1103,6 +1157,7 @@ export class AppSyncState {
|
|
|
1103
1157
|
events: limitedVersions.length > 0
|
|
1104
1158
|
? events.filter((event) => event.version <= Math.min(...limitedVersions))
|
|
1105
1159
|
: events,
|
|
1160
|
+
...(limitedVersions.length > 0 ? { cursorLimited: true } : {}),
|
|
1106
1161
|
};
|
|
1107
1162
|
}
|
|
1108
1163
|
async seedVersionFromDb() {
|
|
@@ -1356,6 +1411,30 @@ export function getDefaultAppSyncState() {
|
|
|
1356
1411
|
export function getVersion() {
|
|
1357
1412
|
return getDefaultAppSyncState().getVersion();
|
|
1358
1413
|
}
|
|
1414
|
+
export async function getCurrentPollBaseline(state = getDefaultAppSyncState()) {
|
|
1415
|
+
if (syncEventsDisabled()) {
|
|
1416
|
+
await state.seedVersionFromDb();
|
|
1417
|
+
await state.checkExternalDbChanges({ durableEvents: false });
|
|
1418
|
+
return state.getPollBaseline(false);
|
|
1419
|
+
}
|
|
1420
|
+
const releaseOwnedServerlessPoll = isProductionServerlessFunctionRuntime() && appMigratesAtRelease();
|
|
1421
|
+
try {
|
|
1422
|
+
if (!releaseOwnedServerlessPoll) {
|
|
1423
|
+
await state.seedVersionFromDb();
|
|
1424
|
+
await state.checkExternalDbChanges({ durableEvents: true });
|
|
1425
|
+
}
|
|
1426
|
+
return await state.getPollBaseline(true);
|
|
1427
|
+
}
|
|
1428
|
+
catch (error) {
|
|
1429
|
+
if (!(error instanceof SyncEventsTableUnavailableError))
|
|
1430
|
+
throw error;
|
|
1431
|
+
if (releaseOwnedServerlessPoll)
|
|
1432
|
+
throw error;
|
|
1433
|
+
await state.seedVersionFromDb();
|
|
1434
|
+
await state.checkExternalDbChanges({ durableEvents: false });
|
|
1435
|
+
return state.getPollBaseline(false);
|
|
1436
|
+
}
|
|
1437
|
+
}
|
|
1359
1438
|
export function getPollEmitter() {
|
|
1360
1439
|
return getDefaultAppSyncState().getPollEmitter();
|
|
1361
1440
|
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { H3Event } from "h3";
|
|
2
|
+
/**
|
|
3
|
+
* Netlify's proxy copies the request's query string onto any redirect whose
|
|
4
|
+
* Location has none, so a bare 302 out of a sign-in callback lands the browser
|
|
5
|
+
* on a page still carrying the one-time `token`, OAuth `code`, or attribution
|
|
6
|
+
* params. Every later bare 302 in the chain inherits the query the same way,
|
|
7
|
+
* so no redirect-only route can shed it. Netlify leaves a 200 alone: a
|
|
8
|
+
* navigation gets an HTML page that replaces itself with the clean Location.
|
|
9
|
+
*/
|
|
10
|
+
export declare function queryEchoSafeRedirect(event: H3Event, response: Response): Response;
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { getHeader } from "h3";
|
|
2
|
+
import { getAppBasePathFromViteEnv } from "./app-base-path.js";
|
|
3
|
+
function requestHasQuery(event) {
|
|
4
|
+
if (event.url?.search)
|
|
5
|
+
return true;
|
|
6
|
+
const raw = event.node?.req?.url ??
|
|
7
|
+
(typeof event.path === "string" ? event.path : "");
|
|
8
|
+
const queryStart = raw.indexOf("?");
|
|
9
|
+
return queryStart >= 0 && queryStart < raw.length - 1;
|
|
10
|
+
}
|
|
11
|
+
function isDocumentNavigation(event) {
|
|
12
|
+
const mode = getHeader(event, "sec-fetch-mode");
|
|
13
|
+
if (mode)
|
|
14
|
+
return mode === "navigate";
|
|
15
|
+
return (getHeader(event, "accept") ?? "").includes("text/html");
|
|
16
|
+
}
|
|
17
|
+
const PATH_BASE = "http://an.invalid";
|
|
18
|
+
// The page navigates with `location.replace`, which runs a `javascript:` URL
|
|
19
|
+
// that a 302 would refuse, so only http(s) destinations qualify. A path is
|
|
20
|
+
// resolved rather than pattern-matched: the URL parser drops tabs and
|
|
21
|
+
// newlines, so `/\t/evil.test` names another host.
|
|
22
|
+
function bareHttpDestination(location) {
|
|
23
|
+
try {
|
|
24
|
+
if (location.startsWith("/")) {
|
|
25
|
+
const target = new URL(location, PATH_BASE);
|
|
26
|
+
if (target.origin !== PATH_BASE || target.search)
|
|
27
|
+
return null;
|
|
28
|
+
return withAppBasePath(location);
|
|
29
|
+
}
|
|
30
|
+
const target = new URL(location);
|
|
31
|
+
if (target.protocol !== "https:" && target.protocol !== "http:") {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
return target.search ? null : location;
|
|
35
|
+
// coercion-ok: a Location the URL parser rejects is not ours to rewrite.
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
// Workspace mounts and the dev gateway prefix an app's path Locations with its
|
|
42
|
+
// base path, but only on 3xx responses; the page has to carry the prefix.
|
|
43
|
+
function withAppBasePath(path) {
|
|
44
|
+
const base = getAppBasePathFromViteEnv();
|
|
45
|
+
if (!base)
|
|
46
|
+
return path;
|
|
47
|
+
const pathname = path.split(/[?#]/, 1)[0] || path;
|
|
48
|
+
if (pathname === base ||
|
|
49
|
+
pathname === `${base}.data` ||
|
|
50
|
+
pathname.startsWith(`${base}/`)) {
|
|
51
|
+
return path;
|
|
52
|
+
}
|
|
53
|
+
return pathname === "/" ? `${base}${path.slice(1)}` : `${base}${path}`;
|
|
54
|
+
}
|
|
55
|
+
function escapeHtmlAttr(value) {
|
|
56
|
+
return value
|
|
57
|
+
.replace(/&/g, "&")
|
|
58
|
+
.replace(/</g, "<")
|
|
59
|
+
.replace(/>/g, ">")
|
|
60
|
+
.replace(/"/g, """);
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Netlify's proxy copies the request's query string onto any redirect whose
|
|
64
|
+
* Location has none, so a bare 302 out of a sign-in callback lands the browser
|
|
65
|
+
* on a page still carrying the one-time `token`, OAuth `code`, or attribution
|
|
66
|
+
* params. Every later bare 302 in the chain inherits the query the same way,
|
|
67
|
+
* so no redirect-only route can shed it. Netlify leaves a 200 alone: a
|
|
68
|
+
* navigation gets an HTML page that replaces itself with the clean Location.
|
|
69
|
+
*/
|
|
70
|
+
export function queryEchoSafeRedirect(event, response) {
|
|
71
|
+
// 307 and 308 replay the request method; a page that navigates would turn a
|
|
72
|
+
// POST into a GET.
|
|
73
|
+
if (![301, 302, 303].includes(response.status))
|
|
74
|
+
return response;
|
|
75
|
+
const location = response.headers.get("location");
|
|
76
|
+
if (!location || !requestHasQuery(event) || !isDocumentNavigation(event)) {
|
|
77
|
+
return response;
|
|
78
|
+
}
|
|
79
|
+
const destination = bareHttpDestination(location);
|
|
80
|
+
if (!destination)
|
|
81
|
+
return response;
|
|
82
|
+
const headers = new Headers();
|
|
83
|
+
for (const [key, value] of response.headers.entries()) {
|
|
84
|
+
const name = key.toLowerCase();
|
|
85
|
+
if (name === "location" ||
|
|
86
|
+
name === "set-cookie" ||
|
|
87
|
+
name === "content-length" ||
|
|
88
|
+
name === "content-type") {
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
headers.append(key, value);
|
|
92
|
+
}
|
|
93
|
+
for (const cookie of response.headers.getSetCookie()) {
|
|
94
|
+
headers.append("set-cookie", cookie);
|
|
95
|
+
}
|
|
96
|
+
headers.set("cache-control", "no-store");
|
|
97
|
+
headers.set("content-type", "text/html; charset=utf-8");
|
|
98
|
+
headers.set("referrer-policy", "no-referrer");
|
|
99
|
+
const href = escapeHtmlAttr(destination);
|
|
100
|
+
const script = JSON.stringify(destination).replace(/</g, "\\u003c");
|
|
101
|
+
return new Response(`<!DOCTYPE html><html><head><meta charset="utf-8"><meta name="referrer" content="no-referrer"><meta http-equiv="refresh" content="0;url=${href}"><script>location.replace(${script})</script></head><body></body></html>`, { status: 200, headers });
|
|
102
|
+
}
|
|
@@ -261,6 +261,10 @@ const FRAMEWORK_SCHEMA_ENSURES = [
|
|
|
261
261
|
"SearchIndex",
|
|
262
262
|
() => import("../search/index-store.js").then((m) => m.ensureSearchIndexTables()),
|
|
263
263
|
],
|
|
264
|
+
[
|
|
265
|
+
"ServicePrincipalPolicies",
|
|
266
|
+
() => import("../org/service-principal-policy.js").then((m) => m.ensureTable()),
|
|
267
|
+
],
|
|
264
268
|
[
|
|
265
269
|
"Settings",
|
|
266
270
|
() => import("../settings/store.js").then((m) => m.ensureTable()),
|
|
@@ -11,6 +11,7 @@ import { getSession, redirectWithStagedCookies, safeReturnPath, } from "./auth.j
|
|
|
11
11
|
import { resolveSecret } from "./credential-provider.js";
|
|
12
12
|
import { canonicalFrameworkPathname } from "./framework-route-prefix.js";
|
|
13
13
|
import { decodeOAuthState, encodeOAuthState, getAppUrl, logOAuthStateDecodeFailure, oauthErrorPage, resolveOAuthRedirectUri, } from "./google-oauth.js";
|
|
14
|
+
import { queryEchoSafeRedirect } from "./query-echo-safe-redirect.js";
|
|
14
15
|
import { runWithRequestContext } from "./request-context.js";
|
|
15
16
|
const SUPPORTED_PROVIDERS = new Set([
|
|
16
17
|
"figma",
|
|
@@ -342,7 +343,7 @@ export async function handleWorkspaceProviderOAuthCallback(event, providerId) {
|
|
|
342
343
|
const returnPath = flow.returnUrl ??
|
|
343
344
|
state.returnUrl ??
|
|
344
345
|
`/settings/integrations?connected=${encodeURIComponent(providerId)}`;
|
|
345
|
-
return redirectWithStagedCookies(event, getAppUrl(event, returnPath));
|
|
346
|
+
return queryEchoSafeRedirect(event, redirectWithStagedCookies(event, getAppUrl(event, returnPath)));
|
|
346
347
|
});
|
|
347
348
|
}
|
|
348
349
|
export function buildWorkspaceProviderAuthorizationUrl(input) {
|
|
@@ -16,6 +16,7 @@ Rules:
|
|
|
16
16
|
- To override the inference on an unusual action (e.g. a `POST` that only reads), pass `readOnly: true` on the action definition.
|
|
17
17
|
- To let a mutating action run concurrently with other same-turn tool calls, pass `parallelSafe: true`. Only do this when the action is internally concurrency-safe and order-independent (for example, it uses an app-level lock or idempotent upsert semantics). Mutating actions remain serialized by default.
|
|
18
18
|
- If a successful call hands control to the user — it renders a question, an intake form, or anything else the turn must wait on — pass `endsTurn: true` and make it the first or only call in the turn. It stops subsequent model steps and later calls, but cannot undo calls emitted before it. Without it a completion guard sees a turn that legitimately paused as one that failed to finish. `endsTurn: true` also hides the action from MCP, WebMCP, and A2A callers by default (`isActionExposedToExternalAgents` in core `action.ts`) — an external host has no in-app chat to receive the answer on. Pass `mcpTool: true` alongside it only when an external caller can complete the same operation without that answer.
|
|
19
|
+
- MCP hosts read tool annotations to decide how carefully to treat a call. Every MCP catalog advertises declared `mcpAnnotations` (`readOnlyHint`, `destructiveHint`, `openWorldHint`, optional `idempotentHint`); otherwise `/mcp` derives only `readOnlyHint` and an approval-based `destructiveHint`. Declare them on any external write that removes, overwrites, or replaces user content — recoverable Trash included — and mark idempotency-key-guarded writes `idempotentHint: true`.
|
|
19
20
|
|
|
20
21
|
Agents do NOT need to call `refresh-screen` after a normal action — it's already handled. `refresh-screen` is only needed when the agent mutates data via a path the framework can't see (e.g. writing to an external system the app mirrors) or when the agent wants to pass a `scope` hint for narrower invalidation.
|
|
21
22
|
|
|
@@ -16,6 +16,7 @@ Rules:
|
|
|
16
16
|
- To override the inference on an unusual action (e.g. a `POST` that only reads), pass `readOnly: true` on the action definition.
|
|
17
17
|
- To let a mutating action run concurrently with other same-turn tool calls, pass `parallelSafe: true`. Only do this when the action is internally concurrency-safe and order-independent (for example, it uses an app-level lock or idempotent upsert semantics). Mutating actions remain serialized by default.
|
|
18
18
|
- If a successful call hands control to the user — it renders a question, an intake form, or anything else the turn must wait on — pass `endsTurn: true` and make it the first or only call in the turn. It stops subsequent model steps and later calls, but cannot undo calls emitted before it. Without it a completion guard sees a turn that legitimately paused as one that failed to finish. `endsTurn: true` also hides the action from MCP, WebMCP, and A2A callers by default (`isActionExposedToExternalAgents` in core `action.ts`) — an external host has no in-app chat to receive the answer on. Pass `mcpTool: true` alongside it only when an external caller can complete the same operation without that answer.
|
|
19
|
+
- MCP hosts read tool annotations to decide how carefully to treat a call. Every MCP catalog advertises declared `mcpAnnotations` (`readOnlyHint`, `destructiveHint`, `openWorldHint`, optional `idempotentHint`); otherwise `/mcp` derives only `readOnlyHint` and an approval-based `destructiveHint`. Declare them on any external write that removes, overwrites, or replaces user content — recoverable Trash included — and mark idempotency-key-guarded writes `idempotentHint: true`.
|
|
19
20
|
|
|
20
21
|
Agents do NOT need to call `refresh-screen` after a normal action — it's already handled. `refresh-screen` is only needed when the agent mutates data via a path the framework can't see (e.g. writing to an external system the app mirrors) or when the agent wants to pass a `scope` hint for narrower invalidation.
|
|
21
22
|
|