@agent-native/core 0.154.0 → 0.154.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/corpus/README.md +1 -1
- package/corpus/templates/analytics/changelog/2026-08-12-feature-flags-can-be-managed-across-apps-whose-workspace-rec.md +6 -0
- package/corpus/templates/analytics/changelog/2026-08-12-kept-feature-flag-details-readable-by-stacking-rollout-contr.md +6 -0
- package/corpus/templates/analytics/scripts/preview-dashboard-report.ts +16 -5
- package/corpus/templates/analytics/server/lib/workspace-feature-flags.ts +98 -31
- package/corpus/templates/content/actions/resolve-content-landing.ts +162 -32
- package/corpus/templates/content/shared/content-landing.ts +5 -0
- package/corpus/templates/dispatch/changelog/2026-08-12-dispatch-now-appears-in-authorized-workspace-app-directories.md +6 -0
- package/corpus/templates/dispatch/server/lib/org-apps-directory.ts +3 -4
- package/corpus/templates/dispatch/server/plugins/org-apps-directory.ts +15 -3
- package/corpus/templates/slides/actions/patch-deck.ts +12 -3
- package/corpus/templates/slides/app/components/editor/EditorActionCluster.tsx +73 -1
- package/corpus/templates/slides/app/components/editor/EditorSidebar.tsx +6 -1
- package/corpus/templates/slides/app/components/editor/EditorToolbar.tsx +6 -0
- package/corpus/templates/slides/app/components/editor/NewDeckReferenceStep.tsx +15 -0
- package/corpus/templates/slides/app/context/DeckContext.tsx +19 -31
- package/corpus/templates/slides/app/hooks/use-navigation-state.ts +15 -16
- package/corpus/templates/slides/app/pages/DeckEditor.tsx +9 -0
- package/corpus/templates/slides/changelog/2026-08-13-clicking-a-slide-thumbnail-now-focuses-it-so-the-slide-shortc.md +6 -0
- package/corpus/templates/slides/changelog/2026-08-13-duplicating-or-restoring-a-slide-now-keeps-its-transition-ani.md +6 -0
- package/corpus/templates/slides/changelog/2026-08-13-opening-presentation-mode-from-the-agent-now-starts-on-the-re.md +6 -0
- package/corpus/templates/slides/changelog/2026-08-13-slide-transitions-can-be-set-from-a-visible-control-in-the-sl.md +6 -0
- package/dist/a2a/audience.d.ts +2 -0
- package/dist/a2a/audience.js +23 -0
- package/dist/a2a/client.d.ts +4 -0
- package/dist/a2a/client.js +32 -14
- package/dist/a2a/server.d.ts +9 -4
- package/dist/a2a/server.js +52 -25
- package/dist/a2a/types.d.ts +4 -4
- package/dist/a2a-claims.d.ts +1 -0
- package/dist/a2a-claims.js +3 -1
- package/dist/client/feature-flags/FeatureFlagsPanel.js +2 -2
- package/dist/collab/awareness.d.ts +2 -2
- package/dist/collab/routes.d.ts +1 -1
- package/dist/feature-flags/a2a-action-route.js +10 -1
- package/dist/feature-flags/actions/set-feature-flag.d.ts +2 -1
- package/dist/feature-flags/actions/set-feature-flag.js +9 -2
- package/dist/mcp/org-directory.d.ts +3 -0
- package/dist/mcp/org-directory.js +15 -5
- package/dist/notifications/routes.d.ts +1 -1
- package/dist/observability/routes.d.ts +6 -6
- package/dist/provider-api/actions/custom-provider-registration.d.ts +2 -2
- package/dist/resources/handlers.d.ts +1 -1
- package/dist/secrets/routes.d.ts +3 -3
- package/dist/server/action-routes.js +10 -2
- package/dist/server/agent-capabilities.js +0 -0
- package/dist/server/agent-chat/action-filters-a2a.d.ts +14 -13
- package/dist/server/agent-chat/action-filters-a2a.js +59 -23
- package/dist/server/agent-discovery.d.ts +6 -2
- package/dist/server/agent-discovery.js +16 -9
- package/dist/server/agent-engine-api-key-route.d.ts +1 -1
- package/dist/server/identity-sso-store.d.ts +5 -4
- package/dist/server/identity-sso-store.js +5 -4
- package/package.json +1 -1
package/corpus/README.md
CHANGED
|
@@ -19,7 +19,8 @@
|
|
|
19
19
|
*/
|
|
20
20
|
|
|
21
21
|
import { mkdir, writeFile } from "node:fs/promises";
|
|
22
|
-
import { join } from "node:path";
|
|
22
|
+
import path, { join } from "node:path";
|
|
23
|
+
import { pathToFileURL } from "node:url";
|
|
23
24
|
|
|
24
25
|
import { runWithRequestContext } from "@agent-native/core/server/request-context";
|
|
25
26
|
import { eq } from "drizzle-orm";
|
|
@@ -215,7 +216,17 @@ async function renderPreview(
|
|
|
215
216
|
if (rendered.degradedPanelIds.length) process.exitCode = 1;
|
|
216
217
|
}
|
|
217
218
|
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
219
|
+
function isDirectRun(): boolean {
|
|
220
|
+
const entrypoint = process.argv[1];
|
|
221
|
+
return Boolean(
|
|
222
|
+
entrypoint &&
|
|
223
|
+
import.meta.url === pathToFileURL(path.resolve(entrypoint)).href,
|
|
224
|
+
);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
if (isDirectRun()) {
|
|
228
|
+
main().catch((err) => {
|
|
229
|
+
console.error("[preview] failed:", err);
|
|
230
|
+
process.exit(1);
|
|
231
|
+
});
|
|
232
|
+
}
|
|
@@ -2,6 +2,7 @@ import { randomUUID } from "node:crypto";
|
|
|
2
2
|
|
|
3
3
|
import { signA2AToken } from "@agent-native/core/a2a";
|
|
4
4
|
import { fetchOrgApps, type OrgApp } from "@agent-native/core/mcp";
|
|
5
|
+
import { getOrgDomain } from "@agent-native/core/org";
|
|
5
6
|
|
|
6
7
|
import type { AnalyticsAdminContext } from "./db-admin-connections.js";
|
|
7
8
|
|
|
@@ -31,11 +32,11 @@ export interface WorkspaceFeatureFlagsResult {
|
|
|
31
32
|
}
|
|
32
33
|
|
|
33
34
|
export interface WorkspaceFeatureFlagMutationResult {
|
|
34
|
-
contractVersion:
|
|
35
|
+
contractVersion: 2;
|
|
35
36
|
status: "ready";
|
|
36
37
|
key: string;
|
|
37
38
|
rules: Record<string, unknown>;
|
|
38
|
-
scope: { orgId: string | null };
|
|
39
|
+
scope: { orgId: string | null; orgDomain: string | null };
|
|
39
40
|
}
|
|
40
41
|
|
|
41
42
|
export interface WorkspaceFeatureFlagMutationInput {
|
|
@@ -66,14 +67,14 @@ export function validateWorkspaceFeatureFlagMutation(
|
|
|
66
67
|
body: unknown,
|
|
67
68
|
expected: {
|
|
68
69
|
key: string;
|
|
69
|
-
|
|
70
|
+
orgDomain: string;
|
|
70
71
|
rules?: Record<string, unknown>;
|
|
71
72
|
enabledForEmail?: string;
|
|
72
73
|
},
|
|
73
74
|
): WorkspaceFeatureFlagMutationResult {
|
|
74
75
|
const payload = body as Partial<WorkspaceFeatureFlagMutationResult> | null;
|
|
75
76
|
const valid =
|
|
76
|
-
payload?.contractVersion ===
|
|
77
|
+
payload?.contractVersion === 2 &&
|
|
77
78
|
payload.status === "ready" &&
|
|
78
79
|
payload.key === expected.key &&
|
|
79
80
|
!!payload.rules &&
|
|
@@ -81,7 +82,7 @@ export function validateWorkspaceFeatureFlagMutation(
|
|
|
81
82
|
!Array.isArray(payload.rules) &&
|
|
82
83
|
!!payload.scope &&
|
|
83
84
|
typeof payload.scope === "object" &&
|
|
84
|
-
payload.scope.
|
|
85
|
+
payload.scope.orgDomain === expected.orgDomain;
|
|
85
86
|
if (!valid)
|
|
86
87
|
throw new Error(
|
|
87
88
|
"The target app returned an unsupported or unverified feature flag mutation response.",
|
|
@@ -152,13 +153,43 @@ async function delegatedToken(
|
|
|
152
153
|
admin: AnalyticsAdminContext,
|
|
153
154
|
origin: string,
|
|
154
155
|
scope: "flags:read" | "flags:write",
|
|
156
|
+
resolvedOrgDomain?: string,
|
|
155
157
|
): Promise<string> {
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
158
|
+
try {
|
|
159
|
+
const orgDomain =
|
|
160
|
+
resolvedOrgDomain ??
|
|
161
|
+
(await getOrgDomain(admin.orgId))?.trim().toLowerCase();
|
|
162
|
+
if (!orgDomain) throw new TargetCallFailure("token-generation");
|
|
163
|
+
return await signA2AToken(admin.userEmail, orgDomain, undefined, {
|
|
164
|
+
expiresIn: "120s",
|
|
165
|
+
preferGlobalSecret: true,
|
|
166
|
+
audience: origin,
|
|
167
|
+
extraClaims: { org_id: admin.orgId, scope, jti: randomUUID() },
|
|
168
|
+
});
|
|
169
|
+
} catch {
|
|
170
|
+
throw new TargetCallFailure("token-generation");
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
type TargetFailureReason = "token-generation" | "timeout" | "network";
|
|
175
|
+
|
|
176
|
+
class TargetCallFailure extends Error {
|
|
177
|
+
constructor(readonly reason: TargetFailureReason) {
|
|
178
|
+
super(`Feature flag target call failed: ${reason}`);
|
|
179
|
+
this.name = "TargetCallFailure";
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export function classifyWorkspaceFeatureFlagTargetFailure(
|
|
184
|
+
error: unknown,
|
|
185
|
+
): TargetFailureReason {
|
|
186
|
+
if (error instanceof TargetCallFailure) return error.reason;
|
|
187
|
+
if (
|
|
188
|
+
error instanceof Error &&
|
|
189
|
+
(error.name === "TimeoutError" || error.name === "AbortError")
|
|
190
|
+
)
|
|
191
|
+
return "timeout";
|
|
192
|
+
return "network";
|
|
162
193
|
}
|
|
163
194
|
|
|
164
195
|
async function callTarget(
|
|
@@ -166,25 +197,34 @@ async function callTarget(
|
|
|
166
197
|
admin: AnalyticsAdminContext,
|
|
167
198
|
action: "list-feature-flags" | "set-feature-flag",
|
|
168
199
|
body: Record<string, unknown>,
|
|
200
|
+
orgDomain?: string,
|
|
169
201
|
): Promise<{ status: number; body: unknown }> {
|
|
170
202
|
const origin = targetOrigin(app);
|
|
171
203
|
const token = await delegatedToken(
|
|
172
204
|
admin,
|
|
173
205
|
origin,
|
|
174
206
|
action === "list-feature-flags" ? "flags:read" : "flags:write",
|
|
207
|
+
orgDomain,
|
|
175
208
|
);
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
209
|
+
let response: Response;
|
|
210
|
+
try {
|
|
211
|
+
response = await fetch(`${origin}/_agent-native/actions/${action}`, {
|
|
212
|
+
method: action === "list-feature-flags" ? "GET" : "POST",
|
|
213
|
+
headers: {
|
|
214
|
+
Authorization: `Bearer ${token}`,
|
|
215
|
+
Accept: "application/json",
|
|
216
|
+
...(action === "set-feature-flag"
|
|
217
|
+
? { "Content-Type": "application/json" }
|
|
218
|
+
: {}),
|
|
219
|
+
},
|
|
220
|
+
...(action === "set-feature-flag" ? { body: JSON.stringify(body) } : {}),
|
|
221
|
+
signal: AbortSignal.timeout(TARGET_TIMEOUT_MS),
|
|
222
|
+
});
|
|
223
|
+
} catch (error) {
|
|
224
|
+
throw new TargetCallFailure(
|
|
225
|
+
classifyWorkspaceFeatureFlagTargetFailure(error),
|
|
226
|
+
);
|
|
227
|
+
}
|
|
188
228
|
let parsed: unknown = null;
|
|
189
229
|
try {
|
|
190
230
|
parsed = await response.json();
|
|
@@ -209,7 +249,7 @@ export function classifyWorkspaceFeatureFlagList(
|
|
|
209
249
|
if (result.status === 404 || result.status === 405)
|
|
210
250
|
return { ...base, state: "unsupported" };
|
|
211
251
|
if (result.status < 200 || result.status >= 300)
|
|
212
|
-
return { ...base, state: "unknown-legacy" };
|
|
252
|
+
return { ...base, state: "unknown-legacy", reason: "target-execution" };
|
|
213
253
|
const payload = result.body as {
|
|
214
254
|
flags?: unknown;
|
|
215
255
|
canManage?: unknown;
|
|
@@ -254,7 +294,11 @@ async function mapBounded<T, R>(
|
|
|
254
294
|
export async function listWorkspaceFeatureFlags(
|
|
255
295
|
admin: AnalyticsAdminContext,
|
|
256
296
|
): Promise<WorkspaceFeatureFlagsResult> {
|
|
257
|
-
const apps = await fetchOrgApps({
|
|
297
|
+
const apps = await fetchOrgApps({
|
|
298
|
+
selfId: "analytics",
|
|
299
|
+
includeDirectoryApp: true,
|
|
300
|
+
serviceOrgId: admin.orgId,
|
|
301
|
+
});
|
|
258
302
|
if (apps.length === 0) return { directoryStatus: "unavailable", apps: [] };
|
|
259
303
|
const entries = await mapBounded(apps, async (app) => {
|
|
260
304
|
try {
|
|
@@ -262,13 +306,14 @@ export async function listWorkspaceFeatureFlags(
|
|
|
262
306
|
app,
|
|
263
307
|
await callTarget(app, admin, "list-feature-flags", {}),
|
|
264
308
|
);
|
|
265
|
-
} catch {
|
|
309
|
+
} catch (error) {
|
|
266
310
|
return {
|
|
267
311
|
appId: app.id,
|
|
268
312
|
appName: app.name,
|
|
269
313
|
appOrigin: targetOrigin(app),
|
|
270
314
|
state: "unreachable" as const,
|
|
271
315
|
flags: [],
|
|
316
|
+
reason: classifyWorkspaceFeatureFlagTargetFailure(error),
|
|
272
317
|
};
|
|
273
318
|
}
|
|
274
319
|
});
|
|
@@ -279,14 +324,31 @@ export async function setWorkspaceFeatureFlag(
|
|
|
279
324
|
admin: AnalyticsAdminContext,
|
|
280
325
|
input: WorkspaceFeatureFlagMutationInput,
|
|
281
326
|
): Promise<WorkspaceFeatureFlagMutationResult> {
|
|
282
|
-
const apps = await fetchOrgApps({
|
|
327
|
+
const apps = await fetchOrgApps({
|
|
328
|
+
selfId: "analytics",
|
|
329
|
+
includeDirectoryApp: true,
|
|
330
|
+
serviceOrgId: admin.orgId,
|
|
331
|
+
});
|
|
283
332
|
const app = apps.find((candidate) => candidate.id === input.appId);
|
|
284
333
|
if (!app)
|
|
285
334
|
throw new Error(
|
|
286
335
|
"The requested app is not available in this organization directory.",
|
|
287
336
|
);
|
|
288
337
|
const targetInput = workspaceFeatureFlagTargetInput(input);
|
|
289
|
-
|
|
338
|
+
let orgDomain: string | undefined;
|
|
339
|
+
try {
|
|
340
|
+
orgDomain = (await getOrgDomain(admin.orgId))?.trim().toLowerCase();
|
|
341
|
+
} catch {
|
|
342
|
+
throw new TargetCallFailure("token-generation");
|
|
343
|
+
}
|
|
344
|
+
if (!orgDomain) throw new TargetCallFailure("token-generation");
|
|
345
|
+
const result = await callTarget(
|
|
346
|
+
app,
|
|
347
|
+
admin,
|
|
348
|
+
"set-feature-flag",
|
|
349
|
+
targetInput,
|
|
350
|
+
orgDomain,
|
|
351
|
+
);
|
|
290
352
|
if (result.status === 401 || result.status === 403)
|
|
291
353
|
throw new Error("The target app denied this delegated flag operation.");
|
|
292
354
|
if (result.status === 404 || result.status === 405)
|
|
@@ -297,7 +359,7 @@ export async function setWorkspaceFeatureFlag(
|
|
|
297
359
|
);
|
|
298
360
|
return validateWorkspaceFeatureFlagMutation(result.body, {
|
|
299
361
|
key: input.key,
|
|
300
|
-
|
|
362
|
+
orgDomain,
|
|
301
363
|
...(input.operation === "replace-rules" && input.rules
|
|
302
364
|
? {
|
|
303
365
|
rules: {
|
|
@@ -324,7 +386,11 @@ export async function getWorkspaceFlagTarget(
|
|
|
324
386
|
admin: AnalyticsAdminContext,
|
|
325
387
|
appId: string,
|
|
326
388
|
): Promise<FleetFlagApp> {
|
|
327
|
-
const apps = await fetchOrgApps({
|
|
389
|
+
const apps = await fetchOrgApps({
|
|
390
|
+
selfId: "analytics",
|
|
391
|
+
includeDirectoryApp: true,
|
|
392
|
+
serviceOrgId: admin.orgId,
|
|
393
|
+
});
|
|
328
394
|
const app = apps.find((candidate) => candidate.id === appId);
|
|
329
395
|
if (!app)
|
|
330
396
|
throw new Error(
|
|
@@ -335,13 +401,14 @@ export async function getWorkspaceFlagTarget(
|
|
|
335
401
|
app,
|
|
336
402
|
await callTarget(app, admin, "list-feature-flags", {}),
|
|
337
403
|
);
|
|
338
|
-
} catch {
|
|
404
|
+
} catch (error) {
|
|
339
405
|
return {
|
|
340
406
|
appId: app.id,
|
|
341
407
|
appName: app.name,
|
|
342
408
|
appOrigin: targetOrigin(app),
|
|
343
409
|
state: "unreachable",
|
|
344
410
|
flags: [],
|
|
411
|
+
reason: classifyWorkspaceFeatureFlagTargetFailure(error),
|
|
345
412
|
};
|
|
346
413
|
}
|
|
347
414
|
}
|
|
@@ -1,15 +1,21 @@
|
|
|
1
|
-
import { createHash } from "node:crypto";
|
|
1
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
2
2
|
|
|
3
3
|
import { defineAction } from "@agent-native/core";
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
compareAndSetAppState,
|
|
6
|
+
listAppState,
|
|
7
|
+
readAppState,
|
|
8
|
+
} from "@agent-native/core/application-state";
|
|
5
9
|
import { runWithRequestContext } from "@agent-native/core/server";
|
|
6
10
|
import { getRequestUserEmail } from "@agent-native/core/server/request-context";
|
|
7
11
|
import { z } from "zod";
|
|
8
12
|
|
|
9
13
|
import {
|
|
10
14
|
CONTENT_LAST_LOCATION_STATE_KEY,
|
|
15
|
+
CONTENT_WELCOME_PAGE_STATE_KEY,
|
|
11
16
|
type ContentLandingResolution,
|
|
12
17
|
type ContentLastLocationState,
|
|
18
|
+
type ContentWelcomePageState,
|
|
13
19
|
} from "../shared/content-landing.js";
|
|
14
20
|
import { resolveContentDocumentAccess } from "./_content-document-access.js";
|
|
15
21
|
import { normalizeContentSpaceEmail } from "./_content-space-access.js";
|
|
@@ -24,12 +30,85 @@ const WELCOME_CONTENT = `Start here with a page that is wholly yours.
|
|
|
24
30
|
- Use the sidebar to find and organize your work.
|
|
25
31
|
- Ask the agent when you want a hand.`;
|
|
26
32
|
|
|
27
|
-
function
|
|
33
|
+
function legacyWelcomeDocumentId(userEmail: string, generation: number) {
|
|
28
34
|
const identity = normalizeContentSpaceEmail(userEmail);
|
|
29
|
-
const
|
|
35
|
+
const input = generation === 0 ? identity : `${identity}:${generation}`;
|
|
36
|
+
const digest = createHash("sha256").update(input).digest("hex");
|
|
30
37
|
return `content_welcome_${digest.slice(0, 32)}`;
|
|
31
38
|
}
|
|
32
39
|
|
|
40
|
+
function randomWelcomeDocumentId() {
|
|
41
|
+
return `content_welcome_${randomUUID().replace(/-/g, "")}`;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function welcomeGeneration(state: Record<string, unknown>): number {
|
|
45
|
+
const candidate = state as ContentWelcomePageState;
|
|
46
|
+
if (
|
|
47
|
+
typeof candidate.generation !== "number" ||
|
|
48
|
+
!Number.isSafeInteger(candidate.generation) ||
|
|
49
|
+
candidate.generation < 0
|
|
50
|
+
) {
|
|
51
|
+
throw new Error("Content welcome page state has an invalid generation");
|
|
52
|
+
}
|
|
53
|
+
return candidate.generation;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function isUniqueConstraintError(error: unknown): boolean {
|
|
57
|
+
let current: unknown = error;
|
|
58
|
+
const seen = new Set<unknown>();
|
|
59
|
+
while (current && typeof current === "object" && !seen.has(current)) {
|
|
60
|
+
seen.add(current);
|
|
61
|
+
const candidate = current as {
|
|
62
|
+
code?: unknown;
|
|
63
|
+
message?: unknown;
|
|
64
|
+
cause?: unknown;
|
|
65
|
+
};
|
|
66
|
+
const code = String(candidate.code ?? "");
|
|
67
|
+
const message = String(candidate.message ?? "");
|
|
68
|
+
if (
|
|
69
|
+
code === "23505" ||
|
|
70
|
+
code.includes("SQLITE_CONSTRAINT") ||
|
|
71
|
+
/unique constraint|unique violation|duplicate key/i.test(message)
|
|
72
|
+
) {
|
|
73
|
+
return true;
|
|
74
|
+
}
|
|
75
|
+
current = candidate.cause;
|
|
76
|
+
}
|
|
77
|
+
return false;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async function resolveWelcomeState(): Promise<{
|
|
81
|
+
generation: number;
|
|
82
|
+
documentId: string | null;
|
|
83
|
+
expectedValue: Record<string, unknown>;
|
|
84
|
+
}> {
|
|
85
|
+
while (true) {
|
|
86
|
+
const entries = await listAppState(CONTENT_WELCOME_PAGE_STATE_KEY);
|
|
87
|
+
const entry = entries.find(
|
|
88
|
+
({ key }) => key === CONTENT_WELCOME_PAGE_STATE_KEY,
|
|
89
|
+
);
|
|
90
|
+
if (entry) {
|
|
91
|
+
if (!entry.value || typeof entry.value !== "object") {
|
|
92
|
+
throw new Error("Content welcome page state must be an object");
|
|
93
|
+
}
|
|
94
|
+
return {
|
|
95
|
+
generation: welcomeGeneration(entry.value),
|
|
96
|
+
documentId:
|
|
97
|
+
typeof entry.value.documentId === "string" && entry.value.documentId
|
|
98
|
+
? entry.value.documentId
|
|
99
|
+
: null,
|
|
100
|
+
expectedValue: entry.value,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const initial = { generation: 0 };
|
|
105
|
+
if (
|
|
106
|
+
await compareAndSetAppState(CONTENT_WELCOME_PAGE_STATE_KEY, null, initial)
|
|
107
|
+
) {
|
|
108
|
+
return { generation: 0, documentId: null, expectedValue: initial };
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
33
112
|
function savedDocumentId(state: Record<string, unknown> | null): string | null {
|
|
34
113
|
const candidate = state as ContentLastLocationState | null;
|
|
35
114
|
return typeof candidate?.documentId === "string" && candidate.documentId
|
|
@@ -44,22 +123,28 @@ async function resolveUsableDocument(documentId: string) {
|
|
|
44
123
|
return access.resource;
|
|
45
124
|
}
|
|
46
125
|
|
|
47
|
-
async function resolveWelcomeDocument(userEmail: string) {
|
|
48
|
-
const
|
|
49
|
-
const document =
|
|
50
|
-
if (!document)
|
|
126
|
+
async function resolveWelcomeDocument(userEmail: string, documentId: string) {
|
|
127
|
+
const access = await resolveContentDocumentAccess(documentId);
|
|
128
|
+
const document = access?.resource;
|
|
129
|
+
if (!document) {
|
|
130
|
+
return (await isSoftDeletedDatabaseDocument(documentId))
|
|
131
|
+
? { status: "unavailable" as const }
|
|
132
|
+
: { status: "missing" as const };
|
|
133
|
+
}
|
|
134
|
+
if (document.trashedAt || (await isSoftDeletedDatabaseDocument(documentId))) {
|
|
135
|
+
return { status: "unavailable" as const };
|
|
136
|
+
}
|
|
51
137
|
|
|
52
138
|
const normalizedEmail = normalizeContentSpaceEmail(userEmail);
|
|
53
139
|
if (
|
|
54
140
|
normalizeContentSpaceEmail(document.ownerEmail) !== normalizedEmail ||
|
|
55
141
|
document.spaceId !== personalContentSpaceId(normalizedEmail) ||
|
|
56
142
|
document.parentId !== null ||
|
|
57
|
-
document.visibility !== "private"
|
|
58
|
-
document.title !== WELCOME_TITLE
|
|
143
|
+
document.visibility !== "private"
|
|
59
144
|
) {
|
|
60
|
-
return
|
|
145
|
+
return { status: "unavailable" as const };
|
|
61
146
|
}
|
|
62
|
-
return documentId;
|
|
147
|
+
return { status: "usable" as const, documentId };
|
|
63
148
|
}
|
|
64
149
|
|
|
65
150
|
async function resolveWelcome(userEmail: string): Promise<{
|
|
@@ -69,27 +154,72 @@ async function resolveWelcome(userEmail: string): Promise<{
|
|
|
69
154
|
"welcome-created" | "welcome-reused"
|
|
70
155
|
>;
|
|
71
156
|
}> {
|
|
72
|
-
const existing = await resolveWelcomeDocument(userEmail);
|
|
73
|
-
if (existing) return { documentId: existing, resolution: "welcome-reused" };
|
|
74
|
-
|
|
75
|
-
const documentId = welcomeDocumentId(userEmail);
|
|
76
157
|
const normalizedEmail = normalizeContentSpaceEmail(userEmail);
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
158
|
+
while (true) {
|
|
159
|
+
const state = await resolveWelcomeState();
|
|
160
|
+
const documentId =
|
|
161
|
+
state.documentId ??
|
|
162
|
+
legacyWelcomeDocumentId(normalizedEmail, state.generation);
|
|
163
|
+
const existing = await resolveWelcomeDocument(normalizedEmail, documentId);
|
|
164
|
+
if (existing.status === "usable") {
|
|
165
|
+
if (!state.documentId) {
|
|
166
|
+
await compareAndSetAppState(
|
|
167
|
+
CONTENT_WELCOME_PAGE_STATE_KEY,
|
|
168
|
+
state.expectedValue,
|
|
169
|
+
{ ...state.expectedValue, documentId },
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
return {
|
|
173
|
+
documentId: existing.documentId,
|
|
174
|
+
resolution: "welcome-reused",
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (!state.documentId || existing.status === "unavailable") {
|
|
179
|
+
await compareAndSetAppState(
|
|
180
|
+
CONTENT_WELCOME_PAGE_STATE_KEY,
|
|
181
|
+
state.expectedValue,
|
|
182
|
+
{
|
|
183
|
+
...state.expectedValue,
|
|
184
|
+
generation: state.generation + 1,
|
|
185
|
+
documentId: randomWelcomeDocumentId(),
|
|
186
|
+
},
|
|
187
|
+
);
|
|
188
|
+
continue;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
try {
|
|
192
|
+
await runWithRequestContext({ userEmail: normalizedEmail }, () =>
|
|
193
|
+
createDocumentAction.run({
|
|
194
|
+
id: documentId,
|
|
195
|
+
title: WELCOME_TITLE,
|
|
196
|
+
content: WELCOME_CONTENT,
|
|
197
|
+
}),
|
|
198
|
+
);
|
|
199
|
+
return { documentId, resolution: "welcome-created" };
|
|
200
|
+
} catch (error) {
|
|
201
|
+
const raced = await resolveWelcomeDocument(normalizedEmail, documentId);
|
|
202
|
+
if (raced.status === "usable") {
|
|
203
|
+
return {
|
|
204
|
+
documentId: raced.documentId,
|
|
205
|
+
resolution: "welcome-reused",
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
if (raced.status === "unavailable") continue;
|
|
209
|
+
if (isUniqueConstraintError(error)) {
|
|
210
|
+
await compareAndSetAppState(
|
|
211
|
+
CONTENT_WELCOME_PAGE_STATE_KEY,
|
|
212
|
+
state.expectedValue,
|
|
213
|
+
{
|
|
214
|
+
...state.expectedValue,
|
|
215
|
+
generation: state.generation + 1,
|
|
216
|
+
documentId: randomWelcomeDocumentId(),
|
|
217
|
+
},
|
|
218
|
+
);
|
|
219
|
+
continue;
|
|
220
|
+
}
|
|
221
|
+
throw error;
|
|
222
|
+
}
|
|
93
223
|
}
|
|
94
224
|
}
|
|
95
225
|
|
|
@@ -1,9 +1,14 @@
|
|
|
1
1
|
export const CONTENT_LAST_LOCATION_STATE_KEY = "content-last-location-v1";
|
|
2
|
+
export const CONTENT_WELCOME_PAGE_STATE_KEY = "content-welcome-page-v1";
|
|
2
3
|
|
|
3
4
|
export type ContentLastLocationState = {
|
|
4
5
|
documentId: string;
|
|
5
6
|
};
|
|
6
7
|
|
|
8
|
+
export type ContentWelcomePageState = {
|
|
9
|
+
generation: number;
|
|
10
|
+
documentId?: string;
|
|
11
|
+
};
|
|
7
12
|
export type ContentLandingResolution =
|
|
8
13
|
| "restored"
|
|
9
14
|
| "welcome-created"
|
|
@@ -278,14 +278,13 @@ export interface DiscoveredAppLike {
|
|
|
278
278
|
|
|
279
279
|
/**
|
|
280
280
|
* Shape the discovered-agent list into the directory response. The input is
|
|
281
|
-
* Dispatch's EXISTING connected-apps registry — `discoverAgents(
|
|
281
|
+
* Dispatch's EXISTING connected-apps registry — `discoverAgents()`
|
|
282
282
|
* from `@agent-native/core/server/agent-discovery`, the same source
|
|
283
283
|
* `list-connected-agents` and the `call-agent` delegation path use. It is
|
|
284
284
|
* already allow-list-respecting (hidden first-party templates are excluded
|
|
285
285
|
* from `BUILTIN_AGENTS` unless `defaultAgent`), so no second filter is
|
|
286
|
-
* needed here; we only drop entries without a usable absolute http(s) URL
|
|
287
|
-
*
|
|
288
|
-
* just called).
|
|
286
|
+
* needed here; we only drop entries without a usable absolute http(s) URL.
|
|
287
|
+
* Callers that identify themselves may also request self-filtering.
|
|
289
288
|
*/
|
|
290
289
|
export function buildOrgAppsResponse(input: {
|
|
291
290
|
org: string;
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
*
|
|
25
25
|
* APP-LIST SOURCE — Dispatch's existing connected-apps registry.
|
|
26
26
|
* -------------------------------------------------------------
|
|
27
|
-
* Dispatch already has a connected-apps concept: `discoverAgents(
|
|
27
|
+
* Dispatch already has a connected-apps concept: `discoverAgents()`
|
|
28
28
|
* from `@agent-native/core/server/agent-discovery` (the same source
|
|
29
29
|
* `list-connected-agents` and the `call-agent` delegation path use). It
|
|
30
30
|
* returns the allow-listed first-party apps with their prod URLs PLUS any
|
|
@@ -143,7 +143,16 @@ const orgAppsHandler = defineEventHandler(
|
|
|
143
143
|
const apps: DiscoveredAppLike[] = await runWithRequestContext(
|
|
144
144
|
{ userEmail: verified.email, orgId: localOrg.orgId },
|
|
145
145
|
async () => {
|
|
146
|
-
const
|
|
146
|
+
const includeDirectoryApp =
|
|
147
|
+
getRequestHeader(event, "x-agent-native-include-directory-app") ===
|
|
148
|
+
"1";
|
|
149
|
+
const discovered = await discoverAgents(
|
|
150
|
+
includeDirectoryApp ? undefined : SELF_APP_ID,
|
|
151
|
+
{
|
|
152
|
+
preferLocalUrls:
|
|
153
|
+
process.env.AGENT_NATIVE_PREFER_LOCAL_APP_URLS === "1",
|
|
154
|
+
},
|
|
155
|
+
);
|
|
147
156
|
return discovered.map((a) => ({
|
|
148
157
|
id: a.id,
|
|
149
158
|
name: a.name,
|
|
@@ -165,7 +174,10 @@ const orgAppsHandler = defineEventHandler(
|
|
|
165
174
|
const body = buildOrgAppsResponse({
|
|
166
175
|
org: orgLabel,
|
|
167
176
|
apps,
|
|
168
|
-
selfId:
|
|
177
|
+
selfId:
|
|
178
|
+
getRequestHeader(event, "x-agent-native-include-directory-app") === "1"
|
|
179
|
+
? undefined
|
|
180
|
+
: SELF_APP_ID,
|
|
169
181
|
});
|
|
170
182
|
|
|
171
183
|
// Short, cacheable, read-only. Private (per-org) so shared caches must
|
|
@@ -157,6 +157,14 @@ const AddSlideOp = z.object({
|
|
|
157
157
|
notes: z.string().optional(),
|
|
158
158
|
layout: z.string().optional(),
|
|
159
159
|
background: z.string().optional(),
|
|
160
|
+
imageUrl: z.string().optional(),
|
|
161
|
+
imagePrompt: z.string().optional(),
|
|
162
|
+
excalidrawData: z.string().optional(),
|
|
163
|
+
transition: z
|
|
164
|
+
.enum(["instant", "none", "fade", "slide", "zoom"])
|
|
165
|
+
.optional(),
|
|
166
|
+
animations: z.array(z.unknown()).optional(),
|
|
167
|
+
splitByParagraph: z.boolean().optional(),
|
|
160
168
|
})
|
|
161
169
|
.passthrough(),
|
|
162
170
|
});
|
|
@@ -382,7 +390,10 @@ export function applyOperation(deck: any, op: Operation): void {
|
|
|
382
390
|
// Idempotency: if the slide already exists (duplicate delivery), skip.
|
|
383
391
|
if (slides.some((s: { id: string }) => s.id === slideId)) return;
|
|
384
392
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
393
|
+
// Copy every provided field: a duplicated or undo-restored slide has to
|
|
394
|
+
// keep its transition, animations, and image data, not just its text.
|
|
385
395
|
const newSlide: any = {
|
|
396
|
+
...fields,
|
|
386
397
|
id: slideId,
|
|
387
398
|
content:
|
|
388
399
|
typeof fields.content === "string"
|
|
@@ -391,9 +402,7 @@ export function applyOperation(deck: any, op: Operation): void {
|
|
|
391
402
|
notes: fields.notes ?? "",
|
|
392
403
|
layout: fields.layout ?? "content",
|
|
393
404
|
};
|
|
394
|
-
|
|
395
|
-
newSlide.background = fields.background;
|
|
396
|
-
}
|
|
405
|
+
delete newSlide.imageLoading;
|
|
397
406
|
const insertAfterIdx = afterSlideId
|
|
398
407
|
? slides.findIndex((s: { id: string }) => s.id === afterSlideId)
|
|
399
408
|
: -1;
|