applaunchflow 0.3.4 → 0.3.6
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/build/http.js +14 -0
- package/build/http.test.js +35 -0
- package/build/projects.test.js +103 -0
- package/build/promovideo-picker.test.js +3 -3
- package/build/tools/projects.js +133 -3
- package/build/tools/promovideo.js +11 -8
- package/package.json +1 -1
package/build/http.js
CHANGED
|
@@ -148,6 +148,20 @@ export function createHttpServer() {
|
|
|
148
148
|
json(response, 200, { ok: true, service: "applaunchflow-mcp" });
|
|
149
149
|
return;
|
|
150
150
|
}
|
|
151
|
+
if (request.method === "GET" &&
|
|
152
|
+
url.pathname === "/.well-known/openai-apps-challenge") {
|
|
153
|
+
const token = process.env.OPENAI_APPS_CHALLENGE_TOKEN?.trim();
|
|
154
|
+
if (!token) {
|
|
155
|
+
json(response, 404, { error: "Challenge token not configured" });
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
response.writeHead(200, {
|
|
159
|
+
"content-type": "text/plain; charset=utf-8",
|
|
160
|
+
"cache-control": "no-store",
|
|
161
|
+
});
|
|
162
|
+
response.end(token);
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
151
165
|
if (request.method === "GET" &&
|
|
152
166
|
url.pathname === "/.well-known/oauth-protected-resource") {
|
|
153
167
|
json(response, 200, {
|
package/build/http.test.js
CHANGED
|
@@ -32,6 +32,41 @@ test("HTTP server exposes health and protected-resource metadata", async () => {
|
|
|
32
32
|
]);
|
|
33
33
|
});
|
|
34
34
|
});
|
|
35
|
+
test("HTTP server exposes the configured OpenAI domain challenge", async () => {
|
|
36
|
+
const previousToken = process.env.OPENAI_APPS_CHALLENGE_TOKEN;
|
|
37
|
+
process.env.OPENAI_APPS_CHALLENGE_TOKEN = "openai-domain-challenge";
|
|
38
|
+
try {
|
|
39
|
+
await withServer(async (baseUrl) => {
|
|
40
|
+
const response = await fetch(`${baseUrl}/.well-known/openai-apps-challenge`);
|
|
41
|
+
assert.equal(response.status, 200);
|
|
42
|
+
assert.equal(response.headers.get("content-type"), "text/plain; charset=utf-8");
|
|
43
|
+
assert.equal(await response.text(), "openai-domain-challenge");
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
finally {
|
|
47
|
+
if (previousToken === undefined) {
|
|
48
|
+
delete process.env.OPENAI_APPS_CHALLENGE_TOKEN;
|
|
49
|
+
}
|
|
50
|
+
else {
|
|
51
|
+
process.env.OPENAI_APPS_CHALLENGE_TOKEN = previousToken;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
test("OpenAI domain challenge returns 404 when it is not configured", async () => {
|
|
56
|
+
const previousToken = process.env.OPENAI_APPS_CHALLENGE_TOKEN;
|
|
57
|
+
delete process.env.OPENAI_APPS_CHALLENGE_TOKEN;
|
|
58
|
+
try {
|
|
59
|
+
await withServer(async (baseUrl) => {
|
|
60
|
+
const response = await fetch(`${baseUrl}/.well-known/openai-apps-challenge`);
|
|
61
|
+
assert.equal(response.status, 404);
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
finally {
|
|
65
|
+
if (previousToken !== undefined) {
|
|
66
|
+
process.env.OPENAI_APPS_CHALLENGE_TOKEN = previousToken;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
});
|
|
35
70
|
test("MCP endpoint challenges unauthenticated callers with resource metadata", async () => {
|
|
36
71
|
await withServer(async (baseUrl) => {
|
|
37
72
|
const response = await fetch(`${baseUrl}/mcp`, {
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import { toProjectSummary, toSafeProjectState } from "./tools/projects.js";
|
|
4
|
+
const rawProject = {
|
|
5
|
+
id: "00000000-0000-4000-8000-000000000001",
|
|
6
|
+
user_id: "private-user-id",
|
|
7
|
+
name: "LaunchNotes Demo",
|
|
8
|
+
platform: "ios",
|
|
9
|
+
created_at: "2026-08-01T10:00:00.000Z",
|
|
10
|
+
updated_at: "2026-08-12T10:00:00.000Z",
|
|
11
|
+
metadata: {
|
|
12
|
+
appName: "LaunchNotes Demo",
|
|
13
|
+
category: "Productivity",
|
|
14
|
+
defaultDeviceType: "phone",
|
|
15
|
+
detectedLanguage: "en",
|
|
16
|
+
logoPath: "private/logo.png",
|
|
17
|
+
logoUrl: "https://storage.example/private-logo?token=secret",
|
|
18
|
+
screenshotCache: { firstMobilePath: "private/screenshot.png" },
|
|
19
|
+
},
|
|
20
|
+
aso_copy: { keywords: "private keyword payload" },
|
|
21
|
+
};
|
|
22
|
+
test("project summaries contain only workflow-relevant public fields", () => {
|
|
23
|
+
assert.deepEqual(toProjectSummary(rawProject), {
|
|
24
|
+
id: rawProject.id,
|
|
25
|
+
name: "LaunchNotes Demo",
|
|
26
|
+
platform: "ios",
|
|
27
|
+
updatedAt: "2026-08-12T10:00:00.000Z",
|
|
28
|
+
category: "Productivity",
|
|
29
|
+
defaultDeviceType: "phone",
|
|
30
|
+
detectedLanguage: "en",
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
test("safe project state removes internal records and signed asset URLs", () => {
|
|
34
|
+
const state = toSafeProjectState({
|
|
35
|
+
project: { ...rawProject, logoUrl: "https://storage.example/logo?token=secret" },
|
|
36
|
+
assets: {
|
|
37
|
+
mobileScreenshots: ["https://storage.example/one?token=secret"],
|
|
38
|
+
tabletScreenshots: [],
|
|
39
|
+
desktopScreenshots: ["https://storage.example/two?token=secret"],
|
|
40
|
+
logo: "https://storage.example/logo?token=secret",
|
|
41
|
+
qrCodes: [{ signedUrl: "https://storage.example/qr?token=secret" }],
|
|
42
|
+
},
|
|
43
|
+
content: {
|
|
44
|
+
screenshots: {
|
|
45
|
+
type: "screenshots",
|
|
46
|
+
variants: [
|
|
47
|
+
{
|
|
48
|
+
id: "variant-id",
|
|
49
|
+
generation_id: "generation-id",
|
|
50
|
+
content_type: "screenshots",
|
|
51
|
+
label: "v1",
|
|
52
|
+
is_active: true,
|
|
53
|
+
created_at: "2026-08-01T10:00:00.000Z",
|
|
54
|
+
updated_at: "2026-08-12T10:00:00.000Z",
|
|
55
|
+
languages: ["en"],
|
|
56
|
+
ready: true,
|
|
57
|
+
config: { internal: true },
|
|
58
|
+
},
|
|
59
|
+
],
|
|
60
|
+
activeVariant: {
|
|
61
|
+
id: "variant-id",
|
|
62
|
+
generation_id: "generation-id",
|
|
63
|
+
is_active: true,
|
|
64
|
+
ready: true,
|
|
65
|
+
},
|
|
66
|
+
editUrl: "https://dashboard.applaunchflow.com/editor?id=safe",
|
|
67
|
+
variantCount: 1,
|
|
68
|
+
isReady: true,
|
|
69
|
+
translations: [{ raw: "private layout" }],
|
|
70
|
+
},
|
|
71
|
+
asoCopy: { asoCopy: { keywords: "private" } },
|
|
72
|
+
appIcon: { variants: [{ config: "private" }] },
|
|
73
|
+
},
|
|
74
|
+
progress: {
|
|
75
|
+
totalItems: 4,
|
|
76
|
+
completedItems: 1,
|
|
77
|
+
percentage: 25,
|
|
78
|
+
missingItems: ["promoVideo"],
|
|
79
|
+
},
|
|
80
|
+
readyItems: { "internal-generation-id": true },
|
|
81
|
+
});
|
|
82
|
+
assert.deepEqual(state.assets, {
|
|
83
|
+
screenshotCounts: { mobile: 1, tablet: 0, desktop: 1 },
|
|
84
|
+
hasLogo: true,
|
|
85
|
+
qrCodeCount: 1,
|
|
86
|
+
});
|
|
87
|
+
assert.deepEqual(Object.keys(state.content), ["screenshots"]);
|
|
88
|
+
const serialized = JSON.stringify(state);
|
|
89
|
+
for (const forbidden of [
|
|
90
|
+
"private-user-id",
|
|
91
|
+
"token=secret",
|
|
92
|
+
"logoPath",
|
|
93
|
+
"screenshotCache",
|
|
94
|
+
"aso_copy",
|
|
95
|
+
"translations",
|
|
96
|
+
"readyItems",
|
|
97
|
+
"created_at",
|
|
98
|
+
"content_type",
|
|
99
|
+
"config",
|
|
100
|
+
]) {
|
|
101
|
+
assert.equal(serialized.includes(forbidden), false, forbidden);
|
|
102
|
+
}
|
|
103
|
+
});
|
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
import assert from "node:assert/strict";
|
|
2
2
|
import test from "node:test";
|
|
3
|
-
import { buildPromoVideoCandidateRequest,
|
|
3
|
+
import { buildPromoVideoCandidateRequest, buildPromoVideoDashboardUrl, } from "./tools/promovideo.js";
|
|
4
4
|
test("generate_promo_video requests three transient candidates", () => {
|
|
5
5
|
assert.deepEqual(buildPromoVideoCandidateRequest({ projectId: "project", message: "fresh" }), { projectId: "project", message: "fresh", candidateCount: 3 });
|
|
6
6
|
});
|
|
7
7
|
test("the MCP opens the dashboard candidate picker instead of a saved variant", () => {
|
|
8
|
-
const url = new URL(
|
|
8
|
+
const url = new URL(buildPromoVideoDashboardUrl({
|
|
9
9
|
credentials: { baseUrl: "https://dashboard.applaunchflow.com" },
|
|
10
10
|
}, {
|
|
11
11
|
generationId: "11111111-1111-4111-8111-111111111111",
|
|
12
12
|
candidateKey: "candidate-key",
|
|
13
13
|
replaceVariantId: "22222222-2222-4222-8222-222222222222",
|
|
14
14
|
}));
|
|
15
|
-
assert.equal(url.pathname, "/
|
|
15
|
+
assert.equal(url.pathname, "/promo-video-picker");
|
|
16
16
|
assert.equal(url.searchParams.get("candidateKey"), "candidate-key");
|
|
17
17
|
assert.equal(url.searchParams.get("replaceVariantId"), "22222222-2222-4222-8222-222222222222");
|
|
18
18
|
});
|
package/build/tools/projects.js
CHANGED
|
@@ -3,13 +3,143 @@ import { fail, ok } from "./utils.js";
|
|
|
3
3
|
function stripUndefined(value) {
|
|
4
4
|
return Object.fromEntries(Object.entries(value).filter(([, entry]) => entry !== undefined));
|
|
5
5
|
}
|
|
6
|
+
function isRecord(value) {
|
|
7
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
8
|
+
}
|
|
9
|
+
function asArray(value) {
|
|
10
|
+
return Array.isArray(value) ? value : [];
|
|
11
|
+
}
|
|
12
|
+
function safeHttpsUrl(value) {
|
|
13
|
+
if (typeof value !== "string")
|
|
14
|
+
return undefined;
|
|
15
|
+
try {
|
|
16
|
+
const url = new URL(value);
|
|
17
|
+
return url.protocol === "https:" ? value : undefined;
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
return undefined;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
export function toProjectSummary(value) {
|
|
24
|
+
const project = isRecord(value) ? value : {};
|
|
25
|
+
const metadata = isRecord(project.metadata) ? project.metadata : {};
|
|
26
|
+
return stripUndefined({
|
|
27
|
+
id: typeof project.id === "string" ? project.id : undefined,
|
|
28
|
+
name: typeof project.name === "string"
|
|
29
|
+
? project.name
|
|
30
|
+
: typeof metadata.appName === "string"
|
|
31
|
+
? metadata.appName
|
|
32
|
+
: undefined,
|
|
33
|
+
platform: typeof project.platform === "string"
|
|
34
|
+
? project.platform
|
|
35
|
+
: typeof metadata.platform === "string"
|
|
36
|
+
? metadata.platform
|
|
37
|
+
: undefined,
|
|
38
|
+
updatedAt: typeof project.updated_at === "string"
|
|
39
|
+
? project.updated_at
|
|
40
|
+
: typeof project.updatedAt === "string"
|
|
41
|
+
? project.updatedAt
|
|
42
|
+
: undefined,
|
|
43
|
+
category: typeof metadata.category === "string" ? metadata.category : undefined,
|
|
44
|
+
defaultDeviceType: typeof metadata.defaultDeviceType === "string"
|
|
45
|
+
? metadata.defaultDeviceType
|
|
46
|
+
: undefined,
|
|
47
|
+
detectedLanguage: typeof metadata.detectedLanguage === "string"
|
|
48
|
+
? metadata.detectedLanguage
|
|
49
|
+
: undefined,
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
function toVariantSummary(value) {
|
|
53
|
+
const variant = isRecord(value) ? value : {};
|
|
54
|
+
return stripUndefined({
|
|
55
|
+
id: typeof variant.id === "string" ? variant.id : undefined,
|
|
56
|
+
generationId: typeof variant.generation_id === "string"
|
|
57
|
+
? variant.generation_id
|
|
58
|
+
: typeof variant.generationId === "string"
|
|
59
|
+
? variant.generationId
|
|
60
|
+
: undefined,
|
|
61
|
+
label: typeof variant.label === "string" ? variant.label : undefined,
|
|
62
|
+
isActive: typeof variant.is_active === "boolean"
|
|
63
|
+
? variant.is_active
|
|
64
|
+
: typeof variant.isActive === "boolean"
|
|
65
|
+
? variant.isActive
|
|
66
|
+
: undefined,
|
|
67
|
+
updatedAt: typeof variant.updated_at === "string"
|
|
68
|
+
? variant.updated_at
|
|
69
|
+
: typeof variant.updatedAt === "string"
|
|
70
|
+
? variant.updatedAt
|
|
71
|
+
: undefined,
|
|
72
|
+
languages: Array.isArray(variant.languages)
|
|
73
|
+
? variant.languages.filter((entry) => typeof entry === "string")
|
|
74
|
+
: undefined,
|
|
75
|
+
ready: typeof variant.ready === "boolean" ? variant.ready : undefined,
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
function toContentSummary(value) {
|
|
79
|
+
const content = isRecord(value) ? value : {};
|
|
80
|
+
const variants = asArray(content.variants).map(toVariantSummary);
|
|
81
|
+
const activeVariant = isRecord(content.activeVariant)
|
|
82
|
+
? toVariantSummary(content.activeVariant)
|
|
83
|
+
: undefined;
|
|
84
|
+
return stripUndefined({
|
|
85
|
+
type: typeof content.type === "string" ? content.type : undefined,
|
|
86
|
+
variants,
|
|
87
|
+
activeVariant,
|
|
88
|
+
editUrl: safeHttpsUrl(content.editUrl),
|
|
89
|
+
activeLabel: typeof content.activeLabel === "string" ? content.activeLabel : undefined,
|
|
90
|
+
variantCount: typeof content.variantCount === "number"
|
|
91
|
+
? content.variantCount
|
|
92
|
+
: variants.length,
|
|
93
|
+
isReady: typeof content.isReady === "boolean" ? content.isReady : undefined,
|
|
94
|
+
hasContent: typeof content.hasContent === "boolean" ? content.hasContent : undefined,
|
|
95
|
+
screenshotTemplateId: typeof content.screenshotTemplateId === "string"
|
|
96
|
+
? content.screenshotTemplateId
|
|
97
|
+
: undefined,
|
|
98
|
+
socialTemplateId: typeof content.socialTemplateId === "string"
|
|
99
|
+
? content.socialTemplateId
|
|
100
|
+
: undefined,
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
export function toSafeProjectState(value) {
|
|
104
|
+
const state = isRecord(value) ? value : {};
|
|
105
|
+
const assets = isRecord(state.assets) ? state.assets : {};
|
|
106
|
+
const content = isRecord(state.content) ? state.content : {};
|
|
107
|
+
const progress = isRecord(state.progress) ? state.progress : {};
|
|
108
|
+
const safeContent = Object.fromEntries(["screenshots", "socialGraphics", "promoVideo", "mockups"]
|
|
109
|
+
.filter((key) => isRecord(content[key]))
|
|
110
|
+
.map((key) => [key, toContentSummary(content[key])]));
|
|
111
|
+
return {
|
|
112
|
+
project: toProjectSummary(state.project),
|
|
113
|
+
assets: {
|
|
114
|
+
screenshotCounts: {
|
|
115
|
+
mobile: asArray(assets.mobileScreenshots).length,
|
|
116
|
+
tablet: asArray(assets.tabletScreenshots).length,
|
|
117
|
+
desktop: asArray(assets.desktopScreenshots).length,
|
|
118
|
+
},
|
|
119
|
+
hasLogo: typeof assets.logo === "string" && assets.logo.length > 0,
|
|
120
|
+
qrCodeCount: asArray(assets.qrCodes).length,
|
|
121
|
+
},
|
|
122
|
+
content: safeContent,
|
|
123
|
+
progress: stripUndefined({
|
|
124
|
+
totalItems: typeof progress.totalItems === "number" ? progress.totalItems : undefined,
|
|
125
|
+
completedItems: typeof progress.completedItems === "number"
|
|
126
|
+
? progress.completedItems
|
|
127
|
+
: undefined,
|
|
128
|
+
percentage: typeof progress.percentage === "number" ? progress.percentage : undefined,
|
|
129
|
+
missingItems: Array.isArray(progress.missingItems)
|
|
130
|
+
? progress.missingItems.filter((entry) => typeof entry === "string")
|
|
131
|
+
: undefined,
|
|
132
|
+
}),
|
|
133
|
+
};
|
|
134
|
+
}
|
|
6
135
|
export function registerProjectTools(server, client) {
|
|
7
136
|
server.registerTool("list_projects", {
|
|
8
137
|
title: "List Projects",
|
|
9
138
|
description: "List AppLaunchFlow projects for the authenticated user",
|
|
10
139
|
}, async () => {
|
|
11
140
|
try {
|
|
12
|
-
|
|
141
|
+
const result = await client.listProjects();
|
|
142
|
+
return ok({ projects: asArray(result.projects).map(toProjectSummary) }, "Fetched projects");
|
|
13
143
|
}
|
|
14
144
|
catch (error) {
|
|
15
145
|
return fail(error);
|
|
@@ -23,7 +153,7 @@ export function registerProjectTools(server, client) {
|
|
|
23
153
|
},
|
|
24
154
|
}, async ({ projectId }) => {
|
|
25
155
|
try {
|
|
26
|
-
return ok(await client.getProject(projectId), "Fetched project");
|
|
156
|
+
return ok(toSafeProjectState(await client.getProject(projectId)), "Fetched project");
|
|
27
157
|
}
|
|
28
158
|
catch (error) {
|
|
29
159
|
return fail(error);
|
|
@@ -88,7 +218,7 @@ export function registerProjectTools(server, client) {
|
|
|
88
218
|
});
|
|
89
219
|
const created = await client.createProject(requestBody);
|
|
90
220
|
return ok({
|
|
91
|
-
project: created.project,
|
|
221
|
+
project: toProjectSummary(created.project),
|
|
92
222
|
nextRecommendedStep: "upload_screenshots",
|
|
93
223
|
}, "Created project");
|
|
94
224
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { createHostedReadReceipt, createReadReceiptStore, fail, hostedMcpEnabled, ok, openUrl, verifyHostedReadReceipt, } from "./utils.js";
|
|
3
3
|
const promoReceiptKey = (generationId, variantId) => ["promo-video", generationId, variantId || "active"].join("::");
|
|
4
|
-
export function
|
|
4
|
+
export function buildPromoVideoDashboardUrl(client, args) {
|
|
5
5
|
const params = new URLSearchParams({ projectId: args.generationId });
|
|
6
6
|
if (args.variantId) {
|
|
7
7
|
params.set("variantId", args.variantId);
|
|
@@ -12,7 +12,10 @@ export function buildPromoVideoEditorUrl(client, args) {
|
|
|
12
12
|
if (args.replaceVariantId) {
|
|
13
13
|
params.set("replaceVariantId", args.replaceVariantId);
|
|
14
14
|
}
|
|
15
|
-
|
|
15
|
+
const pathname = args.candidateKey
|
|
16
|
+
? "/promo-video-picker"
|
|
17
|
+
: "/promovideo";
|
|
18
|
+
return `${client.credentials.baseUrl}${pathname}?${params.toString()}`;
|
|
16
19
|
}
|
|
17
20
|
export function buildPromoVideoCandidateRequest(args) {
|
|
18
21
|
return { ...args, candidateCount: 3 };
|
|
@@ -53,26 +56,26 @@ export function registerPromoVideoTools(server, client) {
|
|
|
53
56
|
if (!result?.candidateKey || result?.candidates?.length !== 3) {
|
|
54
57
|
throw new Error("Promo video generation did not return a reusable three-option picker");
|
|
55
58
|
}
|
|
56
|
-
const
|
|
59
|
+
const pickerUrl = buildPromoVideoDashboardUrl(client, {
|
|
57
60
|
generationId: args.projectId,
|
|
58
61
|
candidateKey: result.candidateKey,
|
|
59
62
|
replaceVariantId: args.replaceVariantId,
|
|
60
63
|
});
|
|
61
|
-
await openUrl(server,
|
|
64
|
+
await openUrl(server, pickerUrl, "Compare three personalized promo-video candidates and choose which one to create.", { signal: extra.signal });
|
|
62
65
|
return {
|
|
63
66
|
content: [
|
|
64
67
|
{
|
|
65
68
|
type: "text",
|
|
66
69
|
text: [
|
|
67
70
|
"Prepared three personalized promo-video candidates without creating a saved variant yet.",
|
|
68
|
-
`Candidate picker URL: ${
|
|
71
|
+
`Candidate picker URL: ${pickerUrl}`,
|
|
69
72
|
"The dashboard picker previews all three options. Choosing one creates that variant and opens it in the promo-video editor.",
|
|
70
73
|
].join("\n"),
|
|
71
74
|
},
|
|
72
75
|
],
|
|
73
76
|
structuredContent: {
|
|
74
77
|
success: true,
|
|
75
|
-
data: { ...result, editorUrl },
|
|
78
|
+
data: { ...result, editorUrl: pickerUrl },
|
|
76
79
|
message: "Prepared promo video candidates",
|
|
77
80
|
},
|
|
78
81
|
};
|
|
@@ -92,7 +95,7 @@ export function registerPromoVideoTools(server, client) {
|
|
|
92
95
|
}, async ({ generationId, variantId }) => {
|
|
93
96
|
try {
|
|
94
97
|
const result = await client.getPromoVideo(generationId, variantId);
|
|
95
|
-
const editorUrl =
|
|
98
|
+
const editorUrl = buildPromoVideoDashboardUrl(client, {
|
|
96
99
|
generationId,
|
|
97
100
|
variantId,
|
|
98
101
|
});
|
|
@@ -168,7 +171,7 @@ export function registerPromoVideoTools(server, client) {
|
|
|
168
171
|
const { readReceipt: _readReceipt, ...updateArgs } = args;
|
|
169
172
|
const result = await client.updatePromoVideo(updateArgs);
|
|
170
173
|
promoVideoReadReceipts.consume(receiptArgs);
|
|
171
|
-
const editorUrl =
|
|
174
|
+
const editorUrl = buildPromoVideoDashboardUrl(client, {
|
|
172
175
|
generationId: args.projectId,
|
|
173
176
|
variantId: args.variantId,
|
|
174
177
|
});
|