summer-engine 3.2.1 → 3.4.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.
Files changed (97) hide show
  1. package/.claude-plugin/marketplace.json +2 -2
  2. package/.claude-plugin/plugin.json +4 -2
  3. package/.codex-plugin/plugin.json +5 -3
  4. package/.cursor-plugin/plugin.json +4 -2
  5. package/.factory-plugin/plugin.json +4 -2
  6. package/CHANGELOG.md +61 -0
  7. package/README.md +7 -4
  8. package/assets/api-docs.json.gz +0 -0
  9. package/dist/cli/commands/install.d.ts +10 -0
  10. package/dist/cli/commands/install.js +55 -10
  11. package/dist/cli/commands/login.js +12 -0
  12. package/dist/cli/commands/publish.js +2 -1
  13. package/dist/cli/commands/tool.d.ts +7 -0
  14. package/dist/cli/commands/tool.js +36 -1
  15. package/dist/core/auth.js +3 -0
  16. package/dist/core/capabilities/audit/args.d.ts +14 -14
  17. package/dist/core/capabilities/capture-gameplay.d.ts +69 -0
  18. package/dist/core/capabilities/capture-gameplay.js +283 -0
  19. package/dist/core/capabilities/creator.js +1 -1
  20. package/dist/core/capabilities/doctor.d.ts +9 -1
  21. package/dist/core/capabilities/doctor.js +47 -4
  22. package/dist/core/capabilities/engine-run.d.ts +12 -0
  23. package/dist/core/capabilities/engine-run.js +61 -0
  24. package/dist/core/capabilities/export-engine-version.d.ts +15 -0
  25. package/dist/core/capabilities/export-engine-version.js +24 -0
  26. package/dist/core/capabilities/export-game.d.ts +107 -0
  27. package/dist/core/capabilities/export-game.js +445 -0
  28. package/dist/core/capabilities/export-presets.d.ts +50 -0
  29. package/dist/core/capabilities/export-presets.js +177 -0
  30. package/dist/core/capabilities/export-templates.d.ts +118 -0
  31. package/dist/core/capabilities/export-templates.js +291 -0
  32. package/dist/core/capabilities/image-generation.d.ts +9 -0
  33. package/dist/core/capabilities/image-generation.js +19 -1
  34. package/dist/core/capabilities/publish-build.d.ts +77 -0
  35. package/dist/core/capabilities/publish-build.js +500 -0
  36. package/dist/core/capabilities/publish-download.d.ts +37 -0
  37. package/dist/core/capabilities/publish-download.js +168 -0
  38. package/dist/core/capabilities/runtime-control.d.ts +10 -10
  39. package/dist/core/capabilities/summer-bundle.d.ts +49 -0
  40. package/dist/core/capabilities/summer-bundle.js +156 -0
  41. package/dist/core/capabilities/tool-dispatch.js +43 -1
  42. package/dist/core/capabilities/ui-control.d.ts +2 -2
  43. package/dist/core/config.js +2 -2
  44. package/dist/core/engine-install.d.ts +16 -0
  45. package/dist/core/engine-install.js +32 -2
  46. package/dist/core/hosted-mcp.d.ts +115 -0
  47. package/dist/core/hosted-mcp.js +164 -0
  48. package/dist/core/launch-posture.d.ts +3 -0
  49. package/dist/core/launch-posture.js +15 -2
  50. package/dist/core/library-read.d.ts +2 -2
  51. package/dist/core/oauth.d.ts +78 -0
  52. package/dist/core/oauth.js +309 -0
  53. package/dist/core/util/zip-write.d.ts +18 -0
  54. package/dist/core/util/zip-write.js +112 -0
  55. package/dist/core/util/zip.d.ts +22 -0
  56. package/dist/core/util/zip.js +140 -0
  57. package/dist/mcp/hosted-mount.d.ts +50 -0
  58. package/dist/mcp/hosted-mount.js +200 -0
  59. package/dist/mcp/server.d.ts +2 -1
  60. package/dist/mcp/server.js +14 -6
  61. package/dist/mcp/tools/creator-tools.js +64 -1
  62. package/dist/mcp/tools/generate-tools.js +12 -2
  63. package/gemini-extension.json +3 -1
  64. package/library/references/mcp-tools-reference/mcp-tools-reference.md +7 -3
  65. package/library/skills/README.md +6 -4
  66. package/library/skills/bounded-engine-runs/SKILL.md +96 -0
  67. package/library/skills/bounded-engine-runs/resource.yaml +29 -0
  68. package/library/skills/compatibility-renderer-traps/SKILL.md +78 -0
  69. package/library/skills/compatibility-renderer-traps/resource.yaml +28 -0
  70. package/library/skills/concept-art/SKILL.md +2 -2
  71. package/library/skills/export-and-ship/SKILL.md +4 -0
  72. package/library/skills/headless-scripting/resource.yaml +1 -0
  73. package/library/skills/in-game-purchases/SKILL.md +123 -118
  74. package/library/skills/in-game-purchases/resource.yaml +2 -2
  75. package/library/skills/multiplayer-publish/SKILL.md +5 -1
  76. package/library/skills/navigate-summer/SKILL.md +1 -1
  77. package/library/skills/web-compatibility-look/resource.yaml +1 -0
  78. package/library/tools/capture-gameplay/resource.yaml +73 -0
  79. package/library/tools/creator-publish/resource.yaml +4 -3
  80. package/library/tools/creator-releases/resource.yaml +1 -1
  81. package/library/tools/export-game/resource.yaml +72 -0
  82. package/library/tools/export-templates/resource.yaml +63 -0
  83. package/library/tools/generate-image/resource.yaml +15 -1
  84. package/library/tools/open/resource.yaml +1 -1
  85. package/library/tools/publish-build/resource.yaml +78 -0
  86. package/package.json +1 -1
  87. package/registry/generated/aliases.json +4 -0
  88. package/registry/generated/counts.json +3 -3
  89. package/registry/generated/gemini-extension.json +3 -1
  90. package/registry/generated/index.json +241 -19
  91. package/registry/generated/marketplace.claude.json +2 -2
  92. package/registry/generated/plugin.claude.json +4 -2
  93. package/registry/generated/plugin.codex.json +5 -3
  94. package/registry/generated/plugin.cursor.json +4 -2
  95. package/registry/generated/plugin.factory.json +4 -2
  96. package/registry/generated/skills-index.md +6 -4
  97. package/registry/generated/skills-registry.json +28 -1
@@ -0,0 +1,309 @@
1
+ import { createHash, randomBytes } from "node:crypto";
2
+ import { createServer } from "node:http";
3
+ import { readStoreJson, writeStoreJson } from "./store.js";
4
+ import { readJsonResponse } from "./util/http.js";
5
+ /**
6
+ * The Summer store sign-in for local tools (summer_publish_build): OAuth 2.1
7
+ * authorization code + PKCE against the same Supabase authorization server
8
+ * and resource the hosted MCP uses (mcp.summerengine.com/mcp), with a
9
+ * loopback redirect (RFC 8252). The access token's audience is the MCP
10
+ * resource, the one token the creator store accepts for agents (creator
11
+ * publishing spec, part C.3).
12
+ *
13
+ * The Summer CLI is one public client (no secret; PKCE proves each login).
14
+ * The authorization server matches redirect URIs exactly, so the CLI listens
15
+ * on one of a few fixed loopback ports and registers its client once per
16
+ * machine with exactly those redirect URIs. Later logins and refreshes reuse
17
+ * that client id (~/.summer/oauth-client); only --force registers anew.
18
+ *
19
+ * The token is stored in ~/.summer/oauth-token (0600) with its refresh token,
20
+ * separate from the CLI JWT in auth-token: the engine reads that file and
21
+ * expects the summer-cli contract.
22
+ */
23
+ export const OAUTH_TOKEN_FILE = "oauth-token";
24
+ export const OAUTH_CLIENT_FILE = "oauth-client";
25
+ /** Loopback ports the Summer CLI client is registered for, tried in order. */
26
+ export const STORE_LOGIN_PORTS = [47615, 47616, 47617];
27
+ export const DEFAULT_MCP_RESOURCE_URL = "https://mcp.summerengine.com/mcp";
28
+ export const MCP_RESOURCE_ENV = "SUMMER_MCP_RESOURCE_URL";
29
+ const SCOPES = ["openid", "email", "offline_access"];
30
+ const REFRESH_MARGIN_MS = 60_000;
31
+ const LOGIN_TIMEOUT_MS = 10 * 60_000;
32
+ export class OAuthError extends Error {
33
+ code;
34
+ recovery;
35
+ constructor(code, message, recovery) {
36
+ super(`${message} ${recovery}`);
37
+ this.code = code;
38
+ this.recovery = recovery;
39
+ this.name = "OAuthError";
40
+ }
41
+ }
42
+ const defaultDependencies = {
43
+ fetch: (input, init) => globalThis.fetch(input, init),
44
+ now: Date.now,
45
+ };
46
+ function isSafeUrl(value) {
47
+ try {
48
+ const url = new URL(value);
49
+ const loopback = ["localhost", "127.0.0.1", "[::1]"].includes(url.hostname);
50
+ return !url.username && !url.password && (url.protocol === "https:" || (loopback && url.protocol === "http:"));
51
+ }
52
+ catch {
53
+ return false;
54
+ }
55
+ }
56
+ export function resolveMcpResourceUrl(env = process.env) {
57
+ const value = env[MCP_RESOURCE_ENV]?.trim() || DEFAULT_MCP_RESOURCE_URL;
58
+ if (!isSafeUrl(value)) {
59
+ throw new OAuthError("oauth_resource_invalid", `${MCP_RESOURCE_ENV} must be an https URL (http only for localhost).`, `Recovery: unset ${MCP_RESOURCE_ENV} to use ${DEFAULT_MCP_RESOURCE_URL}.`);
60
+ }
61
+ return value;
62
+ }
63
+ async function getJson(deps, url) {
64
+ try {
65
+ const response = await deps.fetch(url, { headers: { accept: "application/json" }, signal: AbortSignal.timeout(10_000) });
66
+ if (!response.ok)
67
+ return null;
68
+ const { json, parsed } = await readJsonResponse(response);
69
+ return parsed && json && typeof json === "object" ? json : null;
70
+ }
71
+ catch {
72
+ return null;
73
+ }
74
+ }
75
+ /** RFC 9728 resource metadata, then RFC 8414 server metadata (path-aware first). */
76
+ export async function discoverAuthorizationServer(resource, deps = defaultDependencies) {
77
+ const resourceUrl = new URL(resource);
78
+ const resourcePath = resourceUrl.pathname === "/" ? "" : resourceUrl.pathname.replace(/\/$/, "");
79
+ const prm = (await getJson(deps, `${resourceUrl.origin}/.well-known/oauth-protected-resource${resourcePath}`)) ??
80
+ (await getJson(deps, `${resourceUrl.origin}/.well-known/oauth-protected-resource`));
81
+ const issuer = Array.isArray(prm?.authorization_servers) ? prm.authorization_servers[0] : undefined;
82
+ if (prm?.resource !== resource || typeof issuer !== "string" || !isSafeUrl(issuer)) {
83
+ throw new OAuthError("oauth_discovery_failed", `${resource} did not publish its sign-in server.`, "Recovery: check your network and retry; if it repeats, the hosted Summer MCP is not available yet.");
84
+ }
85
+ const issuerUrl = new URL(issuer);
86
+ const issuerPath = issuerUrl.pathname === "/" ? "" : issuerUrl.pathname.replace(/\/$/, "");
87
+ const metadata = (await getJson(deps, `${issuerUrl.origin}/.well-known/oauth-authorization-server${issuerPath}`)) ??
88
+ (await getJson(deps, `${issuer.replace(/\/$/, "")}/.well-known/oauth-authorization-server`)) ??
89
+ (await getJson(deps, `${issuer.replace(/\/$/, "")}/.well-known/openid-configuration`));
90
+ const endpoints = [metadata?.authorization_endpoint, metadata?.token_endpoint, metadata?.registration_endpoint];
91
+ if (metadata?.issuer !== issuer || !endpoints.every((value) => typeof value === "string" && isSafeUrl(value))) {
92
+ throw new OAuthError("oauth_discovery_failed", `The sign-in server ${issuer} did not publish usable OAuth metadata.`, "Recovery: retry later; if it repeats, report the Summer sign-in server as unhealthy.");
93
+ }
94
+ return {
95
+ issuer,
96
+ authorizationEndpoint: endpoints[0],
97
+ tokenEndpoint: endpoints[1],
98
+ registrationEndpoint: endpoints[2],
99
+ };
100
+ }
101
+ async function postForm(deps, url, form) {
102
+ let response;
103
+ try {
104
+ response = await deps.fetch(url, {
105
+ method: "POST",
106
+ headers: { "content-type": "application/x-www-form-urlencoded", accept: "application/json" },
107
+ body: new URLSearchParams(form).toString(),
108
+ signal: AbortSignal.timeout(15_000),
109
+ });
110
+ }
111
+ catch (error) {
112
+ throw new OAuthError("oauth_network_failed", `The sign-in server did not answer: ${error instanceof Error ? error.message : String(error)}.`, "Recovery: check your network and retry.");
113
+ }
114
+ const { json } = await readJsonResponse(response);
115
+ const body = (json && typeof json === "object" ? json : {});
116
+ if (!response.ok) {
117
+ const reason = typeof body.error === "string" ? body.error : `http_${response.status}`;
118
+ throw new OAuthError(reason === "invalid_grant" ? "oauth_grant_invalid" : "oauth_token_refused", `The sign-in server refused the token request (${reason}).`, 'Recovery: run "summer login --store --force" and approve access again.');
119
+ }
120
+ return body;
121
+ }
122
+ function tokenFromResponse(body, base, now, previousRefresh) {
123
+ if (typeof body.access_token !== "string" || !body.access_token) {
124
+ throw new OAuthError("oauth_token_invalid", "The sign-in server returned no access token.", 'Recovery: run "summer login --store --force" again.');
125
+ }
126
+ const expiresIn = typeof body.expires_in === "number" && body.expires_in > 0 ? body.expires_in : 3600;
127
+ const refreshToken = typeof body.refresh_token === "string" ? body.refresh_token : previousRefresh;
128
+ return {
129
+ ...base,
130
+ accessToken: body.access_token,
131
+ ...(refreshToken ? { refreshToken } : {}),
132
+ expiresAt: new Date(now + expiresIn * 1000).toISOString(),
133
+ ...(typeof body.scope === "string" ? { scope: body.scope } : {}),
134
+ };
135
+ }
136
+ function pkce() {
137
+ const verifier = randomBytes(32).toString("base64url");
138
+ return { verifier, challenge: createHash("sha256").update(verifier).digest("base64url") };
139
+ }
140
+ /** A one-shot loopback listener on 127.0.0.1 for the authorization code. */
141
+ async function listenForCode(state, timeoutMs, ports) {
142
+ let settle;
143
+ const code = new Promise((resolve, reject) => (settle = { resolve, reject }));
144
+ // An unawaited rejection (timeout before anyone awaits) must not crash the process.
145
+ code.catch(() => undefined);
146
+ const server = createServer((request, response) => {
147
+ const url = new URL(request.url ?? "/", "http://127.0.0.1");
148
+ if (url.pathname !== "/callback") {
149
+ response.writeHead(404).end();
150
+ return;
151
+ }
152
+ const error = url.searchParams.get("error");
153
+ const ok = !error && url.searchParams.get("state") === state && url.searchParams.get("code");
154
+ response.writeHead(ok ? 200 : 400, { "content-type": "text/html; charset=utf-8" });
155
+ response.end(ok
156
+ ? "<!doctype html><title>Summer</title><p>Signed in to Summer. You can close this tab.</p>"
157
+ : "<!doctype html><title>Summer</title><p>Sign-in did not finish. Return to the terminal.</p>");
158
+ if (ok)
159
+ settle.resolve(url.searchParams.get("code"));
160
+ else
161
+ settle.reject(new OAuthError(error === "access_denied" ? "oauth_access_denied" : "oauth_callback_invalid", error ? `Sign-in was not approved (${error}).` : "The sign-in answer did not match this login.", 'Recovery: run "summer login --store" again and approve access.'));
162
+ });
163
+ let port = 0;
164
+ for (const candidate of ports) {
165
+ const listening = await new Promise((resolve, reject) => {
166
+ const onError = (error) => error.code === "EADDRINUSE" || error.code === "EACCES" ? resolve(false) : reject(error);
167
+ server.once("error", onError);
168
+ server.listen(candidate, "127.0.0.1", () => {
169
+ server.off("error", onError);
170
+ resolve(true);
171
+ });
172
+ });
173
+ if (listening) {
174
+ port = candidate;
175
+ break;
176
+ }
177
+ }
178
+ if (!port) {
179
+ throw new OAuthError("oauth_ports_busy", `Sign-in needs one free local port (${ports.join(", ")}); all are in use.`, 'Recovery: close the program using them (or an earlier "summer login --store" still waiting), then retry.');
180
+ }
181
+ const timer = setTimeout(() => settle.reject(new OAuthError("oauth_timeout", `Sign-in did not finish within ${Math.round(timeoutMs / 60_000)} minutes.`, 'Recovery: run "summer login --store" again.')), timeoutMs);
182
+ return {
183
+ redirectUri: `http://127.0.0.1:${port}/callback`,
184
+ code,
185
+ close: () => {
186
+ clearTimeout(timer);
187
+ server.close();
188
+ },
189
+ };
190
+ }
191
+ async function readStoredOAuthClient(issuer, redirectUris) {
192
+ const client = await readStoreJson(OAUTH_CLIENT_FILE).catch(() => null);
193
+ const sameRedirects = Array.isArray(client?.redirectUris) &&
194
+ client.redirectUris.length === redirectUris.length &&
195
+ client.redirectUris.every((uri, index) => uri === redirectUris[index]);
196
+ return client?.schemaVersion === 1 && client.issuer === issuer && sameRedirects && typeof client.clientId === "string" && client.clientId
197
+ ? client.clientId
198
+ : null;
199
+ }
200
+ /** One registration per machine and authorization server: a public client for the fixed loopback redirects. */
201
+ async function registerClient(deps, server, redirectUris) {
202
+ let registration;
203
+ try {
204
+ registration = await deps.fetch(server.registrationEndpoint, {
205
+ method: "POST",
206
+ headers: { "content-type": "application/json", accept: "application/json" },
207
+ body: JSON.stringify({
208
+ client_name: "Summer CLI",
209
+ redirect_uris: redirectUris,
210
+ grant_types: ["authorization_code", "refresh_token"],
211
+ response_types: ["code"],
212
+ token_endpoint_auth_method: "none",
213
+ scope: SCOPES.join(" "),
214
+ }),
215
+ signal: AbortSignal.timeout(15_000),
216
+ });
217
+ }
218
+ catch (error) {
219
+ throw new OAuthError("oauth_network_failed", `The sign-in server did not answer: ${error instanceof Error ? error.message : String(error)}.`, "Recovery: check your network and retry.");
220
+ }
221
+ const { json } = await readJsonResponse(registration);
222
+ const clientId = json?.client_id;
223
+ if (!registration.ok || typeof clientId !== "string" || !clientId) {
224
+ throw new OAuthError("oauth_registration_failed", `The sign-in server did not register the Summer CLI (${registration.status}).`, "Recovery: retry; if it repeats, report the Summer sign-in server as unhealthy.");
225
+ }
226
+ const stored = { schemaVersion: 1, issuer: server.issuer, clientId, redirectUris };
227
+ await writeStoreJson(OAUTH_CLIENT_FILE, stored);
228
+ return clientId;
229
+ }
230
+ /** Browser sign-in (authorization code + PKCE) that stores ~/.summer/oauth-token. */
231
+ export async function runStoreLogin(deps) {
232
+ const resource = resolveMcpResourceUrl();
233
+ const server = await discoverAuthorizationServer(resource, deps);
234
+ const state = randomBytes(16).toString("base64url");
235
+ const ports = deps.ports ?? STORE_LOGIN_PORTS;
236
+ const redirectUris = ports.map((port) => `http://127.0.0.1:${port}/callback`);
237
+ const callback = await listenForCode(state, deps.timeoutMs ?? LOGIN_TIMEOUT_MS, ports);
238
+ try {
239
+ const clientId = (!deps.newClient && (await readStoredOAuthClient(server.issuer, redirectUris))) ||
240
+ (await registerClient(deps, server, redirectUris));
241
+ const { verifier, challenge } = pkce();
242
+ const authorize = new URL(server.authorizationEndpoint);
243
+ for (const [key, value] of Object.entries({
244
+ response_type: "code",
245
+ client_id: clientId,
246
+ redirect_uri: callback.redirectUri,
247
+ scope: SCOPES.join(" "),
248
+ state,
249
+ code_challenge: challenge,
250
+ code_challenge_method: "S256",
251
+ resource,
252
+ })) {
253
+ authorize.searchParams.set(key, value);
254
+ }
255
+ deps.log(`Approve Summer store access in your browser: ${authorize}`);
256
+ try {
257
+ await deps.openUrl(authorize.toString());
258
+ }
259
+ catch {
260
+ deps.log("Could not open the browser. Copy the URL above and open it manually.");
261
+ }
262
+ const code = await callback.code;
263
+ const now = deps.now();
264
+ const body = await postForm(deps, server.tokenEndpoint, {
265
+ grant_type: "authorization_code",
266
+ code,
267
+ redirect_uri: callback.redirectUri,
268
+ client_id: clientId,
269
+ code_verifier: verifier,
270
+ resource,
271
+ });
272
+ const token = tokenFromResponse(body, { schemaVersion: 1, resource, issuer: server.issuer, tokenEndpoint: server.tokenEndpoint, clientId }, now);
273
+ await writeStoreJson(OAUTH_TOKEN_FILE, token);
274
+ return token;
275
+ }
276
+ finally {
277
+ callback.close();
278
+ }
279
+ }
280
+ export async function readStoredOAuthToken() {
281
+ const token = await readStoreJson(OAUTH_TOKEN_FILE).catch(() => null);
282
+ return token && token.schemaVersion === 1 && typeof token.accessToken === "string" ? token : null;
283
+ }
284
+ /**
285
+ * A current store access token: the stored one, refreshed when it expires
286
+ * within a minute.
287
+ */
288
+ export async function getStoreAccessToken(deps = defaultDependencies) {
289
+ const stored = await readStoredOAuthToken();
290
+ if (!stored) {
291
+ throw new OAuthError("store_login_required", "This machine is not signed in to the Summer store.", 'Recovery: run "summer login --store" in a terminal and approve access in the browser, then retry.');
292
+ }
293
+ if (Date.parse(stored.expiresAt) - deps.now() > REFRESH_MARGIN_MS)
294
+ return stored.accessToken;
295
+ if (!stored.refreshToken) {
296
+ throw new OAuthError("store_login_expired", "The Summer store sign-in expired.", 'Recovery: run "summer login --store --force", then retry.');
297
+ }
298
+ const now = deps.now();
299
+ const body = await postForm(deps, stored.tokenEndpoint, {
300
+ grant_type: "refresh_token",
301
+ refresh_token: stored.refreshToken,
302
+ client_id: stored.clientId,
303
+ resource: stored.resource,
304
+ });
305
+ const { accessToken: _a, refreshToken: _r, expiresAt: _e, scope: _s, ...base } = stored;
306
+ const next = tokenFromResponse(body, base, now, stored.refreshToken);
307
+ await writeStoreJson(OAUTH_TOKEN_FILE, next);
308
+ return next.accessToken;
309
+ }
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Write a plain .zip (deflate, one entry per file) by streaming each file, so
3
+ * a large export never sits in memory. Sizes and CRCs go in a data descriptor
4
+ * after each entry (general purpose bit 3). Entries and archives over 4 GiB
5
+ * are refused: no zip64. Unix permissions are kept so an executable stays
6
+ * executable after unzip.
7
+ */
8
+ export interface ZipWriteEntry {
9
+ /** Path inside the zip, "/"-separated. */
10
+ name: string;
11
+ /** File on disk. */
12
+ source: string;
13
+ }
14
+ export declare function crc32Update(crc: number, data: Uint8Array): number;
15
+ export declare function writeZip(out: string, entries: ZipWriteEntry[]): Promise<{
16
+ entries: number;
17
+ bytes: number;
18
+ }>;
@@ -0,0 +1,112 @@
1
+ import { createReadStream, createWriteStream } from "node:fs";
2
+ import { stat } from "node:fs/promises";
3
+ import { once } from "node:events";
4
+ import { createDeflateRaw } from "node:zlib";
5
+ const MAX_32 = 0xffffffff;
6
+ const CRC_TABLE = (() => {
7
+ const table = new Uint32Array(256);
8
+ for (let n = 0; n < 256; n++) {
9
+ let c = n;
10
+ for (let k = 0; k < 8; k++)
11
+ c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1;
12
+ table[n] = c >>> 0;
13
+ }
14
+ return table;
15
+ })();
16
+ export function crc32Update(crc, data) {
17
+ let c = ~crc >>> 0;
18
+ for (let i = 0; i < data.length; i++)
19
+ c = CRC_TABLE[(c ^ data[i]) & 0xff] ^ (c >>> 8);
20
+ return ~c >>> 0;
21
+ }
22
+ function dosTime(date) {
23
+ const year = Math.max(1980, date.getFullYear());
24
+ return {
25
+ time: (date.getHours() << 11) | (date.getMinutes() << 5) | Math.floor(date.getSeconds() / 2),
26
+ date: ((year - 1980) << 9) | ((date.getMonth() + 1) << 5) | date.getDate(),
27
+ };
28
+ }
29
+ export async function writeZip(out, entries) {
30
+ const stream = createWriteStream(out);
31
+ let offset = 0;
32
+ const write = async (chunk) => {
33
+ offset += chunk.length;
34
+ if (!stream.write(chunk))
35
+ await once(stream, "drain");
36
+ };
37
+ const central = [];
38
+ try {
39
+ for (const entry of entries) {
40
+ const info = await stat(entry.source);
41
+ const name = Buffer.from(entry.name, "utf8");
42
+ const { time, date } = dosTime(info.mtime);
43
+ const headerOffset = offset;
44
+ const local = Buffer.alloc(30);
45
+ local.writeUInt32LE(0x04034b50, 0);
46
+ local.writeUInt16LE(20, 4);
47
+ local.writeUInt16LE(0x0808, 6); // data descriptor + UTF-8 names
48
+ local.writeUInt16LE(8, 8);
49
+ local.writeUInt16LE(time, 10);
50
+ local.writeUInt16LE(date, 12);
51
+ local.writeUInt16LE(name.length, 26);
52
+ await write(Buffer.concat([local, name]));
53
+ let crc = 0;
54
+ let size = 0;
55
+ const start = offset;
56
+ const deflate = createDeflateRaw({ level: 6 });
57
+ const pumped = (async () => {
58
+ for await (const chunk of deflate)
59
+ await write(chunk);
60
+ })();
61
+ for await (const chunk of createReadStream(entry.source)) {
62
+ crc = crc32Update(crc, chunk);
63
+ size += chunk.length;
64
+ if (!deflate.write(chunk))
65
+ await once(deflate, "drain");
66
+ }
67
+ deflate.end();
68
+ await pumped;
69
+ const compressed = offset - start;
70
+ if (size > MAX_32 || compressed > MAX_32 || offset > MAX_32) {
71
+ throw new Error(`${entry.name} is too large for a zip without zip64 (over 4 GiB).`);
72
+ }
73
+ const descriptor = Buffer.alloc(16);
74
+ descriptor.writeUInt32LE(0x08074b50, 0);
75
+ descriptor.writeUInt32LE(crc, 4);
76
+ descriptor.writeUInt32LE(compressed, 8);
77
+ descriptor.writeUInt32LE(size, 12);
78
+ await write(descriptor);
79
+ const header = Buffer.alloc(46);
80
+ header.writeUInt32LE(0x02014b50, 0);
81
+ header.writeUInt16LE((3 << 8) | 20, 4); // made by Unix, so the mode below counts
82
+ header.writeUInt16LE(20, 6);
83
+ header.writeUInt16LE(0x0808, 8);
84
+ header.writeUInt16LE(8, 10);
85
+ header.writeUInt16LE(time, 12);
86
+ header.writeUInt16LE(date, 14);
87
+ header.writeUInt32LE(crc, 16);
88
+ header.writeUInt32LE(compressed, 20);
89
+ header.writeUInt32LE(size, 24);
90
+ header.writeUInt16LE(name.length, 28);
91
+ header.writeUInt32LE((((info.mode & 0o777) | 0o100000) << 16) >>> 0, 38);
92
+ header.writeUInt32LE(headerOffset, 42);
93
+ central.push(header, name);
94
+ }
95
+ const directory = Buffer.concat(central);
96
+ const end = Buffer.alloc(22);
97
+ end.writeUInt32LE(0x06054b50, 0);
98
+ end.writeUInt16LE(entries.length, 8);
99
+ end.writeUInt16LE(entries.length, 10);
100
+ end.writeUInt32LE(directory.length, 12);
101
+ end.writeUInt32LE(offset, 16);
102
+ if (entries.length > 0xffff || offset + directory.length > MAX_32) {
103
+ throw new Error("The archive is too large for a zip without zip64.");
104
+ }
105
+ await write(Buffer.concat([directory, end]));
106
+ }
107
+ finally {
108
+ stream.end();
109
+ await once(stream, "close");
110
+ }
111
+ return { entries: entries.length, bytes: offset };
112
+ }
@@ -0,0 +1,22 @@
1
+ /**
2
+ * A minimal ZIP central-directory reader: entry names and sizes, plus small
3
+ * entries read whole (stored or deflated). Reads only the tail of the file
4
+ * and the bytes of the entries asked for, so a 40 GB export costs a few
5
+ * reads. ZIP64 sizes and offsets are honoured. No CRC check: the caller
6
+ * verifies content by hash where it matters.
7
+ */
8
+ export interface ZipEntry {
9
+ name: string;
10
+ method: number;
11
+ compressedSize: number;
12
+ uncompressedSize: number;
13
+ localHeaderOffset: number;
14
+ encrypted: boolean;
15
+ }
16
+ export declare class ZipReadError extends Error {
17
+ constructor(message: string);
18
+ }
19
+ /** Every entry in the zip's central directory. */
20
+ export declare function readZipEntries(path: string): Promise<ZipEntry[]>;
21
+ /** One entry's bytes, for small entries (manifests, configuration). */
22
+ export declare function readZipEntry(path: string, entry: ZipEntry, maxBytes?: number): Promise<Buffer>;
@@ -0,0 +1,140 @@
1
+ import { open } from "node:fs/promises";
2
+ import { inflateRawSync } from "node:zlib";
3
+ export class ZipReadError extends Error {
4
+ constructor(message) {
5
+ super(message);
6
+ this.name = "ZipReadError";
7
+ }
8
+ }
9
+ const EOCD = 0x06054b50;
10
+ const ZIP64_LOCATOR = 0x07064b50;
11
+ const ZIP64_EOCD = 0x06064b50;
12
+ const CENTRAL = 0x02014b50;
13
+ const LOCAL = 0x04034b50;
14
+ const MAX_TAIL = 0xffff + 22;
15
+ const MAX_DIRECTORY_BYTES = 64 * 1024 * 1024;
16
+ async function readAt(handle, position, length) {
17
+ const buffer = Buffer.alloc(length);
18
+ const { bytesRead } = await handle.read(buffer, 0, length, position);
19
+ if (bytesRead !== length)
20
+ throw new ZipReadError("The zip ends early.");
21
+ return buffer;
22
+ }
23
+ const u64 = (buffer, at) => Number(buffer.readBigUInt64LE(at));
24
+ async function readDirectory(handle, size) {
25
+ const tailLength = Math.min(size, MAX_TAIL);
26
+ const tail = await readAt(handle, size - tailLength, tailLength);
27
+ let eocd = -1;
28
+ for (let at = tail.length - 22; at >= 0; at--) {
29
+ if (tail.readUInt32LE(at) === EOCD) {
30
+ eocd = at;
31
+ break;
32
+ }
33
+ }
34
+ if (eocd < 0)
35
+ throw new ZipReadError("Not a zip file (no end of central directory).");
36
+ let count = tail.readUInt16LE(eocd + 10);
37
+ let directorySize = tail.readUInt32LE(eocd + 12);
38
+ let directoryOffset = tail.readUInt32LE(eocd + 16);
39
+ if (count === 0xffff || directorySize === 0xffffffff || directoryOffset === 0xffffffff) {
40
+ const locatorAt = eocd - 20;
41
+ if (locatorAt < 0 || tail.readUInt32LE(locatorAt) !== ZIP64_LOCATOR) {
42
+ throw new ZipReadError("The zip's ZIP64 locator is missing.");
43
+ }
44
+ const record = await readAt(handle, u64(tail, locatorAt + 8), 56);
45
+ if (record.readUInt32LE(0) !== ZIP64_EOCD)
46
+ throw new ZipReadError("The zip's ZIP64 record is damaged.");
47
+ count = u64(record, 32);
48
+ directorySize = u64(record, 40);
49
+ directoryOffset = u64(record, 48);
50
+ }
51
+ if (directorySize > MAX_DIRECTORY_BYTES || directoryOffset + directorySize > size) {
52
+ throw new ZipReadError("The zip's central directory is out of range.");
53
+ }
54
+ const directory = await readAt(handle, directoryOffset, directorySize);
55
+ const entries = [];
56
+ let at = 0;
57
+ for (let index = 0; index < count; index++) {
58
+ if (at + 46 > directory.length || directory.readUInt32LE(at) !== CENTRAL) {
59
+ throw new ZipReadError("The zip's central directory is damaged.");
60
+ }
61
+ const flags = directory.readUInt16LE(at + 8);
62
+ const method = directory.readUInt16LE(at + 10);
63
+ let compressedSize = directory.readUInt32LE(at + 20);
64
+ let uncompressedSize = directory.readUInt32LE(at + 24);
65
+ const nameLength = directory.readUInt16LE(at + 28);
66
+ const extraLength = directory.readUInt16LE(at + 30);
67
+ const commentLength = directory.readUInt16LE(at + 32);
68
+ let localHeaderOffset = directory.readUInt32LE(at + 42);
69
+ const name = directory.toString("utf8", at + 46, at + 46 + nameLength);
70
+ // ZIP64 extended information: only the fields that overflowed, in order.
71
+ let extra = at + 46 + nameLength;
72
+ const extraEnd = extra + extraLength;
73
+ while (extra + 4 <= extraEnd) {
74
+ const id = directory.readUInt16LE(extra);
75
+ const length = directory.readUInt16LE(extra + 2);
76
+ if (id === 0x0001) {
77
+ let field = extra + 4;
78
+ if (uncompressedSize === 0xffffffff) {
79
+ uncompressedSize = u64(directory, field);
80
+ field += 8;
81
+ }
82
+ if (compressedSize === 0xffffffff) {
83
+ compressedSize = u64(directory, field);
84
+ field += 8;
85
+ }
86
+ if (localHeaderOffset === 0xffffffff)
87
+ localHeaderOffset = u64(directory, field);
88
+ }
89
+ extra += 4 + length;
90
+ }
91
+ entries.push({
92
+ name,
93
+ method,
94
+ compressedSize,
95
+ uncompressedSize,
96
+ localHeaderOffset,
97
+ encrypted: (flags & 1) !== 0,
98
+ });
99
+ at = extraEnd + commentLength;
100
+ }
101
+ return entries;
102
+ }
103
+ /** Every entry in the zip's central directory. */
104
+ export async function readZipEntries(path) {
105
+ const handle = await open(path, "r");
106
+ try {
107
+ return await readDirectory(handle, (await handle.stat()).size);
108
+ }
109
+ finally {
110
+ await handle.close();
111
+ }
112
+ }
113
+ /** One entry's bytes, for small entries (manifests, configuration). */
114
+ export async function readZipEntry(path, entry, maxBytes = 1024 * 1024) {
115
+ if (entry.encrypted)
116
+ throw new ZipReadError(`${entry.name} is encrypted.`);
117
+ if (entry.uncompressedSize > maxBytes || entry.compressedSize > maxBytes) {
118
+ throw new ZipReadError(`${entry.name} is larger than ${maxBytes} bytes.`);
119
+ }
120
+ const handle = await open(path, "r");
121
+ try {
122
+ const header = await readAt(handle, entry.localHeaderOffset, 30);
123
+ if (header.readUInt32LE(0) !== LOCAL)
124
+ throw new ZipReadError(`${entry.name} has a damaged local header.`);
125
+ const dataAt = entry.localHeaderOffset + 30 + header.readUInt16LE(26) + header.readUInt16LE(28);
126
+ const data = await readAt(handle, dataAt, entry.compressedSize);
127
+ if (entry.method === 0)
128
+ return data;
129
+ if (entry.method === 8) {
130
+ const inflated = inflateRawSync(data, { maxOutputLength: maxBytes });
131
+ if (inflated.length !== entry.uncompressedSize)
132
+ throw new ZipReadError(`${entry.name} did not inflate to its size.`);
133
+ return inflated;
134
+ }
135
+ throw new ZipReadError(`${entry.name} uses unsupported compression method ${entry.method}.`);
136
+ }
137
+ finally {
138
+ await handle.close();
139
+ }
140
+ }
@@ -0,0 +1,50 @@
1
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { type HostedClient, type HostedConnectDependencies } from "../core/hosted-mcp.js";
3
+ export type { HostedClient };
4
+ /**
5
+ * One Summer Engine MCP. The cloud tools a creator's agent needs to publish
6
+ * (projects, store page, build upload, submit for the owner's approval, the
7
+ * board, Grow) live on the hosted Summer Engine MCP at mcp.summerengine.com,
8
+ * the same server ChatGPT and Claude connect to. This module mounts them into
9
+ * the local MCP so a local agent sees one server: each hosted tool, prompt
10
+ * and resource is registered here under its own name and forwarded with the
11
+ * person's store sign-in ("summer login --store"; the token's audience is
12
+ * that server). A name the local MCP already has stays local (the engine and
13
+ * generation tools keep their CLI-token path).
14
+ *
15
+ * Mounting runs after connect. The first tools/list waits for a mount in
16
+ * progress (up to FIRST_LIST_WAIT_MS), so a host that lists tools once still
17
+ * sees the store tools; a slower mount adds them later (tools/list_changed).
18
+ * Without a store sign-in the mount fails at once, and one tool,
19
+ * summer_store_tools, says how to sign in and mounts on the next call.
20
+ */
21
+ export declare const STORE_TOOLS_STATUS = "summer_store_tools";
22
+ /** How long the first tools/list waits for a mount in progress, so hosts that list once see the store tools. */
23
+ export declare const FIRST_LIST_WAIT_MS = 5000;
24
+ export type HostedMountDependencies = HostedConnectDependencies;
25
+ export declare const defaultHostedMountDependencies: HostedMountDependencies;
26
+ export interface HostedMountState {
27
+ status: "idle" | "mounting" | "mounted" | "unavailable";
28
+ tools: string[];
29
+ prompts: string[];
30
+ resources: string[];
31
+ /** Hosted names the local MCP already defines (kept local). */
32
+ collisions: string[];
33
+ error?: {
34
+ code: string;
35
+ message: string;
36
+ recovery: string;
37
+ };
38
+ }
39
+ /**
40
+ * Get the server ready for hosted tools before the transport connects (the
41
+ * SDK fixes capabilities at connect): resource handlers exist, and tools/list
42
+ * reports each mounted tool's own JSON Schema rather than the passthrough one.
43
+ * Returns mount(), which callers run after connect without awaiting it.
44
+ */
45
+ export declare function prepareHostedMount(server: McpServer, deps?: HostedMountDependencies, options?: {
46
+ firstListWaitMs?: number;
47
+ }): {
48
+ mount: () => Promise<HostedMountState>;
49
+ state: HostedMountState;
50
+ };