@sync.so/mcp-server 0.1.4 → 0.2.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/README.md +261 -86
- package/deploy/generation-status/app.html.gz +0 -0
- package/deploy/generation-status/release.json +6 -0
- package/dist/abort-signals.d.ts +6 -0
- package/dist/abort-signals.d.ts.map +1 -0
- package/dist/abort-signals.js +23 -0
- package/dist/account-features.d.ts +7 -0
- package/dist/account-features.d.ts.map +1 -0
- package/dist/account-features.js +48 -0
- package/dist/app-upload-relay.d.ts +13 -0
- package/dist/app-upload-relay.d.ts.map +1 -0
- package/dist/app-upload-relay.js +136 -0
- package/dist/asset-thumbnail.d.ts +3 -0
- package/dist/asset-thumbnail.d.ts.map +1 -0
- package/dist/asset-thumbnail.js +2 -0
- package/dist/auth/async-context.d.ts +5 -0
- package/dist/auth/async-context.d.ts.map +1 -0
- package/dist/auth/async-context.js +14 -0
- package/dist/auth/bearer-auth.d.ts +16 -0
- package/dist/auth/bearer-auth.d.ts.map +1 -0
- package/dist/auth/bearer-auth.js +55 -0
- package/dist/auth/client-credentials.d.ts +13 -0
- package/dist/auth/client-credentials.d.ts.map +1 -0
- package/dist/auth/client-credentials.js +67 -0
- package/dist/auth/device-auth.d.ts.map +1 -1
- package/dist/auth/device-auth.js +3 -2
- package/dist/auth/fixture-cleanup.d.ts +4 -0
- package/dist/auth/fixture-cleanup.d.ts.map +1 -0
- package/dist/auth/fixture-cleanup.js +9 -0
- package/dist/auth/index.d.ts +2 -0
- package/dist/auth/index.d.ts.map +1 -1
- package/dist/auth/index.js +2 -0
- package/dist/auth/oauth-diagnostics.d.ts +11 -0
- package/dist/auth/oauth-diagnostics.d.ts.map +1 -0
- package/dist/auth/oauth-diagnostics.js +78 -0
- package/dist/auth/oauth-provider.d.ts +16 -0
- package/dist/auth/oauth-provider.d.ts.map +1 -0
- package/dist/auth/oauth-provider.js +107 -0
- package/dist/auth/resource-metadata.d.ts +2 -0
- package/dist/auth/resource-metadata.d.ts.map +1 -0
- package/dist/auth/resource-metadata.js +3 -0
- package/dist/auth/token-verification.d.ts +11 -0
- package/dist/auth/token-verification.d.ts.map +1 -0
- package/dist/auth/token-verification.js +99 -0
- package/dist/auth/verification-cache.d.ts +8 -0
- package/dist/auth/verification-cache.d.ts.map +1 -0
- package/dist/auth/verification-cache.js +111 -0
- package/dist/chatgpt-analytics.d.ts +22 -0
- package/dist/chatgpt-analytics.d.ts.map +1 -0
- package/dist/chatgpt-analytics.js +172 -0
- package/dist/chatgpt-app.d.ts +15 -0
- package/dist/chatgpt-app.d.ts.map +1 -0
- package/dist/chatgpt-app.js +133 -0
- package/dist/chatgpt-release.d.ts +12 -0
- package/dist/chatgpt-release.d.ts.map +1 -0
- package/dist/chatgpt-release.js +30 -0
- package/dist/cli.js +30 -16
- package/dist/client-profile.d.ts +10 -0
- package/dist/client-profile.d.ts.map +1 -0
- package/dist/client-profile.js +45 -0
- package/dist/config.d.ts +17 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +52 -0
- package/dist/generation-status-html.d.ts +2 -0
- package/dist/generation-status-html.d.ts.map +1 -0
- package/dist/generation-status-html.js +26 -0
- package/dist/generation-status.d.ts +6 -0
- package/dist/generation-status.d.ts.map +1 -0
- package/dist/generation-status.js +37 -0
- package/dist/http-client.d.ts +45 -1
- package/dist/http-client.d.ts.map +1 -1
- package/dist/http-client.js +171 -34
- package/dist/http-server.d.ts +53 -0
- package/dist/http-server.d.ts.map +1 -0
- package/dist/http-server.js +753 -0
- package/dist/http-session.d.ts +18 -0
- package/dist/http-session.d.ts.map +1 -0
- package/dist/http-session.js +189 -0
- package/dist/index.d.ts +4 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -2
- package/dist/oauth-response.d.ts +3 -0
- package/dist/oauth-response.d.ts.map +1 -0
- package/dist/oauth-response.js +30 -0
- package/dist/openapi/index.d.ts +2 -2
- package/dist/openapi/index.d.ts.map +1 -1
- package/dist/openapi/index.js +1 -1
- package/dist/openapi/parser.d.ts +2 -0
- package/dist/openapi/parser.d.ts.map +1 -1
- package/dist/openapi/parser.js +41 -1
- package/dist/openapi/types.d.ts +3 -0
- package/dist/openapi/types.d.ts.map +1 -1
- package/dist/organization-selection.d.ts +61 -0
- package/dist/organization-selection.d.ts.map +1 -0
- package/dist/organization-selection.js +142 -0
- package/dist/rate-limits.d.ts +13 -0
- package/dist/rate-limits.d.ts.map +1 -0
- package/dist/rate-limits.js +67 -0
- package/dist/revision.d.ts +6 -0
- package/dist/revision.d.ts.map +1 -0
- package/dist/revision.js +17 -0
- package/dist/runtime-diagnostics.d.ts +32 -0
- package/dist/runtime-diagnostics.d.ts.map +1 -0
- package/dist/runtime-diagnostics.js +58 -0
- package/dist/server.d.ts +23 -2
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +893 -32
- package/dist/session-registry.d.ts +54 -0
- package/dist/session-registry.d.ts.map +1 -0
- package/dist/session-registry.js +202 -0
- package/dist/session-telemetry.d.ts +13 -0
- package/dist/session-telemetry.d.ts.map +1 -0
- package/dist/session-telemetry.js +87 -0
- package/dist/spend-confirmation.d.ts +21 -0
- package/dist/spend-confirmation.d.ts.map +1 -0
- package/dist/spend-confirmation.js +105 -0
- package/dist/tool-analytics-context.d.ts +11 -0
- package/dist/tool-analytics-context.d.ts.map +1 -0
- package/dist/tool-analytics-context.js +8 -0
- package/dist/tool-names.d.ts +35 -0
- package/dist/tool-names.d.ts.map +1 -0
- package/dist/tool-names.js +133 -0
- package/dist/tools/account-context.d.ts +5 -0
- package/dist/tools/account-context.d.ts.map +1 -0
- package/dist/tools/account-context.js +131 -0
- package/dist/tools/account-management.d.ts +4 -0
- package/dist/tools/account-management.d.ts.map +1 -0
- package/dist/tools/account-management.js +111 -0
- package/dist/tools/app-tools.d.ts +31 -0
- package/dist/tools/app-tools.d.ts.map +1 -0
- package/dist/tools/app-tools.js +350 -0
- package/dist/tools/asset-analysis.d.ts +4 -0
- package/dist/tools/asset-analysis.d.ts.map +1 -0
- package/dist/tools/asset-analysis.js +172 -0
- package/dist/tools/asset-creators.d.ts +4 -0
- package/dist/tools/asset-creators.d.ts.map +1 -0
- package/dist/tools/asset-creators.js +38 -0
- package/dist/tools/audio-trim.d.ts +6 -0
- package/dist/tools/audio-trim.d.ts.map +1 -0
- package/dist/tools/audio-trim.js +79 -0
- package/dist/tools/batch-results.d.ts +16 -0
- package/dist/tools/batch-results.d.ts.map +1 -0
- package/dist/tools/batch-results.js +103 -0
- package/dist/tools/batches.d.ts +28 -0
- package/dist/tools/batches.d.ts.map +1 -0
- package/dist/tools/batches.js +293 -0
- package/dist/tools/dialogue.d.ts +6 -0
- package/dist/tools/dialogue.d.ts.map +1 -0
- package/dist/tools/dialogue.js +309 -0
- package/dist/tools/estimate-duration.d.ts +35 -0
- package/dist/tools/estimate-duration.d.ts.map +1 -0
- package/dist/tools/estimate-duration.js +66 -0
- package/dist/tools/feedback.d.ts +14 -0
- package/dist/tools/feedback.d.ts.map +1 -0
- package/dist/tools/feedback.js +72 -0
- package/dist/tools/generated-adapters.d.ts +25 -0
- package/dist/tools/generated-adapters.d.ts.map +1 -0
- package/dist/tools/generated-adapters.js +143 -0
- package/dist/tools/generation-context.d.ts +14 -0
- package/dist/tools/generation-context.d.ts.map +1 -0
- package/dist/tools/generation-context.js +39 -0
- package/dist/tools/generation-continuation.d.ts +4 -0
- package/dist/tools/generation-continuation.d.ts.map +1 -0
- package/dist/tools/generation-continuation.js +124 -0
- package/dist/tools/generation-costs.d.ts +6 -0
- package/dist/tools/generation-costs.d.ts.map +1 -0
- package/dist/tools/generation-costs.js +67 -0
- package/dist/tools/generation-output.d.ts +3 -0
- package/dist/tools/generation-output.d.ts.map +1 -0
- package/dist/tools/generation-output.js +39 -0
- package/dist/tools/generator.d.ts +35 -2
- package/dist/tools/generator.d.ts.map +1 -1
- package/dist/tools/generator.js +127 -9
- package/dist/tools/job-status.d.ts +35 -0
- package/dist/tools/job-status.d.ts.map +1 -0
- package/dist/tools/job-status.js +92 -0
- package/dist/tools/job-wait.d.ts +41 -0
- package/dist/tools/job-wait.d.ts.map +1 -0
- package/dist/tools/job-wait.js +84 -0
- package/dist/tools/jobs.d.ts +3 -0
- package/dist/tools/jobs.d.ts.map +1 -0
- package/dist/tools/jobs.js +210 -0
- package/dist/tools/media-upload.d.ts +27 -0
- package/dist/tools/media-upload.d.ts.map +1 -0
- package/dist/tools/media-upload.js +247 -0
- package/dist/tools/model-options.d.ts +36 -0
- package/dist/tools/model-options.d.ts.map +1 -0
- package/dist/tools/model-options.js +48 -0
- package/dist/tools/organization-choice.d.ts +9 -0
- package/dist/tools/organization-choice.d.ts.map +1 -0
- package/dist/tools/organization-choice.js +136 -0
- package/dist/tools/organizations.d.ts +56 -0
- package/dist/tools/organizations.d.ts.map +1 -0
- package/dist/tools/organizations.js +235 -0
- package/dist/tools/output-schemas.d.ts +54 -0
- package/dist/tools/output-schemas.d.ts.map +1 -0
- package/dist/tools/output-schemas.js +58 -0
- package/dist/tools/overrides.d.ts +3 -0
- package/dist/tools/overrides.d.ts.map +1 -1
- package/dist/tools/overrides.js +113 -15
- package/dist/tools/playback-metadata.d.ts +4 -0
- package/dist/tools/playback-metadata.d.ts.map +1 -0
- package/dist/tools/playback-metadata.js +51 -0
- package/dist/tools/project-destination.d.ts +13 -0
- package/dist/tools/project-destination.d.ts.map +1 -0
- package/dist/tools/project-destination.js +45 -0
- package/dist/tools/project-history.d.ts +6 -0
- package/dist/tools/project-history.d.ts.map +1 -0
- package/dist/tools/project-history.js +45 -0
- package/dist/tools/project-intent.d.ts +8 -0
- package/dist/tools/project-intent.d.ts.map +1 -0
- package/dist/tools/project-intent.js +87 -0
- package/dist/tools/running-submissions.d.ts +7 -0
- package/dist/tools/running-submissions.d.ts.map +1 -0
- package/dist/tools/running-submissions.js +91 -0
- package/dist/tools/speech.d.ts +6 -0
- package/dist/tools/speech.d.ts.map +1 -0
- package/dist/tools/speech.js +116 -0
- package/dist/tools/studio-draft.d.ts +7 -0
- package/dist/tools/studio-draft.d.ts.map +1 -0
- package/dist/tools/studio-draft.js +195 -0
- package/dist/tools/studio-view-contract.d.ts +184 -0
- package/dist/tools/studio-view-contract.d.ts.map +1 -0
- package/dist/tools/studio-view-contract.js +67 -0
- package/dist/tools/studio-view.d.ts +6 -0
- package/dist/tools/studio-view.d.ts.map +1 -0
- package/dist/tools/studio-view.js +126 -0
- package/dist/tools/timeline-lipsync.d.ts +316 -0
- package/dist/tools/timeline-lipsync.d.ts.map +1 -0
- package/dist/tools/timeline-lipsync.js +304 -0
- package/dist/tools/translation-preview.d.ts +8 -0
- package/dist/tools/translation-preview.d.ts.map +1 -0
- package/dist/tools/translation-preview.js +146 -0
- package/dist/tools/translation.d.ts +6 -0
- package/dist/tools/translation.d.ts.map +1 -0
- package/dist/tools/translation.js +131 -0
- package/dist/tools/upload-widget.d.ts +13 -0
- package/dist/tools/upload-widget.d.ts.map +1 -0
- package/dist/tools/upload-widget.js +788 -0
- package/dist/toolsets.d.ts +18 -0
- package/dist/toolsets.d.ts.map +1 -0
- package/dist/toolsets.js +157 -0
- package/dist/update-check.d.ts +38 -0
- package/dist/update-check.d.ts.map +1 -0
- package/dist/update-check.js +150 -0
- package/dist/upload-runtime.d.ts +45 -0
- package/dist/upload-runtime.d.ts.map +1 -0
- package/dist/upload-runtime.js +156 -0
- package/dist/upload-source.d.ts +4 -0
- package/dist/upload-source.d.ts.map +1 -0
- package/dist/upload-source.js +46 -0
- package/dist/version.d.ts +2 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +3 -0
- package/package.json +77 -31
- package/dist/openapi/parser.test.d.ts +0 -2
- package/dist/openapi/parser.test.d.ts.map +0 -1
- package/dist/openapi/parser.test.js +0 -188
- package/dist/tools/generator.test.d.ts +0 -2
- package/dist/tools/generator.test.d.ts.map +0 -1
- package/dist/tools/generator.test.js +0 -178
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"resource-metadata.d.ts","sourceRoot":"","sources":["../../src/auth/resource-metadata.ts"],"names":[],"mappings":"AAAA,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAKxD"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { AuthInfo } from '@modelcontextprotocol/server';
|
|
2
|
+
import { TemporarilyUnavailableError } from '@modelcontextprotocol/server-legacy/auth';
|
|
3
|
+
export declare const VERIFICATION_TIMEOUT_MS = 5000;
|
|
4
|
+
export declare class VerificationUnavailableError extends TemporarilyUnavailableError {
|
|
5
|
+
readonly status: 429 | 502 | 503;
|
|
6
|
+
readonly retryAfter?: string | undefined;
|
|
7
|
+
constructor(status?: 429 | 502 | 503, retryAfter?: string | undefined);
|
|
8
|
+
}
|
|
9
|
+
export declare function parseRetryAfter(value: string | null): string | undefined;
|
|
10
|
+
export declare function verifySyncAccessToken(apiBaseUrl: string, token: string, signal?: AbortSignal): Promise<AuthInfo>;
|
|
11
|
+
//# sourceMappingURL=token-verification.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"token-verification.d.ts","sourceRoot":"","sources":["../../src/auth/token-verification.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,8BAA8B,CAAC;AAC7D,OAAO,EAEL,2BAA2B,EAC5B,MAAM,0CAA0C,CAAC;AAGlD,eAAO,MAAM,uBAAuB,OAAQ,CAAC;AAG7C,qBAAa,4BAA6B,SAAQ,2BAA2B;IAEzE,QAAQ,CAAC,MAAM,EAAE,GAAG,GAAG,GAAG,GAAG,GAAG;IAChC,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM;IAF9B,YACW,MAAM,GAAE,GAAG,GAAG,GAAG,GAAG,GAAS,EAC7B,UAAU,CAAC,EAAE,MAAM,YAAA,EAG7B;CACF;AAED,wBAAgB,eAAe,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,GAAG,MAAM,GAAG,SAAS,CAOxE;AAYD,wBAAsB,qBAAqB,CACzC,UAAU,EAAE,MAAM,EAClB,KAAK,EAAE,MAAM,EACb,MAAM,CAAC,EAAE,WAAW,GACnB,OAAO,CAAC,QAAQ,CAAC,CA8DnB"}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { InvalidTokenError, TemporarilyUnavailableError, } from '@modelcontextprotocol/server-legacy/auth';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
export const VERIFICATION_TIMEOUT_MS = 5_000;
|
|
4
|
+
// Never include upstream bodies, exceptions, or credentials in public errors.
|
|
5
|
+
export class VerificationUnavailableError extends TemporarilyUnavailableError {
|
|
6
|
+
status;
|
|
7
|
+
retryAfter;
|
|
8
|
+
constructor(status = 503, retryAfter) {
|
|
9
|
+
super('Token verification is temporarily unavailable');
|
|
10
|
+
this.status = status;
|
|
11
|
+
this.retryAfter = retryAfter;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
export function parseRetryAfter(value) {
|
|
15
|
+
if (!value)
|
|
16
|
+
return undefined;
|
|
17
|
+
if (/^\d{1,15}$/.test(value) && Number.isSafeInteger(Number(value)))
|
|
18
|
+
return value;
|
|
19
|
+
// Accept canonical IMF-fixdate only, not Date.parse's permissive numeric forms.
|
|
20
|
+
const time = Date.parse(value);
|
|
21
|
+
if (Number.isFinite(time) && new Date(time).toUTCString() === value)
|
|
22
|
+
return value;
|
|
23
|
+
return undefined;
|
|
24
|
+
}
|
|
25
|
+
const userInfoSchema = z.object({
|
|
26
|
+
sub: z.string().refine((value) => value.trim().length > 0),
|
|
27
|
+
client_id: z.string().refine((value) => value.trim().length > 0),
|
|
28
|
+
expires_at: z.number().int().positive().optional(),
|
|
29
|
+
organization_id: z
|
|
30
|
+
.string()
|
|
31
|
+
.refine((value) => value.trim().length > 0)
|
|
32
|
+
.optional(),
|
|
33
|
+
});
|
|
34
|
+
export async function verifySyncAccessToken(apiBaseUrl, token, signal) {
|
|
35
|
+
const controller = new AbortController();
|
|
36
|
+
const cancel = () => controller.abort();
|
|
37
|
+
signal?.addEventListener('abort', cancel, { once: true });
|
|
38
|
+
if (signal?.aborted)
|
|
39
|
+
cancel();
|
|
40
|
+
const timer = setTimeout(cancel, VERIFICATION_TIMEOUT_MS);
|
|
41
|
+
let onAbort = () => { };
|
|
42
|
+
const aborted = new Promise((_resolve, reject) => {
|
|
43
|
+
onAbort = () => reject(new VerificationUnavailableError());
|
|
44
|
+
controller.signal.addEventListener('abort', onAbort, { once: true });
|
|
45
|
+
if (controller.signal.aborted)
|
|
46
|
+
onAbort();
|
|
47
|
+
});
|
|
48
|
+
try {
|
|
49
|
+
// Race the whole body read as well as headers. Abort also closes native fetch I/O.
|
|
50
|
+
return await Promise.race([
|
|
51
|
+
aborted,
|
|
52
|
+
(async () => {
|
|
53
|
+
if (controller.signal.aborted)
|
|
54
|
+
throw new VerificationUnavailableError();
|
|
55
|
+
const res = await fetch(`${apiBaseUrl}/v2/oauth/userinfo`, {
|
|
56
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
57
|
+
signal: controller.signal,
|
|
58
|
+
redirect: 'error',
|
|
59
|
+
});
|
|
60
|
+
if (!res.ok) {
|
|
61
|
+
const retryAfter = parseRetryAfter(res.headers.get('retry-after'));
|
|
62
|
+
void res.body?.cancel().catch(() => { });
|
|
63
|
+
if (res.status === 401 || res.status === 403) {
|
|
64
|
+
throw new InvalidTokenError('Invalid or expired token');
|
|
65
|
+
}
|
|
66
|
+
throw new VerificationUnavailableError(res.status === 429 ? 429 : res.status >= 500 ? 503 : 502, retryAfter);
|
|
67
|
+
}
|
|
68
|
+
const parsed = userInfoSchema.safeParse(await res.json());
|
|
69
|
+
if (!parsed.success)
|
|
70
|
+
throw new VerificationUnavailableError(502);
|
|
71
|
+
return {
|
|
72
|
+
token,
|
|
73
|
+
clientId: parsed.data.client_id,
|
|
74
|
+
// Stable verified identity for A's session ownership checks. Never use
|
|
75
|
+
// token bytes/expiry or copy arbitrary upstream fields into this object.
|
|
76
|
+
extra: {
|
|
77
|
+
sub: parsed.data.sub,
|
|
78
|
+
...(parsed.data.organization_id === undefined
|
|
79
|
+
? {}
|
|
80
|
+
: { organizationId: parsed.data.organization_id }),
|
|
81
|
+
},
|
|
82
|
+
scopes: [],
|
|
83
|
+
// Preserve the legacy expiry fallback; the provider bounds reuse to 15 seconds.
|
|
84
|
+
expiresAt: parsed.data.expires_at ?? Math.floor(Date.now() / 1000) + 3600,
|
|
85
|
+
};
|
|
86
|
+
})(),
|
|
87
|
+
]);
|
|
88
|
+
}
|
|
89
|
+
catch (error) {
|
|
90
|
+
if (error instanceof InvalidTokenError || error instanceof VerificationUnavailableError)
|
|
91
|
+
throw error;
|
|
92
|
+
throw new VerificationUnavailableError(error instanceof SyntaxError ? 502 : 503);
|
|
93
|
+
}
|
|
94
|
+
finally {
|
|
95
|
+
clearTimeout(timer);
|
|
96
|
+
signal?.removeEventListener('abort', cancel);
|
|
97
|
+
controller.signal.removeEventListener('abort', onAbort);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { AuthInfo } from '@modelcontextprotocol/server';
|
|
2
|
+
export declare const VERIFICATION_CACHE_MS = 15000;
|
|
3
|
+
/** Provider-scoped cache: successful checks only, bounded by both TTL and token expiry. */
|
|
4
|
+
export declare function createTokenVerifier(apiBaseUrl: string): {
|
|
5
|
+
verify: (token: string, signal?: AbortSignal) => Promise<AuthInfo>;
|
|
6
|
+
clear: () => void;
|
|
7
|
+
};
|
|
8
|
+
//# sourceMappingURL=verification-cache.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"verification-cache.d.ts","sourceRoot":"","sources":["../../src/auth/verification-cache.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,8BAA8B,CAAC;AAG7D,eAAO,MAAM,qBAAqB,QAAS,CAAC;AAK5C,2FAA2F;AAC3F,wBAAgB,mBAAmB,CAAC,UAAU,EAAE,MAAM;oBAcvB,MAAM,WAAW,WAAW,KAAG,OAAO,CAAC,QAAQ,CAAC;;EAkF9E"}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { VerificationUnavailableError, verifySyncAccessToken } from './token-verification.js';
|
|
3
|
+
export const VERIFICATION_CACHE_MS = 15_000;
|
|
4
|
+
const MAX_ENTRIES = 512;
|
|
5
|
+
const MAX_PENDING = 128;
|
|
6
|
+
/** Provider-scoped cache: successful checks only, bounded by both TTL and token expiry. */
|
|
7
|
+
export function createTokenVerifier(apiBaseUrl) {
|
|
8
|
+
const cache = new Map();
|
|
9
|
+
const pending = new Map();
|
|
10
|
+
let epoch = 0;
|
|
11
|
+
const cooldowns = new Map();
|
|
12
|
+
function clear() {
|
|
13
|
+
epoch++;
|
|
14
|
+
cache.clear();
|
|
15
|
+
cooldowns.clear();
|
|
16
|
+
for (const entry of pending.values())
|
|
17
|
+
entry.controller.abort();
|
|
18
|
+
pending.clear();
|
|
19
|
+
}
|
|
20
|
+
async function verify(token, signal) {
|
|
21
|
+
if (signal?.aborted)
|
|
22
|
+
throw new VerificationUnavailableError();
|
|
23
|
+
const key = createHash('sha256').update(token).digest('hex');
|
|
24
|
+
const now = Date.now();
|
|
25
|
+
const cached = cache.get(key);
|
|
26
|
+
if (cached && cached.until > now) {
|
|
27
|
+
cache.delete(key);
|
|
28
|
+
cache.set(key, cached);
|
|
29
|
+
return structuredClone(cached.info);
|
|
30
|
+
}
|
|
31
|
+
cache.delete(key);
|
|
32
|
+
const cooldownUntil = cooldowns.get(key) ?? 0;
|
|
33
|
+
if (cooldownUntil > now)
|
|
34
|
+
throw new VerificationUnavailableError(429, String(Math.ceil((cooldownUntil - now) / 1000)));
|
|
35
|
+
cooldowns.delete(key);
|
|
36
|
+
let entry = pending.get(key);
|
|
37
|
+
if (!entry) {
|
|
38
|
+
if (pending.size >= MAX_PENDING)
|
|
39
|
+
throw new VerificationUnavailableError(503, '1');
|
|
40
|
+
const controller = new AbortController();
|
|
41
|
+
const startedEpoch = epoch;
|
|
42
|
+
entry = { controller, waiters: 0, promise: Promise.resolve({}) };
|
|
43
|
+
const current = entry;
|
|
44
|
+
entry.promise = verifySyncAccessToken(apiBaseUrl, token, controller.signal)
|
|
45
|
+
.then((info) => {
|
|
46
|
+
const until = Math.min(now + VERIFICATION_CACHE_MS, (info.expiresAt ?? 0) * 1000);
|
|
47
|
+
if (!controller.signal.aborted && epoch === startedEpoch && until > Date.now()) {
|
|
48
|
+
for (const [cacheKey, value] of cache)
|
|
49
|
+
if (value.until <= Date.now())
|
|
50
|
+
cache.delete(cacheKey);
|
|
51
|
+
if (cache.size >= MAX_ENTRIES)
|
|
52
|
+
cache.delete(cache.keys().next().value);
|
|
53
|
+
cache.set(key, { info: structuredClone(info), until });
|
|
54
|
+
}
|
|
55
|
+
return info;
|
|
56
|
+
})
|
|
57
|
+
.catch((error) => {
|
|
58
|
+
if (error instanceof VerificationUnavailableError && error.status === 429) {
|
|
59
|
+
const delay = error.retryAfter
|
|
60
|
+
? /^\d+$/.test(error.retryAfter)
|
|
61
|
+
? Number(error.retryAfter) * 1000
|
|
62
|
+
: Date.parse(error.retryAfter) - Date.now()
|
|
63
|
+
: 1000;
|
|
64
|
+
for (const [cooldownKey, until] of cooldowns)
|
|
65
|
+
if (until <= Date.now())
|
|
66
|
+
cooldowns.delete(cooldownKey);
|
|
67
|
+
if (cooldowns.size >= MAX_ENTRIES)
|
|
68
|
+
cooldowns.delete(cooldowns.keys().next().value);
|
|
69
|
+
const boundedDelay = Number.isFinite(delay)
|
|
70
|
+
? Math.min(VERIFICATION_CACHE_MS, Math.max(1000, delay))
|
|
71
|
+
: 1000;
|
|
72
|
+
if (epoch === startedEpoch)
|
|
73
|
+
cooldowns.set(key, Date.now() + boundedDelay);
|
|
74
|
+
}
|
|
75
|
+
throw error;
|
|
76
|
+
})
|
|
77
|
+
.finally(() => {
|
|
78
|
+
if (pending.get(key) === current)
|
|
79
|
+
pending.delete(key);
|
|
80
|
+
});
|
|
81
|
+
pending.set(key, entry);
|
|
82
|
+
}
|
|
83
|
+
// Cancelling one HTTP request must not cancel another caller's shared check.
|
|
84
|
+
entry.waiters++;
|
|
85
|
+
const current = entry;
|
|
86
|
+
return new Promise((resolve, reject) => {
|
|
87
|
+
let settled = false;
|
|
88
|
+
const finish = (error, info) => {
|
|
89
|
+
if (settled)
|
|
90
|
+
return;
|
|
91
|
+
settled = true;
|
|
92
|
+
signal?.removeEventListener('abort', abort);
|
|
93
|
+
current.waiters--;
|
|
94
|
+
if (current.waiters === 0 && pending.get(key) === current) {
|
|
95
|
+
pending.delete(key);
|
|
96
|
+
current.controller.abort();
|
|
97
|
+
}
|
|
98
|
+
if (error)
|
|
99
|
+
reject(error);
|
|
100
|
+
else
|
|
101
|
+
resolve(structuredClone(info));
|
|
102
|
+
};
|
|
103
|
+
const abort = () => finish(new VerificationUnavailableError());
|
|
104
|
+
signal?.addEventListener('abort', abort, { once: true });
|
|
105
|
+
current.promise.then((info) => finish(undefined, info), (error) => finish(error));
|
|
106
|
+
if (signal?.aborted)
|
|
107
|
+
abort();
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
return { verify, clear };
|
|
111
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { type HttpClient } from './http-client.js';
|
|
3
|
+
import type { ParsedOperation } from './openapi/types.js';
|
|
4
|
+
import type { McpToolDefinition } from './tools/generator.js';
|
|
5
|
+
export declare const uiAnalyticsSchema: z.ZodObject<{
|
|
6
|
+
version: z.ZodLiteral<1>;
|
|
7
|
+
invocation_source: z.ZodEnum<{
|
|
8
|
+
background: "background";
|
|
9
|
+
ui: "ui";
|
|
10
|
+
}>;
|
|
11
|
+
journey_id: z.ZodUUID;
|
|
12
|
+
action_id: z.ZodOptional<z.ZodUUID>;
|
|
13
|
+
}, z.core.$strict>;
|
|
14
|
+
/** Which tool events the API accepts, read from its published schema. */
|
|
15
|
+
export type ToolAnalytics = {
|
|
16
|
+
tools: ReadonlySet<string>;
|
|
17
|
+
errorDetails: boolean;
|
|
18
|
+
};
|
|
19
|
+
export declare function toolAnalyticsSupport(operations: ParsedOperation[]): ToolAnalytics | undefined;
|
|
20
|
+
export declare function trackToolExecution<T>(name: string, args: Record<string, unknown>, meta: Record<string, unknown> | undefined, profile: string, http: HttpClient, execute: (args: Record<string, unknown>) => Promise<T>, analytics?: ToolAnalytics): Promise<T>;
|
|
21
|
+
export declare function createAnalyticsTool(http: HttpClient): McpToolDefinition;
|
|
22
|
+
//# sourceMappingURL=chatgpt-analytics.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"chatgpt-analytics.d.ts","sourceRoot":"","sources":["../src/chatgpt-analytics.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EAAgC,KAAK,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAEjF,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAC;AAM1D,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,sBAAsB,CAAC;AAE9D,eAAO,MAAM,iBAAiB;;;;;;;;kBAOnB,CAAC;AAsCZ,yEAAyE;AACzE,MAAM,MAAM,aAAa,GAAG;IAAE,KAAK,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;IAAC,YAAY,EAAE,OAAO,CAAA;CAAE,CAAC;AAIlF,wBAAgB,oBAAoB,CAAC,UAAU,EAAE,eAAe,EAAE,GAAG,aAAa,GAAG,SAAS,CAe7F;AA+BD,wBAAsB,kBAAkB,CAAC,CAAC,EACxC,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC7B,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,EACzC,OAAO,EAAE,MAAM,EACf,IAAI,EAAE,UAAU,EAChB,OAAO,EAAE,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,OAAO,CAAC,CAAC,CAAC,EACtD,SAAS,CAAC,EAAE,aAAa,GACxB,OAAO,CAAC,CAAC,CAAC,CAmDZ;AAED,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,UAAU,GAAG,iBAAiB,CA+BvE"}
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { ApiRequestError, cameFromApi } from './http-client.js';
|
|
4
|
+
import { schemaProperty } from './openapi/parser.js';
|
|
5
|
+
import { getToolAnalytics, runWithToolAnalytics, } from './tool-analytics-context.js';
|
|
6
|
+
export const uiAnalyticsSchema = z
|
|
7
|
+
.object({
|
|
8
|
+
version: z.literal(1),
|
|
9
|
+
invocation_source: z.enum(['ui', 'background']),
|
|
10
|
+
journey_id: z.uuid(),
|
|
11
|
+
action_id: z.uuid().optional(),
|
|
12
|
+
})
|
|
13
|
+
.strict();
|
|
14
|
+
const attributionSchema = uiAnalyticsSchema.extend({
|
|
15
|
+
invocation_source: z.enum(['model', 'ui', 'background', 'unknown']),
|
|
16
|
+
journey_id: z.uuid().optional(),
|
|
17
|
+
conversation_id: z
|
|
18
|
+
.string()
|
|
19
|
+
.regex(/^[a-f0-9]{64}$/)
|
|
20
|
+
.optional(),
|
|
21
|
+
});
|
|
22
|
+
// The API's names for these tools: the collector predates the tool rename, so events
|
|
23
|
+
// keep the legacy names (see previousToolName in tool-names.ts).
|
|
24
|
+
const toolNames = [
|
|
25
|
+
'open-sync-app',
|
|
26
|
+
'create-lipsync',
|
|
27
|
+
'create-lipsync-from-assets',
|
|
28
|
+
'create-translate-and-dub',
|
|
29
|
+
'create-dialogue-video',
|
|
30
|
+
'update-studio-draft',
|
|
31
|
+
'open-studio-project',
|
|
32
|
+
];
|
|
33
|
+
const eventSchema = z
|
|
34
|
+
.object({
|
|
35
|
+
event: z.enum([
|
|
36
|
+
'chatgpt_ui_loaded',
|
|
37
|
+
'chatgpt_project_opened',
|
|
38
|
+
'chatgpt_asset_selected',
|
|
39
|
+
'chatgpt_generate_clicked',
|
|
40
|
+
'chatgpt_tool_called',
|
|
41
|
+
]),
|
|
42
|
+
event_id: z.uuid(),
|
|
43
|
+
attribution: attributionSchema,
|
|
44
|
+
tool_name: z.enum(toolNames).optional(),
|
|
45
|
+
outcome: z.enum(['success', 'error']).optional(),
|
|
46
|
+
duration_ms: z.number().finite().nonnegative().max(3_600_000).optional(),
|
|
47
|
+
})
|
|
48
|
+
.strict();
|
|
49
|
+
// The API rejects a whole event for an unknown tool or field, so new ones wait until
|
|
50
|
+
// the deployed API's schema lists them.
|
|
51
|
+
export function toolAnalyticsSupport(operations) {
|
|
52
|
+
const operation = operations.find((candidate) => candidate.operationId === 'analytics_captureChatgptEvent');
|
|
53
|
+
if (!operation)
|
|
54
|
+
return undefined;
|
|
55
|
+
const schema = operation.requestBody?.schema;
|
|
56
|
+
const names = schemaProperty(schema, 'tool_name')?.enum;
|
|
57
|
+
return {
|
|
58
|
+
tools: new Set(Array.isArray(names) ? names.filter((name) => typeof name === 'string') : toolNames),
|
|
59
|
+
errorDetails: ['error_source', 'error_status', 'error_code'].every((key) => Boolean(schemaProperty(schema, key))),
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
const ERROR_CODE = /^[A-Za-z0-9_.:-]{1,80}$/;
|
|
63
|
+
function errorCode(error, reported) {
|
|
64
|
+
if (error instanceof ApiRequestError)
|
|
65
|
+
return error.code;
|
|
66
|
+
if (error instanceof z.ZodError)
|
|
67
|
+
return 'invalid_arguments';
|
|
68
|
+
if (error instanceof Error && ['AbortError', 'TimeoutError'].includes(error.name))
|
|
69
|
+
return 'aborted';
|
|
70
|
+
return reported?.code;
|
|
71
|
+
}
|
|
72
|
+
// Why a call failed, without its message: messages can echo user input.
|
|
73
|
+
function errorDetails(error, result) {
|
|
74
|
+
const reported = result && typeof result === 'object' && 'structuredContent' in result
|
|
75
|
+
? result.structuredContent?.error
|
|
76
|
+
: undefined;
|
|
77
|
+
const status = error instanceof ApiRequestError ? error.status : reported?.status;
|
|
78
|
+
const code = errorCode(error, reported);
|
|
79
|
+
return {
|
|
80
|
+
error_source: cameFromApi(error) ? 'api' : 'mcp',
|
|
81
|
+
...(typeof status === 'number' && Number.isInteger(status) && status >= 100 && status <= 599
|
|
82
|
+
? { error_status: status }
|
|
83
|
+
: {}),
|
|
84
|
+
...(typeof code === 'string' && ERROR_CODE.test(code) ? { error_code: code } : {}),
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
export async function trackToolExecution(name, args, meta, profile, http, execute, analytics) {
|
|
88
|
+
const { syncAnalytics, ...businessArgs } = args;
|
|
89
|
+
if (profile !== 'chatgpt')
|
|
90
|
+
return execute(businessArgs);
|
|
91
|
+
const ui = uiAnalyticsSchema.safeParse(syncAnalytics);
|
|
92
|
+
const session = meta?.['openai/session'];
|
|
93
|
+
const attribution = {
|
|
94
|
+
version: 1,
|
|
95
|
+
invocation_source: 'unknown',
|
|
96
|
+
...(ui.success ? ui.data : {}),
|
|
97
|
+
...(typeof session === 'string' && session.length <= 1024
|
|
98
|
+
? {
|
|
99
|
+
conversation_id: createHash('sha256').update(session).digest('hex'),
|
|
100
|
+
}
|
|
101
|
+
: {}),
|
|
102
|
+
};
|
|
103
|
+
return runWithToolAnalytics(attribution, async () => {
|
|
104
|
+
const started = performance.now();
|
|
105
|
+
let outcome = 'error';
|
|
106
|
+
let result;
|
|
107
|
+
let failure;
|
|
108
|
+
try {
|
|
109
|
+
result = await execute(businessArgs);
|
|
110
|
+
outcome =
|
|
111
|
+
result && typeof result === 'object' && 'isError' in result && result.isError
|
|
112
|
+
? 'error'
|
|
113
|
+
: 'success';
|
|
114
|
+
return result;
|
|
115
|
+
}
|
|
116
|
+
catch (error) {
|
|
117
|
+
failure = error;
|
|
118
|
+
throw error;
|
|
119
|
+
}
|
|
120
|
+
finally {
|
|
121
|
+
if (analytics?.tools.has(name) && attribution.invocation_source !== 'background') {
|
|
122
|
+
void http
|
|
123
|
+
.request('post', '/v2/analytics/chatgpt-events', {
|
|
124
|
+
signal: AbortSignal.timeout(1500),
|
|
125
|
+
body: {
|
|
126
|
+
event: 'chatgpt_tool_called',
|
|
127
|
+
event_id: randomUUID(),
|
|
128
|
+
attribution,
|
|
129
|
+
tool_name: name,
|
|
130
|
+
outcome,
|
|
131
|
+
duration_ms: Math.min(performance.now() - started, 3_600_000),
|
|
132
|
+
...(outcome === 'error' && analytics.errorDetails
|
|
133
|
+
? errorDetails(failure, result)
|
|
134
|
+
: {}),
|
|
135
|
+
},
|
|
136
|
+
})
|
|
137
|
+
.catch(() => undefined);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
export function createAnalyticsTool(http) {
|
|
143
|
+
return {
|
|
144
|
+
name: 'analytics_track-event',
|
|
145
|
+
title: 'Record Sync interface interaction',
|
|
146
|
+
description: 'Record bounded interface telemetry for the authenticated Sync user. No prompts, media, credentials or account identifiers are accepted.',
|
|
147
|
+
inputSchema: eventSchema.shape,
|
|
148
|
+
outputSchema: { accepted: z.boolean() },
|
|
149
|
+
meta: { ui: { visibility: ['app'] }, 'openai/visibility': 'private' },
|
|
150
|
+
annotations: {
|
|
151
|
+
readOnlyHint: false,
|
|
152
|
+
destructiveHint: false,
|
|
153
|
+
openWorldHint: false,
|
|
154
|
+
idempotentHint: true,
|
|
155
|
+
},
|
|
156
|
+
handler: async (args) => {
|
|
157
|
+
const event = eventSchema.parse(args);
|
|
158
|
+
const attribution = getToolAnalytics();
|
|
159
|
+
await http.request('post', '/v2/analytics/chatgpt-events', {
|
|
160
|
+
signal: AbortSignal.timeout(1500),
|
|
161
|
+
body: {
|
|
162
|
+
...event,
|
|
163
|
+
attribution: {
|
|
164
|
+
...event.attribution,
|
|
165
|
+
...(attribution?.conversation_id && { conversation_id: attribution.conversation_id }),
|
|
166
|
+
},
|
|
167
|
+
},
|
|
168
|
+
});
|
|
169
|
+
return { accepted: true };
|
|
170
|
+
},
|
|
171
|
+
};
|
|
172
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { ResourceMetadata } from '@modelcontextprotocol/server';
|
|
2
|
+
import type { SyncMcpConfig } from './config.js';
|
|
3
|
+
import type { McpToolDefinition } from './tools/index.js';
|
|
4
|
+
export type ChatgptApp = {
|
|
5
|
+
uri: string;
|
|
6
|
+
html: string;
|
|
7
|
+
initialProjectsVersion?: number;
|
|
8
|
+
metadata: ResourceMetadata;
|
|
9
|
+
};
|
|
10
|
+
/** Load once per server factory so every session serves the same verified bytes. */
|
|
11
|
+
export declare function loadChatgptApp(config: SyncMcpConfig['chatgptApp']): Promise<ChatgptApp | undefined>;
|
|
12
|
+
/** Retain immutable releases so cached tool descriptors remain readable after upgrades. */
|
|
13
|
+
export declare function loadChatgptAppReleases(config: SyncMcpConfig['chatgptApp']): Promise<ChatgptApp[]>;
|
|
14
|
+
export declare function createOpenSyncAppTool(app: ChatgptApp, projects?: McpToolDefinition): McpToolDefinition;
|
|
15
|
+
//# sourceMappingURL=chatgpt-app.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"chatgpt-app.d.ts","sourceRoot":"","sources":["../src/chatgpt-app.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,8BAA8B,CAAC;AAIrE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AACjD,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AAuB1D,MAAM,MAAM,UAAU,GAAG;IACvB,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;IACb,sBAAsB,CAAC,EAAE,MAAM,CAAC;IAChC,QAAQ,EAAE,gBAAgB,CAAC;CAC5B,CAAC;AAUF,oFAAoF;AACpF,wBAAsB,cAAc,CAClC,MAAM,EAAE,aAAa,CAAC,YAAY,CAAC,GAClC,OAAO,CAAC,UAAU,GAAG,SAAS,CAAC,CAoCjC;AAED,2FAA2F;AAC3F,wBAAsB,sBAAsB,CAC1C,MAAM,EAAE,aAAa,CAAC,YAAY,CAAC,GAClC,OAAO,CAAC,UAAU,EAAE,CAAC,CAcvB;AAED,wBAAgB,qBAAqB,CACnC,GAAG,EAAE,UAAU,EACf,QAAQ,CAAC,EAAE,iBAAiB,GAC3B,iBAAiB,CAuDnB"}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { readFile, stat } from 'node:fs/promises';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { z } from 'zod';
|
|
5
|
+
import { combineSignals } from './abort-signals.js';
|
|
6
|
+
import { MAX_PREVIOUS_CHATGPT_RELEASES } from './chatgpt-release.js';
|
|
7
|
+
import { MCP_APP_RESOURCE_MIME_TYPE } from './tools/upload-widget.js';
|
|
8
|
+
const originSchema = z
|
|
9
|
+
.url({ protocol: /^https$/ })
|
|
10
|
+
.refine((value) => !value.includes('*') && new URL(value).origin === value, 'Use an exact HTTPS origin without paths, credentials, or wildcards.');
|
|
11
|
+
const configSchema = z.object({
|
|
12
|
+
directory: z.string().min(1),
|
|
13
|
+
domain: originSchema,
|
|
14
|
+
resourceDomains: originSchema.array(),
|
|
15
|
+
connectDomains: originSchema.array().default([]),
|
|
16
|
+
uploadOrigins: originSchema.array().optional(),
|
|
17
|
+
});
|
|
18
|
+
const manifestSchema = z.object({
|
|
19
|
+
version: z.literal(1),
|
|
20
|
+
file: z.literal('app.html'),
|
|
21
|
+
initialProjectsVersion: z.number().int().positive().optional(),
|
|
22
|
+
sha256: z.string().regex(/^[a-f0-9]{64}$/),
|
|
23
|
+
});
|
|
24
|
+
async function readBounded(path, limit) {
|
|
25
|
+
if ((await stat(path)).size > limit)
|
|
26
|
+
throw new Error('ChatGPT app artifact exceeds its size limit.');
|
|
27
|
+
const bytes = await readFile(path);
|
|
28
|
+
if (bytes.length > limit)
|
|
29
|
+
throw new Error('ChatGPT app artifact exceeds its size limit.');
|
|
30
|
+
return bytes;
|
|
31
|
+
}
|
|
32
|
+
/** Load once per server factory so every session serves the same verified bytes. */
|
|
33
|
+
export async function loadChatgptApp(config) {
|
|
34
|
+
if (!config)
|
|
35
|
+
return undefined;
|
|
36
|
+
const { directory, domain, resourceDomains, connectDomains } = configSchema.parse(config);
|
|
37
|
+
// Recording previews use object URLs; the verified bundle embeds Studio's font.
|
|
38
|
+
// Resource schemes do not expand backend connection origins.
|
|
39
|
+
const resourceSources = [...resourceDomains, 'blob:', 'data:'];
|
|
40
|
+
const manifest = manifestSchema.parse(JSON.parse((await readBounded(join(directory, 'manifest.json'), 4096)).toString('utf8')));
|
|
41
|
+
const bytes = await readBounded(join(directory, manifest.file), 8 * 1024 * 1024);
|
|
42
|
+
if (createHash('sha256').update(bytes).digest('hex') !== manifest.sha256)
|
|
43
|
+
throw new Error('ChatGPT app SHA256 does not match its release manifest.');
|
|
44
|
+
return {
|
|
45
|
+
uri: `ui://sync/app-${manifest.sha256}.html`,
|
|
46
|
+
html: new TextDecoder('utf-8', { fatal: true }).decode(bytes),
|
|
47
|
+
initialProjectsVersion: manifest.initialProjectsVersion,
|
|
48
|
+
metadata: {
|
|
49
|
+
title: 'Sync',
|
|
50
|
+
description: 'Browse your Sync projects and create videos using your existing assets.',
|
|
51
|
+
mimeType: MCP_APP_RESOURCE_MIME_TYPE,
|
|
52
|
+
_meta: {
|
|
53
|
+
ui: {
|
|
54
|
+
domain,
|
|
55
|
+
prefersBorder: true,
|
|
56
|
+
csp: { connectDomains, resourceDomains: resourceSources },
|
|
57
|
+
permissions: { camera: {}, microphone: {} },
|
|
58
|
+
},
|
|
59
|
+
'openai/widgetDomain': domain,
|
|
60
|
+
'openai/widgetCSP': { connect_domains: connectDomains, resource_domains: resourceSources },
|
|
61
|
+
'openai/widgetPrefersBorder': true,
|
|
62
|
+
'openai/widgetDescription': 'Sync projects and video creation interface. A successful open request does not confirm that the interface has rendered.',
|
|
63
|
+
'openai/ui': { availableDisplayModes: ['inline'] },
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
/** Retain immutable releases so cached tool descriptors remain readable after upgrades. */
|
|
69
|
+
export async function loadChatgptAppReleases(config) {
|
|
70
|
+
if (!config)
|
|
71
|
+
return [];
|
|
72
|
+
const previous = z
|
|
73
|
+
.array(z.string().min(1))
|
|
74
|
+
.max(MAX_PREVIOUS_CHATGPT_RELEASES)
|
|
75
|
+
.parse(config.previousDirectories ?? []);
|
|
76
|
+
const apps = await Promise.all([config.directory, ...previous].map((directory) => loadChatgptApp({ ...config, directory })));
|
|
77
|
+
return [
|
|
78
|
+
...new Map(apps.filter((app) => !!app).map((app) => [app.uri, app])).values(),
|
|
79
|
+
];
|
|
80
|
+
}
|
|
81
|
+
export function createOpenSyncAppTool(app, projects) {
|
|
82
|
+
return {
|
|
83
|
+
name: 'app_open',
|
|
84
|
+
title: 'sync. labs',
|
|
85
|
+
description: 'Use this when the user asks to open Sync, upload media in its interface, or visually configure a talking photo, lipsync video, Translate & Dub, or Edit Dialogue workflow. Browse existing projects and choose media before generating. This interface does not provide subtitle-only output, standalone MP3 voiceovers, trimming, or background-music mixing; explain that limitation rather than opening it to fulfill those tasks. Requesting the interface does not start a generation or spend credits. The result only confirms the request; do not claim the interface rendered unless the user confirms it.',
|
|
86
|
+
inputSchema: {},
|
|
87
|
+
outputSchema: { requested: z.boolean(), renderStatus: z.literal('awaiting_client') },
|
|
88
|
+
annotations: {
|
|
89
|
+
readOnlyHint: true,
|
|
90
|
+
destructiveHint: false,
|
|
91
|
+
openWorldHint: false,
|
|
92
|
+
idempotentHint: true,
|
|
93
|
+
},
|
|
94
|
+
meta: {
|
|
95
|
+
ui: { resourceUri: app.uri },
|
|
96
|
+
'openai/outputTemplate': app.uri,
|
|
97
|
+
'openai/toolInvocation/invoking': 'Preparing Sync',
|
|
98
|
+
'openai/toolInvocation/invoked': 'Sync interface requested',
|
|
99
|
+
'openai/ui': { entrypoints: [{ type: 'global' }, { type: 'thread' }] },
|
|
100
|
+
},
|
|
101
|
+
resultFormat: 'mcp',
|
|
102
|
+
handler: async (_args, context) => {
|
|
103
|
+
const structuredContent = { requested: true, renderStatus: 'awaiting_client' };
|
|
104
|
+
const result = {
|
|
105
|
+
content: [{ type: 'text', text: JSON.stringify(structuredContent) }],
|
|
106
|
+
structuredContent,
|
|
107
|
+
};
|
|
108
|
+
if (!projects || app.initialProjectsVersion !== 1)
|
|
109
|
+
return result;
|
|
110
|
+
// Optional read: never make opening the UI depend on a slow project API.
|
|
111
|
+
const deadline = AbortSignal.timeout(500);
|
|
112
|
+
const { signal, dispose } = combineSignals(context?.signal ? [context.signal, deadline] : [deadline]);
|
|
113
|
+
try {
|
|
114
|
+
const page = await projects.handler({ searchQuery: '', limit: 12, sortBy: 'updatedAt' }, { signal });
|
|
115
|
+
return {
|
|
116
|
+
...result,
|
|
117
|
+
_meta: {
|
|
118
|
+
// Per-invocation, authenticated data, never part of shared resource HTML.
|
|
119
|
+
'sync/initialProjects': { version: 1, fetchedAt: Date.now(), page },
|
|
120
|
+
},
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
catch {
|
|
124
|
+
// The widget can fetch normally and display the canonical API error/retry.
|
|
125
|
+
context?.signal?.throwIfAborted();
|
|
126
|
+
return result;
|
|
127
|
+
}
|
|
128
|
+
finally {
|
|
129
|
+
dispose();
|
|
130
|
+
}
|
|
131
|
+
},
|
|
132
|
+
};
|
|
133
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
export declare const MAX_PREVIOUS_CHATGPT_RELEASES = 11;
|
|
3
|
+
export declare const chatgptReleaseSchema: z.ZodObject<{
|
|
4
|
+
current: z.ZodString;
|
|
5
|
+
previous: z.ZodArray<z.ZodString>;
|
|
6
|
+
}, z.core.$strict>;
|
|
7
|
+
/** The image owns release selection; legacy environment paths remain readable. */
|
|
8
|
+
export declare function resolveChatgptRelease(catalogPath: string, legacyDirectories: string[]): {
|
|
9
|
+
directory: string;
|
|
10
|
+
previousDirectories: string[];
|
|
11
|
+
};
|
|
12
|
+
//# sourceMappingURL=chatgpt-release.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"chatgpt-release.d.ts","sourceRoot":"","sources":["../src/chatgpt-release.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAGxB,eAAO,MAAM,6BAA6B,KAAK,CAAC;AAChD,eAAO,MAAM,oBAAoB;;;kBAM9B,CAAC;AAEJ,kFAAkF;AAClF,wBAAgB,qBAAqB,CAAC,WAAW,EAAE,MAAM,EAAE,iBAAiB,EAAE,MAAM,EAAE;;;EAuBrF"}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
2
|
+
import { basename, dirname, resolve } from 'node:path';
|
|
3
|
+
import { z } from 'zod';
|
|
4
|
+
const digest = z.string().regex(/^[a-f0-9]{64}$/);
|
|
5
|
+
export const MAX_PREVIOUS_CHATGPT_RELEASES = 11;
|
|
6
|
+
export const chatgptReleaseSchema = z
|
|
7
|
+
.object({ current: digest, previous: z.array(digest).max(MAX_PREVIOUS_CHATGPT_RELEASES) })
|
|
8
|
+
.strict()
|
|
9
|
+
.refine(({ current, previous }) => new Set([current, ...previous]).size === previous.length + 1, 'Release hashes must be unique.');
|
|
10
|
+
/** The image owns release selection; legacy environment paths remain readable. */
|
|
11
|
+
export function resolveChatgptRelease(catalogPath, legacyDirectories) {
|
|
12
|
+
const catalog = chatgptReleaseSchema.parse(JSON.parse(readFileSync(catalogPath, 'utf8')));
|
|
13
|
+
const root = dirname(resolve(catalogPath));
|
|
14
|
+
const directory = resolve(root, catalog.current);
|
|
15
|
+
const previousDirectories = [
|
|
16
|
+
...new Set([
|
|
17
|
+
...catalog.previous.map((hash) => resolve(root, hash)),
|
|
18
|
+
...legacyDirectories
|
|
19
|
+
.map((path) => resolve(path))
|
|
20
|
+
// Ignore only absent old image pins; a mount at the same path must stay readable.
|
|
21
|
+
.filter((path) => dirname(path) !== root || !digest.safeParse(basename(path)).success || existsSync(path)),
|
|
22
|
+
]),
|
|
23
|
+
].filter((path) => path !== directory);
|
|
24
|
+
z.array(z.string())
|
|
25
|
+
.max(MAX_PREVIOUS_CHATGPT_RELEASES, {
|
|
26
|
+
message: 'ChatGPT UI retention exceeds eleven previous bundles across the release manifest and legacy settings. Retire unused releases explicitly before deploying.',
|
|
27
|
+
})
|
|
28
|
+
.parse(previousDirectories);
|
|
29
|
+
return { directory, previousDirectories };
|
|
30
|
+
}
|