@agent-native/core 0.222.0 → 0.222.1-nightly-20261029092708
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/agent/production-agent.d.ts +2 -0
- package/dist/agent/production-agent.js +86 -6
- package/dist/agent/run-store.js +3 -2
- package/dist/app-config/agent.d.ts +9 -0
- package/dist/app-config/agent.js +31 -0
- package/dist/app-config/schema.d.ts +9 -0
- package/dist/app-config/store.js +48 -5
- package/dist/collab/routes.d.ts +1 -1
- package/dist/collab/struct-routes.d.ts +1 -1
- package/dist/file-upload/actions/upload-image.d.ts +4 -4
- package/dist/jobs/run-history.js +3 -2
- package/dist/observability/hosted-model-experiment.d.ts +1 -1
- package/dist/observability/hosted-model-experiment.js +7 -1
- package/dist/observability/question-intake-experiment.d.ts +34 -0
- package/dist/observability/question-intake-experiment.js +114 -0
- package/dist/observability/traces.d.ts +4 -0
- package/dist/observability/traces.js +5 -4
- package/dist/observability/tracing.d.ts +1 -0
- package/dist/observability/tracing.js +14 -4
- package/dist/org/index.d.ts +1 -1
- package/dist/provider-api/actions/custom-provider-registration.d.ts +7 -7
- package/dist/provider-api/actions/provider-api.d.ts +7 -7
- package/dist/server/agent-chat/browser-team-tools.js +2 -2
- package/dist/server/agent-engine-api-key-route.d.ts +3 -3
- package/dist/server/better-auth-instance.js +43 -0
- package/dist/server/builder-branch-files.d.ts +3 -1
- package/dist/server/builder-branch-files.js +74 -33
- package/dist/server/builder-branch-git.d.ts +1 -1
- package/dist/server/builder-branch-git.js +2 -0
- package/dist/server/builder-browser.d.ts +17 -0
- package/dist/server/builder-browser.js +33 -0
- package/dist/server/core-routes-plugin.js +42 -8
- package/dist/server/index.d.ts +1 -1
- package/dist/server/index.js +1 -1
- package/dist/server/request-context.d.ts +8 -0
- package/dist/server/transcribe-voice.d.ts +1 -1
- package/dist/shared/first-run-builder-credits-experiment.d.ts +1 -0
- package/dist/shared/first-run-builder-credits-experiment.js +19 -0
- package/dist/tracking/index.d.ts +1 -1
- package/dist/tracking/index.js +1 -1
- package/dist/tracking/registry.d.ts +7 -0
- package/dist/tracking/registry.js +84 -4
- package/package.json +4 -4
|
@@ -35,6 +35,7 @@ export interface TrackingEventScope {
|
|
|
35
35
|
pending: Set<Promise<void>>;
|
|
36
36
|
}
|
|
37
37
|
export declare function createTrackingEventScope(): TrackingEventScope;
|
|
38
|
+
export declare function registerTrackingWork(pending: Promise<void>, scope?: TrackingEventScope): void;
|
|
38
39
|
export declare function recordTrackingEvent(name: string, properties?: Record<string, unknown>, origin?: TrackingEventOrigin): Promise<void>;
|
|
39
40
|
export declare function queueTrackingEvent(name: string, properties?: Record<string, unknown>, origin?: TrackingEventOrigin, scope?: TrackingEventScope): void;
|
|
40
41
|
export declare function flushTrackingEvents(scope?: TrackingEventScope): Promise<void>;
|
|
@@ -59,6 +59,11 @@ const pendingTrackingEvents = new Set();
|
|
|
59
59
|
export function createTrackingEventScope() {
|
|
60
60
|
return { pending: new Set() };
|
|
61
61
|
}
|
|
62
|
+
export function registerTrackingWork(pending, scope) {
|
|
63
|
+
const pendingEvents = scope?.pending ?? pendingTrackingEvents;
|
|
64
|
+
pendingEvents.add(pending);
|
|
65
|
+
void pending.then(() => pendingEvents.delete(pending), () => pendingEvents.delete(pending));
|
|
66
|
+
}
|
|
62
67
|
function numericDuration(properties) {
|
|
63
68
|
const duration = properties.duration_ms;
|
|
64
69
|
return typeof duration === "number" &&
|
|
@@ -194,12 +199,17 @@ export async function recordTrackingEvent(name, properties = {}, origin) {
|
|
|
194
199
|
}
|
|
195
200
|
export function queueTrackingEvent(name, properties = {}, origin = "server", scope) {
|
|
196
201
|
const pending = recordTrackingEvent(name, properties, origin);
|
|
197
|
-
|
|
198
|
-
pendingEvents.add(pending);
|
|
199
|
-
void pending.then(() => pendingEvents.delete(pending), () => pendingEvents.delete(pending));
|
|
202
|
+
registerTrackingWork(pending, scope);
|
|
200
203
|
}
|
|
201
204
|
export async function flushTrackingEvents(scope) {
|
|
202
|
-
|
|
205
|
+
const pendingEvents = scope?.pending ?? pendingTrackingEvents;
|
|
206
|
+
if (!scope) {
|
|
207
|
+
await Promise.allSettled([...pendingEvents]);
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
while (pendingEvents.size > 0) {
|
|
211
|
+
await Promise.allSettled([...pendingEvents]);
|
|
212
|
+
}
|
|
203
213
|
}
|
|
204
214
|
export async function startAgentSpan(name, attributes = {}, parentSpan = null, startTime) {
|
|
205
215
|
const runtime = await resolveRuntime();
|
package/dist/org/index.d.ts
CHANGED
|
@@ -180,10 +180,10 @@ export declare const getMyOrgHandler: import("h3").EventHandlerWithFetch<import(
|
|
|
180
180
|
soloDeploymentAdmin: boolean | undefined;
|
|
181
181
|
}>>;
|
|
182
182
|
export declare const createOrgHandler: import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<{
|
|
183
|
-
id?: undefined;
|
|
184
183
|
name?: undefined;
|
|
185
184
|
success: boolean;
|
|
186
185
|
role?: undefined;
|
|
186
|
+
id?: undefined;
|
|
187
187
|
} | {
|
|
188
188
|
success?: undefined;
|
|
189
189
|
id: string;
|
|
@@ -76,9 +76,9 @@ export declare function createCustomProviderRegistrationAction<TSchema extends Z
|
|
|
76
76
|
}>>;
|
|
77
77
|
}, z.core.$strip>>, {
|
|
78
78
|
id?: undefined;
|
|
79
|
+
provider?: undefined;
|
|
79
80
|
deleted?: undefined;
|
|
80
81
|
message?: undefined;
|
|
81
|
-
found?: undefined;
|
|
82
82
|
providers: {
|
|
83
83
|
id: string;
|
|
84
84
|
label: string;
|
|
@@ -89,7 +89,7 @@ export declare function createCustomProviderRegistrationAction<TSchema extends Z
|
|
|
89
89
|
updatedAt: number;
|
|
90
90
|
}[];
|
|
91
91
|
count: number;
|
|
92
|
-
|
|
92
|
+
found?: undefined;
|
|
93
93
|
registered?: undefined;
|
|
94
94
|
label?: undefined;
|
|
95
95
|
} | {
|
|
@@ -103,31 +103,31 @@ export declare function createCustomProviderRegistrationAction<TSchema extends Z
|
|
|
103
103
|
registered?: undefined;
|
|
104
104
|
label?: undefined;
|
|
105
105
|
} | {
|
|
106
|
+
provider?: undefined;
|
|
106
107
|
count?: undefined;
|
|
107
108
|
deleted?: undefined;
|
|
108
109
|
message?: undefined;
|
|
109
110
|
providers?: undefined;
|
|
110
|
-
provider?: undefined;
|
|
111
111
|
found: boolean;
|
|
112
112
|
id: string;
|
|
113
113
|
registered?: undefined;
|
|
114
114
|
label?: undefined;
|
|
115
115
|
} | {
|
|
116
|
+
provider?: undefined;
|
|
116
117
|
count?: undefined;
|
|
117
118
|
message?: undefined;
|
|
118
|
-
found?: undefined;
|
|
119
119
|
providers?: undefined;
|
|
120
|
-
|
|
120
|
+
found?: undefined;
|
|
121
121
|
deleted: boolean;
|
|
122
122
|
id: string;
|
|
123
123
|
registered?: undefined;
|
|
124
124
|
label?: undefined;
|
|
125
125
|
} | {
|
|
126
|
+
provider?: undefined;
|
|
126
127
|
count?: undefined;
|
|
127
128
|
deleted?: undefined;
|
|
128
|
-
found?: undefined;
|
|
129
129
|
providers?: undefined;
|
|
130
|
-
|
|
130
|
+
found?: undefined;
|
|
131
131
|
registered: boolean;
|
|
132
132
|
id: string;
|
|
133
133
|
label: string;
|
|
@@ -316,9 +316,9 @@ export declare function createProviderApiActions(runtime: Pick<ProviderApiRuntim
|
|
|
316
316
|
notes?: string | undefined;
|
|
317
317
|
scope?: "org" | "user" | undefined;
|
|
318
318
|
}, {
|
|
319
|
+
provider?: undefined;
|
|
319
320
|
id?: undefined;
|
|
320
321
|
message?: undefined;
|
|
321
|
-
found?: undefined;
|
|
322
322
|
providers: {
|
|
323
323
|
id: string;
|
|
324
324
|
label: string;
|
|
@@ -329,7 +329,7 @@ export declare function createProviderApiActions(runtime: Pick<ProviderApiRuntim
|
|
|
329
329
|
updatedAt: number;
|
|
330
330
|
}[];
|
|
331
331
|
count: number;
|
|
332
|
-
|
|
332
|
+
found?: undefined;
|
|
333
333
|
deleted?: undefined;
|
|
334
334
|
registered?: undefined;
|
|
335
335
|
label?: undefined;
|
|
@@ -344,30 +344,30 @@ export declare function createProviderApiActions(runtime: Pick<ProviderApiRuntim
|
|
|
344
344
|
registered?: undefined;
|
|
345
345
|
label?: undefined;
|
|
346
346
|
} | {
|
|
347
|
+
provider?: undefined;
|
|
347
348
|
count?: undefined;
|
|
348
349
|
message?: undefined;
|
|
349
350
|
providers?: undefined;
|
|
350
|
-
provider?: undefined;
|
|
351
351
|
found: boolean;
|
|
352
352
|
id: string;
|
|
353
353
|
deleted?: undefined;
|
|
354
354
|
registered?: undefined;
|
|
355
355
|
label?: undefined;
|
|
356
356
|
} | {
|
|
357
|
+
provider?: undefined;
|
|
357
358
|
count?: undefined;
|
|
358
359
|
message?: undefined;
|
|
359
|
-
found?: undefined;
|
|
360
360
|
providers?: undefined;
|
|
361
|
-
|
|
361
|
+
found?: undefined;
|
|
362
362
|
deleted: boolean;
|
|
363
363
|
id: string;
|
|
364
364
|
registered?: undefined;
|
|
365
365
|
label?: undefined;
|
|
366
366
|
} | {
|
|
367
|
+
provider?: undefined;
|
|
367
368
|
count?: undefined;
|
|
368
|
-
found?: undefined;
|
|
369
369
|
providers?: undefined;
|
|
370
|
-
|
|
370
|
+
found?: undefined;
|
|
371
371
|
deleted?: undefined;
|
|
372
372
|
registered: boolean;
|
|
373
373
|
id: string;
|
|
@@ -5,7 +5,7 @@ import { fail, isActionContractError, } from "../../action.js";
|
|
|
5
5
|
import { getActiveFileUploadProviderForRequest } from "../../file-upload/registry.js";
|
|
6
6
|
import { areBuiltinMcpCapabilitiesSupported, setBuiltinMcpCapabilityEnabled, } from "../../mcp-client/index.js";
|
|
7
7
|
import { frameworkMcpTools } from "../../mcp/framework-tools.js";
|
|
8
|
-
import { getBuilderBrowserConnectUrlForOwner,
|
|
8
|
+
import { getBuilderBrowserConnectUrlForOwner, resolveBuilderHandoffProjectId, } from "../builder-browser.js";
|
|
9
9
|
import { getRequestOrgId, getRequestUserEmail } from "../request-context.js";
|
|
10
10
|
import { getMcpManagerForCurrentRequest } from "./mcp-glue.js";
|
|
11
11
|
const MAX_EXTENSION_PROMOTION_CONTENT_CHARS = 200_000;
|
|
@@ -199,7 +199,7 @@ export function createBuilderBrowserTool(deps) {
|
|
|
199
199
|
// re-authorizing renders unconfigured, so the card offers to connect.
|
|
200
200
|
const configured = (await builderConnectionForRequest(creds, BUILDER_OAUTH_SCOPE))
|
|
201
201
|
.status === "connected";
|
|
202
|
-
const branchProjectId = await
|
|
202
|
+
const branchProjectId = await resolveBuilderHandoffProjectId();
|
|
203
203
|
let prompt = typeof args?.prompt === "string" ? args.prompt : "";
|
|
204
204
|
const extensionId = typeof args?.extensionId === "string" ? args.extensionId.trim() : "";
|
|
205
205
|
let promotion;
|
|
@@ -54,21 +54,21 @@ export declare function resolveAgentEngineApiKeyWriteTarget(event: H3Event, scop
|
|
|
54
54
|
error: string;
|
|
55
55
|
}>;
|
|
56
56
|
export declare function createAgentEngineApiKeyHandler(): import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<{
|
|
57
|
+
error: string;
|
|
57
58
|
errorCode?: undefined;
|
|
58
59
|
code?: undefined;
|
|
59
|
-
error: string;
|
|
60
60
|
} | {
|
|
61
|
-
code?: undefined;
|
|
62
61
|
error: string;
|
|
63
62
|
errorCode: string;
|
|
63
|
+
code?: undefined;
|
|
64
64
|
} | {
|
|
65
65
|
errorCode?: undefined;
|
|
66
66
|
error: string;
|
|
67
67
|
code: ProviderKeyCheckCode;
|
|
68
68
|
} | {
|
|
69
|
+
error?: undefined;
|
|
69
70
|
errorCode?: undefined;
|
|
70
71
|
code?: undefined;
|
|
71
|
-
error?: undefined;
|
|
72
72
|
ok: boolean;
|
|
73
73
|
key: string;
|
|
74
74
|
baseUrlKey?: string | undefined;
|
|
@@ -26,6 +26,7 @@ import { CORE_CHANGE_EMAIL_CONFIRMATION_EMAIL_ID, CORE_CHANGE_EMAIL_VERIFICATION
|
|
|
26
26
|
import { renderTransactionalEmail } from "../email-catalog/templates.js";
|
|
27
27
|
import { executeIdentityRekey, rekeyIdentity, resumePendingIdentityRekeys, verifiedEmailChangeFromToken, } from "../identity/rekey.js";
|
|
28
28
|
import { saveOAuthTokens } from "../oauth-tokens/store.js";
|
|
29
|
+
import { assignQuestionIntakeExperimentOnSignup } from "../observability/question-intake-experiment.js";
|
|
29
30
|
import { acceptPendingInvitationsForEmail } from "../org/accept-pending.js";
|
|
30
31
|
import { getAuthEmailForUserId, getRequiredAuthProviderForEmail, } from "../org/auth-policy.js";
|
|
31
32
|
import { autoJoinDomainMatchingOrgs } from "../org/auto-join-domain.js";
|
|
@@ -1849,6 +1850,48 @@ async function createBetterAuthInstance(config) {
|
|
|
1849
1850
|
if (!email)
|
|
1850
1851
|
return;
|
|
1851
1852
|
await emitSignupEventForCreatedUser(user, context);
|
|
1853
|
+
const actingUserId = context?.context?.session?.user?.id;
|
|
1854
|
+
if (user.id && (!actingUserId || actingUserId === user.id)) {
|
|
1855
|
+
let experimentId;
|
|
1856
|
+
try {
|
|
1857
|
+
const questionIntakeConfig = getAppConfig().agent.questionIntake;
|
|
1858
|
+
experimentId = questionIntakeConfig?.experimentId;
|
|
1859
|
+
// raw-sql-ok: the assignment store binds the authenticated user ID as a query argument.
|
|
1860
|
+
await assignQuestionIntakeExperimentOnSignup(questionIntakeConfig, user.id);
|
|
1861
|
+
}
|
|
1862
|
+
catch (error) {
|
|
1863
|
+
console.error("[auth] question-intake assignment failed after retry", {
|
|
1864
|
+
code: "question_intake_assignment_failed",
|
|
1865
|
+
experimentId,
|
|
1866
|
+
});
|
|
1867
|
+
if (experimentId) {
|
|
1868
|
+
try {
|
|
1869
|
+
track("agent_experiment_assignment_failed", {
|
|
1870
|
+
experiment_id: experimentId,
|
|
1871
|
+
failure_stage: "signup",
|
|
1872
|
+
error_code: "question_intake_assignment_failed",
|
|
1873
|
+
}, { authUserId: user.id, userId: email });
|
|
1874
|
+
}
|
|
1875
|
+
catch {
|
|
1876
|
+
console.error("[auth] failed to track question-intake assignment failure", { code: "question_intake_assignment_tracking_failed" });
|
|
1877
|
+
}
|
|
1878
|
+
}
|
|
1879
|
+
try {
|
|
1880
|
+
const { captureError } = await import("./capture-error.js");
|
|
1881
|
+
captureError(error, {
|
|
1882
|
+
route: "auth.signup",
|
|
1883
|
+
tags: {
|
|
1884
|
+
failureClass: "question-intake-assignment",
|
|
1885
|
+
errorCode: "question_intake_assignment_failed",
|
|
1886
|
+
},
|
|
1887
|
+
extra: { experimentId },
|
|
1888
|
+
});
|
|
1889
|
+
}
|
|
1890
|
+
catch {
|
|
1891
|
+
console.error("[auth] failed to report question-intake assignment failure", { code: "question_intake_assignment_reporting_failed" });
|
|
1892
|
+
}
|
|
1893
|
+
}
|
|
1894
|
+
}
|
|
1852
1895
|
if (user.emailVerified !== true)
|
|
1853
1896
|
return;
|
|
1854
1897
|
try {
|
|
@@ -1,6 +1,8 @@
|
|
|
1
|
+
export type BuilderBranchReadbackStage = "status_request" | "status_shape" | "status_binding" | "status_currentness" | "post_read_status_request" | "post_read_status_shape" | "post_read_status_binding" | "post_read_status_currentness" | "download_url_request" | "download_url_shape" | "download_url_binding" | "bundle_download" | "bundle_read" | "bundle_digest" | "bundle_parse" | "target_subtree_missing" | "post_read_currentness";
|
|
1
2
|
export declare class BuilderBranchFilesError extends Error {
|
|
2
3
|
readonly code: "invalid_request" | "not_connected" | "forbidden" | "unavailable" | "unverified" | "integrity_failed";
|
|
3
|
-
|
|
4
|
+
readonly readbackStage?: BuilderBranchReadbackStage | undefined;
|
|
5
|
+
constructor(code: "invalid_request" | "not_connected" | "forbidden" | "unavailable" | "unverified" | "integrity_failed", message: string, readbackStage?: BuilderBranchReadbackStage | undefined);
|
|
4
6
|
}
|
|
5
7
|
type BranchRef = {
|
|
6
8
|
projectId: string;
|
|
@@ -10,9 +10,11 @@ const MAX_BUNDLE_BYTES = 32 * 1024 * 1024;
|
|
|
10
10
|
const API_TIMEOUT_MS = 30_000;
|
|
11
11
|
export class BuilderBranchFilesError extends Error {
|
|
12
12
|
code;
|
|
13
|
-
|
|
13
|
+
readbackStage;
|
|
14
|
+
constructor(code, message, readbackStage) {
|
|
14
15
|
super(message);
|
|
15
16
|
this.code = code;
|
|
17
|
+
this.readbackStage = readbackStage;
|
|
16
18
|
this.name = "BuilderBranchFilesError";
|
|
17
19
|
}
|
|
18
20
|
}
|
|
@@ -56,6 +58,20 @@ const downloadSchema = z.object({
|
|
|
56
58
|
partial: z.literal(false),
|
|
57
59
|
expiresAt: z.string(),
|
|
58
60
|
});
|
|
61
|
+
const statusFailureStages = {
|
|
62
|
+
before_read: {
|
|
63
|
+
request: "status_request",
|
|
64
|
+
shape: "status_shape",
|
|
65
|
+
binding: "status_binding",
|
|
66
|
+
currentness: "status_currentness",
|
|
67
|
+
},
|
|
68
|
+
after_read: {
|
|
69
|
+
request: "post_read_status_request",
|
|
70
|
+
shape: "post_read_status_shape",
|
|
71
|
+
binding: "post_read_status_binding",
|
|
72
|
+
currentness: "post_read_status_currentness",
|
|
73
|
+
},
|
|
74
|
+
};
|
|
59
75
|
function backupIdentityFingerprint(backup) {
|
|
60
76
|
return JSON.stringify([
|
|
61
77
|
backup.projectId,
|
|
@@ -67,10 +83,10 @@ function backupIdentityFingerprint(backup) {
|
|
|
67
83
|
backup.size,
|
|
68
84
|
]);
|
|
69
85
|
}
|
|
70
|
-
function unverified() {
|
|
71
|
-
throw new BuilderBranchFilesError("unverified", "Builder has no complete, current, identity-bound branch backup to verify.");
|
|
86
|
+
function unverified(stage) {
|
|
87
|
+
throw new BuilderBranchFilesError("unverified", "Builder has no complete, current, identity-bound branch backup to verify.", stage);
|
|
72
88
|
}
|
|
73
|
-
function primaryCommit(snapshot) {
|
|
89
|
+
function primaryCommit(snapshot, failureStage) {
|
|
74
90
|
if (SHA.test(snapshot))
|
|
75
91
|
return snapshot;
|
|
76
92
|
const pairs = snapshot.split(",");
|
|
@@ -78,25 +94,25 @@ function primaryCommit(snapshot) {
|
|
|
78
94
|
for (const pair of pairs) {
|
|
79
95
|
const separator = pair.indexOf(":");
|
|
80
96
|
if (separator < 1)
|
|
81
|
-
unverified();
|
|
97
|
+
unverified(failureStage);
|
|
82
98
|
let folder;
|
|
83
99
|
try {
|
|
84
100
|
folder = decodeURIComponent(pair.slice(0, separator));
|
|
85
101
|
}
|
|
86
102
|
catch {
|
|
87
|
-
return unverified();
|
|
103
|
+
return unverified(failureStage);
|
|
88
104
|
}
|
|
89
105
|
const commit = pair.slice(separator + 1);
|
|
90
106
|
if (!SHA.test(commit))
|
|
91
|
-
unverified();
|
|
107
|
+
unverified(failureStage);
|
|
92
108
|
if (folder === "code")
|
|
93
109
|
entries.push(commit);
|
|
94
110
|
}
|
|
95
111
|
return entries.length === 1 ? entries[0] : undefined;
|
|
96
112
|
}
|
|
97
|
-
async function readBytes(response, maxBytes) {
|
|
113
|
+
async function readBytes(response, maxBytes, stage) {
|
|
98
114
|
if (!response.body) {
|
|
99
|
-
throw new BuilderBranchFilesError("unavailable", "Builder response has no body.");
|
|
115
|
+
throw new BuilderBranchFilesError("unavailable", "Builder response has no body.", stage);
|
|
100
116
|
}
|
|
101
117
|
const reader = response.body.getReader();
|
|
102
118
|
const chunks = [];
|
|
@@ -108,7 +124,7 @@ async function readBytes(response, maxBytes) {
|
|
|
108
124
|
break;
|
|
109
125
|
size += part.value.byteLength;
|
|
110
126
|
if (size > maxBytes) {
|
|
111
|
-
throw new BuilderBranchFilesError("integrity_failed", "Builder readback exceeds its byte limit.");
|
|
127
|
+
throw new BuilderBranchFilesError("integrity_failed", "Builder readback exceeds its byte limit.", stage);
|
|
112
128
|
}
|
|
113
129
|
chunks.push(part.value);
|
|
114
130
|
}
|
|
@@ -116,7 +132,7 @@ async function readBytes(response, maxBytes) {
|
|
|
116
132
|
catch (error) {
|
|
117
133
|
if (error instanceof BuilderBranchFilesError)
|
|
118
134
|
throw error;
|
|
119
|
-
throw new BuilderBranchFilesError("unavailable", "Builder response could not be read completely.");
|
|
135
|
+
throw new BuilderBranchFilesError("unavailable", "Builder response could not be read completely.", stage);
|
|
120
136
|
}
|
|
121
137
|
finally {
|
|
122
138
|
await reader.cancel().catch(() => { });
|
|
@@ -190,31 +206,54 @@ export async function readBuilderBranchFiles(input) {
|
|
|
190
206
|
throw new BuilderBranchFilesError("unavailable", "Builder returned an invalid readback response.");
|
|
191
207
|
}
|
|
192
208
|
}
|
|
193
|
-
async function readStatus() {
|
|
194
|
-
|
|
209
|
+
async function readStatus(phase) {
|
|
210
|
+
let rawStatus;
|
|
211
|
+
try {
|
|
212
|
+
rawStatus = await request("/projects/branch/status");
|
|
213
|
+
}
|
|
214
|
+
catch (error) {
|
|
215
|
+
if (error instanceof BuilderBranchFilesError) {
|
|
216
|
+
throw new BuilderBranchFilesError(error.code, error.message, statusFailureStages[phase].request);
|
|
217
|
+
}
|
|
218
|
+
throw error;
|
|
219
|
+
}
|
|
220
|
+
const result = statusSchema.safeParse(rawStatus);
|
|
195
221
|
if (!result.success)
|
|
196
|
-
return unverified();
|
|
222
|
+
return unverified(statusFailureStages[phase].shape);
|
|
197
223
|
const { branch, backupStatuses } = result.data;
|
|
198
224
|
const backup = backupStatuses.code.backup;
|
|
199
225
|
if (branch.projectId !== input.projectId ||
|
|
200
226
|
branch.name !== input.branchName ||
|
|
201
227
|
backup.projectId !== input.projectId ||
|
|
202
228
|
backup.branchName !== input.branchName ||
|
|
203
|
-
branch.ownerId !== backup.ownerId
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
229
|
+
branch.ownerId !== backup.ownerId) {
|
|
230
|
+
return unverified(statusFailureStages[phase].binding);
|
|
231
|
+
}
|
|
232
|
+
const branchCommit = primaryCommit(branch.lastCommitHash, statusFailureStages[phase].currentness);
|
|
233
|
+
if (backup.at < branch.lastActivityAt ||
|
|
234
|
+
branchCommit !== backup.lastCommitHash)
|
|
235
|
+
return unverified(statusFailureStages[phase].currentness);
|
|
207
236
|
return backup;
|
|
208
237
|
}
|
|
209
238
|
// Export can fall back to an older backup after a refresh error. Only accept
|
|
210
239
|
// the bytes whose digest and commit still match the branch's recorded state.
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
240
|
+
let rawDownload;
|
|
241
|
+
try {
|
|
242
|
+
rawDownload = await request("/projects/branch/bundle-download-url", {
|
|
243
|
+
projectId: input.projectId,
|
|
244
|
+
branchName: input.branchName,
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
catch (error) {
|
|
248
|
+
if (error instanceof BuilderBranchFilesError) {
|
|
249
|
+
throw new BuilderBranchFilesError(error.code, error.message, "download_url_request");
|
|
250
|
+
}
|
|
251
|
+
throw error;
|
|
252
|
+
}
|
|
253
|
+
const download = downloadSchema.safeParse(rawDownload);
|
|
215
254
|
if (!download.success)
|
|
216
|
-
return unverified();
|
|
217
|
-
const before = await readStatus();
|
|
255
|
+
return unverified("download_url_shape");
|
|
256
|
+
const before = await readStatus("before_read");
|
|
218
257
|
const beforeFingerprint = backupIdentityFingerprint(before);
|
|
219
258
|
const filePath = before.ownerId +
|
|
220
259
|
"/" +
|
|
@@ -229,7 +268,7 @@ export async function readBuilderBranchFiles(input) {
|
|
|
229
268
|
storagePath = decodeURIComponent(url.pathname);
|
|
230
269
|
}
|
|
231
270
|
catch {
|
|
232
|
-
return unverified();
|
|
271
|
+
return unverified("download_url_binding");
|
|
233
272
|
}
|
|
234
273
|
if (download.data.filePath !== filePath ||
|
|
235
274
|
url.origin !== "https://storage.googleapis.com" ||
|
|
@@ -240,7 +279,7 @@ export async function readBuilderBranchFiles(input) {
|
|
|
240
279
|
!Number.isFinite(expiresAt) ||
|
|
241
280
|
expiresAt <= Date.now() ||
|
|
242
281
|
expiresAt > Date.now() + 25 * 60 * 60 * 1000)
|
|
243
|
-
return unverified();
|
|
282
|
+
return unverified("download_url_binding");
|
|
244
283
|
let response;
|
|
245
284
|
try {
|
|
246
285
|
response = await ssrfSafeFetch(url.toString(), { signal: AbortSignal.timeout(API_TIMEOUT_MS) }, {
|
|
@@ -254,16 +293,16 @@ export async function readBuilderBranchFiles(input) {
|
|
|
254
293
|
});
|
|
255
294
|
}
|
|
256
295
|
catch {
|
|
257
|
-
throw new BuilderBranchFilesError("unavailable", "Builder branch bundle could not be downloaded.");
|
|
296
|
+
throw new BuilderBranchFilesError("unavailable", "Builder branch bundle could not be downloaded.", "bundle_download");
|
|
258
297
|
}
|
|
259
298
|
if (!response.ok) {
|
|
260
299
|
await response.body?.cancel().catch(() => { });
|
|
261
|
-
throw new BuilderBranchFilesError("unavailable", "Builder branch bundle is unavailable.");
|
|
300
|
+
throw new BuilderBranchFilesError("unavailable", "Builder branch bundle is unavailable.", "bundle_download");
|
|
262
301
|
}
|
|
263
|
-
const bundle = await readBytes(response, MAX_BUNDLE_BYTES);
|
|
302
|
+
const bundle = await readBytes(response, MAX_BUNDLE_BYTES, "bundle_read");
|
|
264
303
|
if (bundle.byteLength !== before.size ||
|
|
265
304
|
createHash("md5").update(bundle).digest("base64") !== before.contentMd5) {
|
|
266
|
-
throw new BuilderBranchFilesError("integrity_failed", "Builder branch bundle failed its recorded byte integrity check.");
|
|
305
|
+
throw new BuilderBranchFilesError("integrity_failed", "Builder branch bundle failed its recorded byte integrity check.", "bundle_digest");
|
|
267
306
|
}
|
|
268
307
|
let snapshot;
|
|
269
308
|
try {
|
|
@@ -276,11 +315,13 @@ export async function readBuilderBranchFiles(input) {
|
|
|
276
315
|
catch (error) {
|
|
277
316
|
throw new BuilderBranchFilesError(error instanceof BuilderGitBundleError && error.code === "unavailable"
|
|
278
317
|
? "unavailable"
|
|
279
|
-
: "integrity_failed", "Builder branch files could not be verified from the bundle."
|
|
318
|
+
: "integrity_failed", "Builder branch files could not be verified from the bundle.", error instanceof BuilderGitBundleError && error.code === "target_missing"
|
|
319
|
+
? "target_subtree_missing"
|
|
320
|
+
: "bundle_parse");
|
|
280
321
|
}
|
|
281
|
-
const after = await readStatus();
|
|
322
|
+
const after = await readStatus("after_read");
|
|
282
323
|
if (backupIdentityFingerprint(after) !== beforeFingerprint)
|
|
283
|
-
return unverified();
|
|
324
|
+
return unverified("post_read_currentness");
|
|
284
325
|
return {
|
|
285
326
|
projectId: input.projectId,
|
|
286
327
|
branchName: input.branchName,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export type BuilderGitBundleErrorCode = "invalid_request" | "integrity_failed" | "unavailable";
|
|
1
|
+
export type BuilderGitBundleErrorCode = "invalid_request" | "integrity_failed" | "target_missing" | "unavailable";
|
|
2
2
|
export declare class BuilderGitBundleError extends Error {
|
|
3
3
|
readonly code: BuilderGitBundleErrorCode;
|
|
4
4
|
constructor(code: BuilderGitBundleErrorCode);
|
|
@@ -100,6 +100,8 @@ async function findTargetTree(gitApi, root, gitdir, rootTreeOid, appPath, cache)
|
|
|
100
100
|
const current = await readTree(gitApi, root, gitdir, treeOid, MAX_TREE_ENTRIES - traversedEntries, cache);
|
|
101
101
|
traversedEntries += current.entryCount;
|
|
102
102
|
const matching = current.tree.tree.filter((entry) => entry.path === segment);
|
|
103
|
+
if (matching.length === 0)
|
|
104
|
+
fail("target_missing");
|
|
103
105
|
if (matching.length !== 1)
|
|
104
106
|
fail("integrity_failed");
|
|
105
107
|
const entry = matching.at(0);
|
|
@@ -192,7 +192,24 @@ export declare function getBuilderAppHost(): string;
|
|
|
192
192
|
export declare function getBuilderApiHost(): string;
|
|
193
193
|
export declare function getBuilderBranchProjectId(): string;
|
|
194
194
|
export declare function isBuilderBranchingEnabled(): boolean;
|
|
195
|
+
type BuilderBranchProjectProvider = () => string | undefined | Promise<string | undefined>;
|
|
196
|
+
/**
|
|
197
|
+
* A host that binds each view to a Builder project (Design, mounted inside
|
|
198
|
+
* Builder) names that project here, so a code-change handoff targets the
|
|
199
|
+
* project the user is looking at rather than one deployment-wide setting.
|
|
200
|
+
* Return `undefined` when the request has no bound project; the usual
|
|
201
|
+
* Dispatch, env and secret lookups then apply. See
|
|
202
|
+
* `resolveBuilderHandoffProjectId`.
|
|
203
|
+
*/
|
|
204
|
+
export declare function registerBuilderBranchProject(provider: BuilderBranchProjectProvider | null): void;
|
|
195
205
|
export declare function resolveBuilderBranchProjectId(): Promise<string>;
|
|
206
|
+
/**
|
|
207
|
+
* The project a code-change handoff targets: the one the host binds to the
|
|
208
|
+
* current view when it names one, otherwise the deployment's configured
|
|
209
|
+
* project. Only the handoff uses this. Realtime channels and Builder status
|
|
210
|
+
* keep the deployment's project, so a view bound elsewhere cannot move them.
|
|
211
|
+
*/
|
|
212
|
+
export declare function resolveBuilderHandoffProjectId(): Promise<string>;
|
|
196
213
|
export declare function resolveIsBuilderBranchingEnabled(): Promise<boolean>;
|
|
197
214
|
export declare const BUILDER_OPENER_PARAM = "_an_opener";
|
|
198
215
|
/**
|
|
@@ -766,6 +766,18 @@ async function resolveDispatchBuilderProjectId() {
|
|
|
766
766
|
return { status: "unreadable" };
|
|
767
767
|
}
|
|
768
768
|
}
|
|
769
|
+
let builderBranchProjectProvider = null;
|
|
770
|
+
/**
|
|
771
|
+
* A host that binds each view to a Builder project (Design, mounted inside
|
|
772
|
+
* Builder) names that project here, so a code-change handoff targets the
|
|
773
|
+
* project the user is looking at rather than one deployment-wide setting.
|
|
774
|
+
* Return `undefined` when the request has no bound project; the usual
|
|
775
|
+
* Dispatch, env and secret lookups then apply. See
|
|
776
|
+
* `resolveBuilderHandoffProjectId`.
|
|
777
|
+
*/
|
|
778
|
+
export function registerBuilderBranchProject(provider) {
|
|
779
|
+
builderBranchProjectProvider = provider;
|
|
780
|
+
}
|
|
769
781
|
export async function resolveBuilderBranchProjectId() {
|
|
770
782
|
const dispatchProject = await resolveDispatchBuilderProjectId();
|
|
771
783
|
if (dispatchProject.status === "unreadable")
|
|
@@ -792,6 +804,27 @@ export async function resolveBuilderBranchProjectId() {
|
|
|
792
804
|
}
|
|
793
805
|
return "";
|
|
794
806
|
}
|
|
807
|
+
/**
|
|
808
|
+
* The project a code-change handoff targets: the one the host binds to the
|
|
809
|
+
* current view when it names one, otherwise the deployment's configured
|
|
810
|
+
* project. Only the handoff uses this. Realtime channels and Builder status
|
|
811
|
+
* keep the deployment's project, so a view bound elsewhere cannot move them.
|
|
812
|
+
*/
|
|
813
|
+
export async function resolveBuilderHandoffProjectId() {
|
|
814
|
+
if (builderBranchProjectProvider) {
|
|
815
|
+
try {
|
|
816
|
+
const hostProjectId = (await builderBranchProjectProvider())?.trim();
|
|
817
|
+
if (hostProjectId)
|
|
818
|
+
return hostProjectId;
|
|
819
|
+
}
|
|
820
|
+
catch {
|
|
821
|
+
// coercion-ok: fails closed like an unreadable Dispatch setting, so no
|
|
822
|
+
// other project stands in for the one the host could not name.
|
|
823
|
+
return "";
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
return resolveBuilderBranchProjectId();
|
|
827
|
+
}
|
|
795
828
|
export async function resolveIsBuilderBranchingEnabled() {
|
|
796
829
|
return !!(await resolveBuilderBranchProjectId());
|
|
797
830
|
}
|