@tokenoftrust/storefront-runner 2.0.3 → 2.0.4
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/apps/storefront/.astro/content.d.ts +159 -0
- package/apps/storefront/.astro/integrations/_astrojs_cloudflare/cloudflare.d.ts +1 -0
- package/apps/storefront/.astro/types.d.ts +3 -0
- package/apps/storefront/node_modules/.bin/acorn +43 -0
- package/apps/storefront/node_modules/.bin/astro +43 -0
- package/apps/storefront/node_modules/.bin/astro-check +43 -0
- package/apps/storefront/node_modules/.bin/browserslist +43 -0
- package/apps/storefront/node_modules/.bin/cf-wrangler +43 -0
- package/apps/storefront/node_modules/.bin/depcruise +43 -0
- package/apps/storefront/node_modules/.bin/depcruise-baseline +43 -0
- package/apps/storefront/node_modules/.bin/depcruise-fmt +43 -0
- package/apps/storefront/node_modules/.bin/depcruise-wrap-stream-in-html +43 -0
- package/apps/storefront/node_modules/.bin/dependency-cruise +43 -0
- package/apps/storefront/node_modules/.bin/dependency-cruiser +43 -0
- package/apps/storefront/node_modules/.bin/drizzle-kit +43 -0
- package/apps/storefront/node_modules/.bin/esbuild +43 -0
- package/apps/storefront/node_modules/.bin/jiti +43 -0
- package/apps/storefront/node_modules/.bin/playwright +43 -0
- package/apps/storefront/node_modules/.bin/playwright-core +43 -0
- package/apps/storefront/node_modules/.bin/prettier +43 -0
- package/apps/storefront/node_modules/.bin/rollup +43 -0
- package/apps/storefront/node_modules/.bin/tsc +43 -0
- package/apps/storefront/node_modules/.bin/tsserver +43 -0
- package/apps/storefront/node_modules/.bin/tsx +43 -0
- package/apps/storefront/node_modules/.bin/vite +43 -0
- package/apps/storefront/node_modules/.bin/vitest +43 -0
- package/apps/storefront/node_modules/.bin/workerd +43 -0
- package/apps/storefront/node_modules/.bin/wrangler +43 -0
- package/apps/storefront/node_modules/.bin/wrangler2 +43 -0
- package/apps/storefront/node_modules/.bin/yaml +43 -0
- package/apps/storefront/node_modules/.vite/deps/@astrojs_preact_client__js.js +58 -0
- package/apps/storefront/node_modules/.vite/deps/@astrojs_preact_client__js.js.map +1 -0
- package/apps/storefront/node_modules/.vite/deps/@astrojs_preact_n_@preact_signals.js +741 -0
- package/apps/storefront/node_modules/.vite/deps/@astrojs_preact_n_@preact_signals.js.map +1 -0
- package/apps/storefront/node_modules/.vite/deps/@astrojs_preact_server__js.js +2549 -0
- package/apps/storefront/node_modules/.vite/deps/@astrojs_preact_server__js.js.map +1 -0
- package/apps/storefront/node_modules/.vite/deps/_metadata.json +150 -0
- package/apps/storefront/node_modules/.vite/deps/astro-BIHuWFxm.js +434 -0
- package/apps/storefront/node_modules/.vite/deps/astro-BIHuWFxm.js.map +1 -0
- package/apps/storefront/node_modules/.vite/deps/astro_n_aria-query.js +6390 -0
- package/apps/storefront/node_modules/.vite/deps/astro_n_aria-query.js.map +1 -0
- package/apps/storefront/node_modules/.vite/deps/astro_n_axobject-query.js +2705 -0
- package/apps/storefront/node_modules/.vite/deps/astro_n_axobject-query.js.map +1 -0
- package/apps/storefront/node_modules/.vite/deps/astro_n_html-escaper.js +65 -0
- package/apps/storefront/node_modules/.vite/deps/astro_n_html-escaper.js.map +1 -0
- package/apps/storefront/node_modules/.vite/deps/astro_runtime_client_dev-toolbar_entrypoint__js.js +455 -0
- package/apps/storefront/node_modules/.vite/deps/astro_runtime_client_dev-toolbar_entrypoint__js.js.map +1 -0
- package/apps/storefront/node_modules/.vite/deps/audit-DKDpcdEI.js +1535 -0
- package/apps/storefront/node_modules/.vite/deps/audit-DKDpcdEI.js.map +1 -0
- package/apps/storefront/node_modules/.vite/deps/highlight-B9UoX1wa.js +48 -0
- package/apps/storefront/node_modules/.vite/deps/highlight-B9UoX1wa.js.map +1 -0
- package/apps/storefront/node_modules/.vite/deps/icons-BWi2wI9n.js +42 -0
- package/apps/storefront/node_modules/.vite/deps/icons-BWi2wI9n.js.map +1 -0
- package/apps/storefront/node_modules/.vite/deps/package.json +3 -0
- package/apps/storefront/node_modules/.vite/deps/preact.js +310 -0
- package/apps/storefront/node_modules/.vite/deps/preact.js.map +1 -0
- package/apps/storefront/node_modules/.vite/deps/preact_compat.js +420 -0
- package/apps/storefront/node_modules/.vite/deps/preact_compat.js.map +1 -0
- package/apps/storefront/node_modules/.vite/deps/preact_compat_jsx-runtime.js +4 -0
- package/apps/storefront/node_modules/.vite/deps/preact_debug.js +236 -0
- package/apps/storefront/node_modules/.vite/deps/preact_debug.js.map +1 -0
- package/apps/storefront/node_modules/.vite/deps/preact_devtools.js +12 -0
- package/apps/storefront/node_modules/.vite/deps/preact_devtools.js.map +1 -0
- package/apps/storefront/node_modules/.vite/deps/preact_hooks.js +192 -0
- package/apps/storefront/node_modules/.vite/deps/preact_hooks.js.map +1 -0
- package/apps/storefront/node_modules/.vite/deps/preact_jsx-dev-runtime.js +3 -0
- package/apps/storefront/node_modules/.vite/deps/preact_jsx-runtime.js +92 -0
- package/apps/storefront/node_modules/.vite/deps/preact_jsx-runtime.js.map +1 -0
- package/apps/storefront/node_modules/.vite/deps/preact_test-utils.js +59 -0
- package/apps/storefront/node_modules/.vite/deps/preact_test-utils.js.map +1 -0
- package/apps/storefront/node_modules/.vite/deps/rolldown-runtime-BPOCksWG.js +24 -0
- package/apps/storefront/node_modules/.vite/deps/settings-BVDV0Roj.js +189 -0
- package/apps/storefront/node_modules/.vite/deps/settings-BVDV0Roj.js.map +1 -0
- package/apps/storefront/node_modules/.vite/deps/static-html-DveYqwMo.js +13 -0
- package/apps/storefront/node_modules/.vite/deps/static-html-DveYqwMo.js.map +1 -0
- package/apps/storefront/node_modules/.vite/deps/toolbar-B3VIHCXg.js +458 -0
- package/apps/storefront/node_modules/.vite/deps/toolbar-B3VIHCXg.js.map +1 -0
- package/apps/storefront/node_modules/.vite/deps/ui-library-BK6ricUd.js +1016 -0
- package/apps/storefront/node_modules/.vite/deps/ui-library-BK6ricUd.js.map +1 -0
- package/apps/storefront/node_modules/.vite/deps/window-BvxDenKN.js +32 -0
- package/apps/storefront/node_modules/.vite/deps/window-BvxDenKN.js.map +1 -0
- package/apps/storefront/node_modules/.vite/deps/xray-YFiRUU0Z.js +113 -0
- package/apps/storefront/node_modules/.vite/deps/xray-YFiRUU0Z.js.map +1 -0
- package/apps/storefront/node_modules/.vite/deps_ssr/_metadata.json +8 -0
- package/apps/storefront/node_modules/.vite/deps_ssr/package.json +3 -0
- package/apps/storefront/src/layouts/Layout.astro +1 -1
- package/apps/storefront/src/lib/apps/registryAccess.ts +45 -0
- package/apps/storefront/src/lib/auth/adminEntry.ts +4 -4
- package/apps/storefront/src/lib/shared/capability.ts +68 -0
- package/apps/storefront/src/lib/shared/developerInvites.ts +1250 -0
- package/apps/storefront/src/lib/shared/invitePreflight.ts +123 -0
- package/apps/storefront/src/lib/webhooks/deliveryEngine.ts +38 -4
- package/apps/storefront/src/lib/webhooks/emit.ts +1 -1
- package/apps/storefront/src/lib/webhooks/getDispatcher.ts +1 -1
- package/apps/storefront/src/pages/api/auth/magic-exchange.ts +1 -1
- package/apps/storefront/src/pages/index.astro +1 -1
- package/apps/storefront/src/pages/products/[handle].astro +1 -1
- package/package.json +1 -1
- package/packages/public-runtime/node_modules/.bin/tsc +43 -0
- package/packages/public-runtime/node_modules/.bin/tsserver +43 -0
- package/packages/public-runtime/node_modules/.bin/vitest +43 -0
- package/pnpm-workspace.yaml +3 -0
- package/apps/storefront/src/lib/apps/adminService.ts +0 -116
- package/apps/storefront/src/lib/apps/adminSession.ts +0 -214
- package/apps/storefront/src/lib/apps/apiAuth.ts +0 -91
- package/apps/storefront/src/lib/apps/apiRoute.ts +0 -35
- package/apps/storefront/src/lib/apps/catalogMapper.ts +0 -39
- package/apps/storefront/src/lib/apps/consentConfigStore.ts +0 -228
- package/apps/storefront/src/lib/apps/gtmMigrationInstall.ts +0 -447
- package/apps/storefront/src/lib/apps/gtmMigrationReport.ts +0 -367
- package/apps/storefront/src/lib/apps/gtmMigrationTab.ts +0 -315
- package/apps/storefront/src/lib/apps/healthAggregate.ts +0 -66
- package/apps/storefront/src/lib/apps/integrationLifecycle.ts +0 -614
- package/apps/storefront/src/lib/apps/integrationLifecycleController.ts +0 -125
- package/apps/storefront/src/lib/apps/integrationReadModel.ts +0 -563
- package/apps/storefront/src/lib/apps/integrationsTab.ts +0 -547
- package/apps/storefront/src/lib/apps/integrationsView.ts +0 -106
- package/apps/storefront/src/lib/apps/managedAnalyticsService.ts +0 -599
- package/apps/storefront/src/lib/apps/tenantIntegrationMaterialization.ts +0 -170
- package/apps/storefront/src/lib/apps/tokenIssuer.ts +0 -121
- package/apps/storefront/src/lib/apps/tokenVerifier.ts +0 -148
- package/apps/storefront/src/lib/apps/v84CleanCloneReplayContext.ts +0 -16
- package/apps/storefront/src/lib/apps/v84CloneLifecycleCacheEvidence.ts +0 -105
- package/apps/storefront/src/lib/apps/v84CloneLifecycleEvidence.ts +0 -177
- package/apps/storefront/src/lib/apps/v84CloneLifecycleMaterialization.ts +0 -251
- package/apps/storefront/src/lib/apps/v84CloneLifecycleSemantics.ts +0 -241
- package/apps/storefront/src/lib/apps/v84CloneMigrationEvidence.ts +0 -211
- package/apps/storefront/src/lib/apps/v84CloneNativeInstallEvidence.ts +0 -409
- package/apps/storefront/src/lib/apps/v84NonportableCodeCoaching.ts +0 -368
- package/apps/storefront/src/lib/apps/v84NonportableRouteContract.ts +0 -188
- package/apps/storefront/src/lib/apps/v84NonportableRouteDogfood.ts +0 -291
- package/apps/storefront/src/lib/apps/ws1ManagedAnalyticsClient.ts +0 -95
- package/apps/storefront/src/lib/content-edit/applyDiff.ts +0 -91
- package/apps/storefront/src/lib/content-edit/client.ts +0 -286
- package/apps/storefront/src/lib/content-edit/index.ts +0 -14
- package/apps/storefront/src/lib/content-edit/route.ts +0 -25
- package/apps/storefront/src/lib/content-edit/types.ts +0 -19
- package/apps/storefront/src/pages/api/content-edit/ai-edit.ts +0 -164
|
@@ -1,286 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* JsonRpcContentEditClient — the storefront's server-to-server caller for
|
|
3
|
-
* tot-mcp's `content_edit` tool (the inline-AI edit loop, WS5 G1). Mirrors
|
|
4
|
-
* {@link JsonRpcChangeClient} exactly (same transport, same session-validation,
|
|
5
|
-
* same tool-result unwrap, same typed errors) — reusing the forge seam's
|
|
6
|
-
* exported transport + error types so there is ONE wire contract, not two.
|
|
7
|
-
*
|
|
8
|
-
* AUTH SHAPE (Option A, `content-edit-machine-identity` — mirrors change/B3):
|
|
9
|
-
* the storefront backend calls `content_edit` with the SAME machine-identity
|
|
10
|
-
* APP token the change/forge clients use — a `client_credentials` +
|
|
11
|
-
* `private_key_jwt` token minted against the MCP's OAuth token endpoint via
|
|
12
|
-
* {@link AppTokenProvider}, attached as Bearer at `initialize` and on every
|
|
13
|
-
* request by {@link FetchTransport} — NOT the retired `credential_validate`
|
|
14
|
-
* handshake and NOT a human `TOT_ACCESS_TOKEN`. Developer-level authorization
|
|
15
|
-
* is, and remains, the caller's job: `/dev`'s own `readViewerSession()` +
|
|
16
|
-
* capability check (BEFORE this client is ever constructed) derives the
|
|
17
|
-
* tenant from the signed-in developer's OWN session, never from a
|
|
18
|
-
* caller-supplied value — tenant isolation is enforced there, unaffected by
|
|
19
|
-
* this change. This client only proves "a legitimate storefront backend
|
|
20
|
-
* (machine identity) is asking", the same trust boundary
|
|
21
|
-
* `/api/changes/accept` already relies on post-B3.
|
|
22
|
-
*/
|
|
23
|
-
import {
|
|
24
|
-
FetchTransport,
|
|
25
|
-
ForgeAuthError,
|
|
26
|
-
ForgeConfigError,
|
|
27
|
-
ForgeTransportError,
|
|
28
|
-
SESSION_HEADER,
|
|
29
|
-
AppTokenProvider,
|
|
30
|
-
deriveMcpOauthEndpoints,
|
|
31
|
-
type McpTransport,
|
|
32
|
-
type JsonRpcRequest,
|
|
33
|
-
type TotCredentials,
|
|
34
|
-
type TransportResponse,
|
|
35
|
-
type BearerProvider,
|
|
36
|
-
} from "@/lib/forge";
|
|
37
|
-
import { mapForgeError } from "@/lib/forge/errors.js";
|
|
38
|
-
import { readEnv } from "@/lib/env";
|
|
39
|
-
import type { ContentEditFile, ContentEditResult } from "./types.js";
|
|
40
|
-
|
|
41
|
-
export const CONTENT_EDIT_TOOL = {
|
|
42
|
-
/**
|
|
43
|
-
* Session-validation tool for the NON-OAuth (api-key/secret) construction —
|
|
44
|
-
* kept ONLY for backward-compatible test-transport injection (no
|
|
45
|
-
* bearerProvider). The deployed path never calls this — see the module
|
|
46
|
-
* header.
|
|
47
|
-
*/
|
|
48
|
-
validate: "credential_validate",
|
|
49
|
-
edit: "content_edit",
|
|
50
|
-
} as const;
|
|
51
|
-
|
|
52
|
-
export { SESSION_HEADER } from "@/lib/forge";
|
|
53
|
-
|
|
54
|
-
// --- MCP tool-result envelope (mirrors the change client) -------------------
|
|
55
|
-
|
|
56
|
-
interface McpToolResult {
|
|
57
|
-
content?: Array<{ type?: string; text?: string }>;
|
|
58
|
-
structuredContent?: unknown;
|
|
59
|
-
isError?: boolean;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
function parseTextContent(content: McpToolResult["content"]): unknown {
|
|
63
|
-
const text = content?.find((c) => c.type === "text" || c.text != null)?.text;
|
|
64
|
-
if (text == null) return undefined;
|
|
65
|
-
try {
|
|
66
|
-
return JSON.parse(text);
|
|
67
|
-
} catch {
|
|
68
|
-
return text;
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
function unwrapToolResult<T>(result: unknown): T {
|
|
73
|
-
const env = (result ?? {}) as McpToolResult;
|
|
74
|
-
const payload =
|
|
75
|
-
env.structuredContent !== undefined
|
|
76
|
-
? env.structuredContent
|
|
77
|
-
: parseTextContent(env.content);
|
|
78
|
-
|
|
79
|
-
if (env.isError) {
|
|
80
|
-
const p = (payload ?? {}) as Record<string, unknown>;
|
|
81
|
-
throw mapForgeError({
|
|
82
|
-
code: typeof p.code === "number" ? p.code : undefined,
|
|
83
|
-
message: typeof p.message === "string" ? p.message : "content_edit tool error",
|
|
84
|
-
data: (p.data as Record<string, unknown>) ?? p,
|
|
85
|
-
});
|
|
86
|
-
}
|
|
87
|
-
return payload as T;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
// --- Config -------------------------------------------------------------
|
|
91
|
-
|
|
92
|
-
export interface JsonRpcContentEditClientConfig {
|
|
93
|
-
/** MCP base URL (the `POST /mcp` endpoint). */
|
|
94
|
-
baseUrl: string;
|
|
95
|
-
/** Storefront's ToT credentials for the legacy `credential_validate` path. */
|
|
96
|
-
credentials: TotCredentials;
|
|
97
|
-
/** Transport override (defaults to fetch). Injected by tests. */
|
|
98
|
-
transport?: McpTransport;
|
|
99
|
-
/**
|
|
100
|
-
* Machine-identity app-token source (mirrors forge's B2 / change's B3,
|
|
101
|
-
* binding `c0-forge-auth-supersede`). When present, the client
|
|
102
|
-
* authenticates PURELY via a `client_credentials` + `private_key_jwt` app
|
|
103
|
-
* token — attached as Bearer at `initialize` and on every request by
|
|
104
|
-
* {@link FetchTransport}, re-minted on expiry/401 — and NEVER calls the
|
|
105
|
-
* retired `credential_validate` handshake. Built by
|
|
106
|
-
* {@link createContentEditClient} from `MCP_BASE_URL`; tests may inject
|
|
107
|
-
* their own {@link BearerProvider}.
|
|
108
|
-
*/
|
|
109
|
-
bearerProvider?: BearerProvider;
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
// --- The client -----------------------------------------------------------
|
|
113
|
-
|
|
114
|
-
export class JsonRpcContentEditClient {
|
|
115
|
-
private readonly baseUrl: string;
|
|
116
|
-
private readonly credentials: TotCredentials;
|
|
117
|
-
private readonly transport: McpTransport;
|
|
118
|
-
private nextId = 1;
|
|
119
|
-
/** Cached session validation — validate once, reuse (lazily). */
|
|
120
|
-
private sessionPromise?: Promise<string | undefined>;
|
|
121
|
-
/** True when a machine-identity app token authenticates the session (mirrors forge B2 / change B3). */
|
|
122
|
-
private readonly usesServiceAuth: boolean;
|
|
123
|
-
|
|
124
|
-
constructor(config: JsonRpcContentEditClientConfig) {
|
|
125
|
-
if (!config.baseUrl) {
|
|
126
|
-
throw new ForgeConfigError("content-edit: MCP_BASE_URL is not configured");
|
|
127
|
-
}
|
|
128
|
-
if (!config.credentials?.apiKey) {
|
|
129
|
-
throw new ForgeConfigError(
|
|
130
|
-
"content-edit: storefront ToT credentials are not configured",
|
|
131
|
-
);
|
|
132
|
-
}
|
|
133
|
-
this.baseUrl = config.baseUrl;
|
|
134
|
-
this.credentials = config.credentials;
|
|
135
|
-
this.usesServiceAuth = config.bearerProvider != null;
|
|
136
|
-
this.transport =
|
|
137
|
-
config.transport ?? new FetchTransport(config.baseUrl, config.bearerProvider);
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
async editContent(
|
|
141
|
-
prompt: string,
|
|
142
|
-
files: readonly ContentEditFile[],
|
|
143
|
-
): Promise<ContentEditResult> {
|
|
144
|
-
const sessionId = await this.ensureSession();
|
|
145
|
-
const result = await this.rpc(
|
|
146
|
-
"tools/call",
|
|
147
|
-
{ name: CONTENT_EDIT_TOOL.edit, arguments: { prompt, files } },
|
|
148
|
-
sessionId,
|
|
149
|
-
);
|
|
150
|
-
return unwrapToolResult<ContentEditResult>(result);
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
// --- internals (mirror JsonRpcChangeClient) --------------------------------
|
|
154
|
-
|
|
155
|
-
private ensureSession(): Promise<string | undefined> {
|
|
156
|
-
// Service-auth (mirrors forge B2 / change B3 / binding
|
|
157
|
-
// `c0-forge-auth-supersede`): the app token is attached as Bearer at the
|
|
158
|
-
// transport layer (initialize + every request), so the legacy
|
|
159
|
-
// `credential_validate` round-trip is skipped entirely — it was retired
|
|
160
|
-
// MCP-side and would now be rejected. FetchTransport still threads the
|
|
161
|
-
// real `Mcp-Session-Id` the Bearer-authed handshake minted, so a session
|
|
162
|
-
// id is never needed from here. Without a bearerProvider (e.g. an
|
|
163
|
-
// injected mock transport in unit tests) the legacy validate path is kept.
|
|
164
|
-
this.sessionPromise ??= this.usesServiceAuth
|
|
165
|
-
? Promise.resolve(undefined)
|
|
166
|
-
: this.validateSession();
|
|
167
|
-
return this.sessionPromise;
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
private async validateSession(): Promise<string | undefined> {
|
|
171
|
-
const args = this.credentialArgs();
|
|
172
|
-
let res: TransportResponse;
|
|
173
|
-
try {
|
|
174
|
-
res = await this.rpcRaw(
|
|
175
|
-
"tools/call",
|
|
176
|
-
{ name: CONTENT_EDIT_TOOL.validate, arguments: args },
|
|
177
|
-
undefined,
|
|
178
|
-
);
|
|
179
|
-
} catch (err) {
|
|
180
|
-
this.sessionPromise = undefined; // let a later call retry
|
|
181
|
-
throw err;
|
|
182
|
-
}
|
|
183
|
-
if (res.body.error) {
|
|
184
|
-
this.sessionPromise = undefined;
|
|
185
|
-
throw new ForgeAuthError(
|
|
186
|
-
res.body.error.message ?? "content-edit: credential validation failed",
|
|
187
|
-
);
|
|
188
|
-
}
|
|
189
|
-
try {
|
|
190
|
-
const validated = unwrapToolResult<{ session?: string; sessionId?: string }>(
|
|
191
|
-
res.body.result,
|
|
192
|
-
);
|
|
193
|
-
return (
|
|
194
|
-
res.sessionId ?? validated?.session ?? validated?.sessionId ?? undefined
|
|
195
|
-
);
|
|
196
|
-
} catch (err) {
|
|
197
|
-
this.sessionPromise = undefined;
|
|
198
|
-
throw err instanceof ForgeAuthError
|
|
199
|
-
? err
|
|
200
|
-
: new ForgeAuthError("content-edit: credential validation was rejected", {
|
|
201
|
-
cause: err,
|
|
202
|
-
});
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
/** Build the credential payload; omits absent secrets. Never logged. */
|
|
207
|
-
private credentialArgs(): Record<string, unknown> {
|
|
208
|
-
const c = this.credentials;
|
|
209
|
-
const out: Record<string, unknown> = { apiKey: c.apiKey };
|
|
210
|
-
if (c.secretKey) out.secretKey = c.secretKey;
|
|
211
|
-
if (c.accessToken) out.accessToken = c.accessToken;
|
|
212
|
-
return out;
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
private async rpc(
|
|
216
|
-
method: string,
|
|
217
|
-
params: unknown,
|
|
218
|
-
sessionId: string | undefined,
|
|
219
|
-
): Promise<unknown> {
|
|
220
|
-
const res = await this.rpcRaw(method, params, sessionId);
|
|
221
|
-
if (res.body.error) throw mapForgeError(res.body.error);
|
|
222
|
-
return res.body.result;
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
private async rpcRaw(
|
|
226
|
-
method: string,
|
|
227
|
-
params: unknown,
|
|
228
|
-
sessionId: string | undefined,
|
|
229
|
-
): Promise<TransportResponse> {
|
|
230
|
-
const headers: Record<string, string> = {};
|
|
231
|
-
if (sessionId) headers[SESSION_HEADER] = sessionId;
|
|
232
|
-
const body: JsonRpcRequest = {
|
|
233
|
-
jsonrpc: "2.0",
|
|
234
|
-
id: this.nextId++,
|
|
235
|
-
method,
|
|
236
|
-
params,
|
|
237
|
-
};
|
|
238
|
-
try {
|
|
239
|
-
return await this.transport.post({ body, headers });
|
|
240
|
-
} catch (err) {
|
|
241
|
-
if (err instanceof ForgeTransportError) throw err;
|
|
242
|
-
throw new ForgeTransportError(
|
|
243
|
-
`content-edit: transport failure calling ${method}`,
|
|
244
|
-
{ cause: err },
|
|
245
|
-
);
|
|
246
|
-
}
|
|
247
|
-
}
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
// --- Factory ----------------------------------------------------------------
|
|
251
|
-
|
|
252
|
-
/**
|
|
253
|
-
* Build a {@link JsonRpcContentEditClient} from the runtime env — the SAME env
|
|
254
|
-
* the change client reads:
|
|
255
|
-
* - `MCP_BASE_URL` — the `POST /mcp` endpoint (its ORIGIN also hosts the OAuth
|
|
256
|
-
* token endpoint + resource the app token binds to; already `…/storefront`
|
|
257
|
-
* per B4, so the app token's resource + this RPC endpoint match).
|
|
258
|
-
* - `TOT_API_KEY` (+ optional `TOT_SECRET_KEY`) — storefront's OWN ToT
|
|
259
|
-
* credentials (retained for legacy/local paths; the deployed content-edit
|
|
260
|
-
* session no longer uses them for auth).
|
|
261
|
-
*
|
|
262
|
-
* Machine identity (mirrors forge B2 / change B3 / binding
|
|
263
|
-
* `c0-forge-auth-supersede`): the content-edit session authenticates with a
|
|
264
|
-
* `client_credentials` + `private_key_jwt` APP token minted against the MCP's
|
|
265
|
-
* OAuth token endpoint via {@link AppTokenProvider} — NOT a human
|
|
266
|
-
* `TOT_ACCESS_TOKEN` and NOT a shared secret. `TOT_ACCESS_TOKEN` is
|
|
267
|
-
* deliberately NOT read here.
|
|
268
|
-
*
|
|
269
|
-
* Throws {@link ForgeConfigError} when the base URL or api key is missing.
|
|
270
|
-
* Secrets are read via {@link readEnv} and never logged.
|
|
271
|
-
*/
|
|
272
|
-
export async function createContentEditClient(
|
|
273
|
-
overrides: Partial<JsonRpcContentEditClientConfig> = {},
|
|
274
|
-
): Promise<JsonRpcContentEditClient> {
|
|
275
|
-
const baseUrl = overrides.baseUrl ?? (await readEnv("MCP_BASE_URL")) ?? "";
|
|
276
|
-
const credentials =
|
|
277
|
-
overrides.credentials ??
|
|
278
|
-
({
|
|
279
|
-
apiKey: (await readEnv("TOT_API_KEY")) ?? "",
|
|
280
|
-
secretKey: await readEnv("TOT_SECRET_KEY"),
|
|
281
|
-
} satisfies TotCredentials);
|
|
282
|
-
const bearerProvider =
|
|
283
|
-
overrides.bearerProvider ??
|
|
284
|
-
(baseUrl ? new AppTokenProvider(deriveMcpOauthEndpoints(baseUrl)) : undefined);
|
|
285
|
-
return new JsonRpcContentEditClient({ ...overrides, baseUrl, credentials, bearerProvider });
|
|
286
|
-
}
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* content_edit integration — public barrel. Import the typed client and its
|
|
3
|
-
* route helper from here (`@/lib/content-edit`) so call sites never reach into
|
|
4
|
-
* individual files.
|
|
5
|
-
*/
|
|
6
|
-
export type { ContentEditFile, ContentEditFileDiff, ContentEditResult } from "./types.js";
|
|
7
|
-
export {
|
|
8
|
-
JsonRpcContentEditClient,
|
|
9
|
-
createContentEditClient,
|
|
10
|
-
CONTENT_EDIT_TOOL,
|
|
11
|
-
SESSION_HEADER,
|
|
12
|
-
} from "./client.js";
|
|
13
|
-
export type { JsonRpcContentEditClientConfig } from "./client.js";
|
|
14
|
-
export { getContentEditClient } from "./route.js";
|
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Route helper for `/api/content-edit/ai-edit` — mirrors `@/lib/change/route/route.ts`'s
|
|
3
|
-
* `getChangeClient`: build the env-configured client, or a 503 Response when
|
|
4
|
-
* the MCP integration isn't configured (missing `MCP_BASE_URL`/credentials).
|
|
5
|
-
*/
|
|
6
|
-
import { ForgeError } from "@/lib/forge";
|
|
7
|
-
import { createContentEditClient } from "./client.js";
|
|
8
|
-
import { json } from "@/lib/grants/route.js";
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* Build a content-edit client over the real MCP from env, or return a 503
|
|
12
|
-
* Response when the integration is not configured. Never leaks secret values.
|
|
13
|
-
*/
|
|
14
|
-
export async function getContentEditClient(): Promise<
|
|
15
|
-
Awaited<ReturnType<typeof createContentEditClient>> | Response
|
|
16
|
-
> {
|
|
17
|
-
try {
|
|
18
|
-
return await createContentEditClient();
|
|
19
|
-
} catch (err) {
|
|
20
|
-
if (err instanceof ForgeError && err.kind === "config") {
|
|
21
|
-
return json({ error: "content_edit integration not configured" }, 503);
|
|
22
|
-
}
|
|
23
|
-
throw err;
|
|
24
|
-
}
|
|
25
|
-
}
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
/** Wire shape for tot-mcp's `content_edit` tool — mirrors tot-mcp's own
|
|
2
|
-
* ContentFile/ContentFileDiff/ContentEditResult (src/services/content-editor.ts). */
|
|
3
|
-
|
|
4
|
-
export interface ContentEditFile {
|
|
5
|
-
/** e.g. "content/home.html" or "theme.json" — content_edit's own allowlist. */
|
|
6
|
-
path: string;
|
|
7
|
-
content: string;
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
export interface ContentEditFileDiff {
|
|
11
|
-
path: string;
|
|
12
|
-
/** Unified diff (`--- a/<path>` / `+++ b/<path>`) against the given content. */
|
|
13
|
-
diff: string;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
export interface ContentEditResult {
|
|
17
|
-
diffs: ContentEditFileDiff[];
|
|
18
|
-
summary: string;
|
|
19
|
-
}
|
|
@@ -1,164 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* POST /api/content-edit/ai-edit — the `/dev` page's "Build with Claude" live loop
|
|
3
|
-
* (WS5 G4 + decision draft-overlay-same-origin). A prompt becomes a reviewable
|
|
4
|
-
* per-file diff via tot-mcp's `content_edit` tool, applied to the signed-in
|
|
5
|
-
* developer's OWN tenant content — and materialized into their private DRAFT
|
|
6
|
-
* overlay so the /dev live preview shows the change instantly (visible AND
|
|
7
|
-
* interactive), while live/test channels and every other viewer are untouched.
|
|
8
|
-
*
|
|
9
|
-
* AUTH: `/dev`'s own signed-in-viewer session (`readViewerSession`) is the
|
|
10
|
-
* developer-level gate; the MCP call authenticates as the storefront's own
|
|
11
|
-
* TENANT-OPERATOR credential (`getContentEditClient` — the same
|
|
12
|
-
* credential_validate path `/api/changes/accept` uses). The tenant edited is the
|
|
13
|
-
* SESSION'S OWN first membership — never a caller-supplied tenant.
|
|
14
|
-
*
|
|
15
|
-
* DRAFT MATERIALIZATION: content_edit returns unified DIFFS, not contents, so we
|
|
16
|
-
* re-apply each diff to the exact content we sent (applyUnifiedDiff, strict +
|
|
17
|
-
* fail-closed) and write the results to `dev-draft:` KV keys with a TTL, minting
|
|
18
|
-
* the signed tot_dev_draft cookie the middleware's overlay verifies. When KV or
|
|
19
|
-
* the signing secret is absent (e.g. bare `astro dev`), the edit still runs and
|
|
20
|
-
* returns diffs — `applied:false` tells the page the preview won't change.
|
|
21
|
-
*/
|
|
22
|
-
import type { APIContext } from "astro";
|
|
23
|
-
import { readViewerSession } from "@/lib/auth/route";
|
|
24
|
-
import { getContentEditClient } from "@/lib/content-edit";
|
|
25
|
-
import { applyUnifiedDiff, DiffApplyError } from "@/lib/content-edit/applyDiff";
|
|
26
|
-
import { json } from "@/lib/grants/route.js";
|
|
27
|
-
import { readEnv, readKv } from "@/lib/env";
|
|
28
|
-
// Arbitrary-tenant lookup lives behind the config-layer provider so this handler
|
|
29
|
-
// doesn't import the concrete resolver/static-map directly (tenancy-resolver-fence).
|
|
30
|
-
import { resolvePreviewTenant } from "@/config/previewTenantLookup";
|
|
31
|
-
import { createContentProvider } from "@/lib/storyblok/provider";
|
|
32
|
-
import { readPublishedArtifact, themeArtifactPath } from "@tot/public-runtime";
|
|
33
|
-
import {
|
|
34
|
-
DEV_DRAFT_COOKIE,
|
|
35
|
-
DEV_DRAFT_TTL_SECONDS,
|
|
36
|
-
devDraftKey,
|
|
37
|
-
signDevDraftToken,
|
|
38
|
-
verifyDevDraftToken,
|
|
39
|
-
} from "@/lib/dev/devDraft";
|
|
40
|
-
import type { ContentEditFile } from "@/lib/content-edit/types";
|
|
41
|
-
|
|
42
|
-
export const prerender = false;
|
|
43
|
-
|
|
44
|
-
export async function POST(context: APIContext): Promise<Response> {
|
|
45
|
-
const session = await readViewerSession({ cookies: context.cookies });
|
|
46
|
-
if (!session) return json({ error: "sign in required" }, 401);
|
|
47
|
-
if (!session.capability || session.capability === "none") {
|
|
48
|
-
return json({ error: "no developer capability on this account" }, 403);
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
let body: { prompt?: unknown };
|
|
52
|
-
try {
|
|
53
|
-
body = (await context.request.json()) as typeof body;
|
|
54
|
-
} catch {
|
|
55
|
-
return json({ error: "invalid JSON body" }, 400);
|
|
56
|
-
}
|
|
57
|
-
const prompt = typeof body.prompt === "string" ? body.prompt.trim() : "";
|
|
58
|
-
if (!prompt) return json({ error: "prompt is required" }, 400);
|
|
59
|
-
|
|
60
|
-
// The tenant is the session's OWN store (first membership, else the session's
|
|
61
|
-
// primary resource) — the same resolution /dev renders with. No caller-supplied
|
|
62
|
-
// tenant is ever honored here.
|
|
63
|
-
const wanted = (session.tenants?.[0]?.resource ?? session.resource ?? "").toLowerCase();
|
|
64
|
-
const tenant = wanted ? await resolvePreviewTenant(wanted) : null;
|
|
65
|
-
if (!tenant) return json({ error: "no store linked to this account" }, 403);
|
|
66
|
-
|
|
67
|
-
const kv = await readKv("TENANT_CACHE");
|
|
68
|
-
const kvGet = kv ? (key: string) => kv.get(key) : async () => null;
|
|
69
|
-
const secret = (await readEnv("CUSTOMIZATION_PREVIEW_SECRET")) ?? null;
|
|
70
|
-
const nowSeconds = Math.floor(Date.now() / 1000);
|
|
71
|
-
|
|
72
|
-
// Reuse the caller's existing draft (so successive edits stack) or start a new one.
|
|
73
|
-
const priorCookie = context.cookies.get(DEV_DRAFT_COOKIE)?.value ?? null;
|
|
74
|
-
const priorClaims =
|
|
75
|
-
priorCookie && secret ? await verifyDevDraftToken(secret, priorCookie, nowSeconds) : null;
|
|
76
|
-
const draftId =
|
|
77
|
-
priorClaims?.tenantId === tenant.tenant_id ? priorClaims.draftId : crypto.randomUUID();
|
|
78
|
-
|
|
79
|
-
// Current content = the developer's draft when present, else what actually serves
|
|
80
|
-
// (published overlay → build-time), so edits stack and always start from reality.
|
|
81
|
-
const provider = createContentProvider(tenant, kv ? { get: kvGet, env: "live" } : undefined);
|
|
82
|
-
const draftHome = await kvGet(devDraftKey(tenant.tenant_id, draftId, "content/home.html"));
|
|
83
|
-
const homeHtml = draftHome ?? (await provider.getHomeHtml());
|
|
84
|
-
const draftTheme = await kvGet(devDraftKey(tenant.tenant_id, draftId, "theme.json"));
|
|
85
|
-
const themeJson =
|
|
86
|
-
draftTheme ??
|
|
87
|
-
(await readPublishedArtifact(kvGet, tenant.tenant_id, "live", themeArtifactPath(tenant.tenant_id))) ??
|
|
88
|
-
JSON.stringify(tenant.theme_tokens ?? {}, null, 2);
|
|
89
|
-
|
|
90
|
-
// content_edit's allowlist: home document + theme. A block-based tenant with no raw
|
|
91
|
-
// home.html still gets theme edits (themeStyle drives its pages).
|
|
92
|
-
const files: ContentEditFile[] = [];
|
|
93
|
-
if (homeHtml) files.push({ path: "content/home.html", content: homeHtml });
|
|
94
|
-
files.push({ path: "theme.json", content: themeJson });
|
|
95
|
-
|
|
96
|
-
const client = await getContentEditClient();
|
|
97
|
-
if (client instanceof Response) return client;
|
|
98
|
-
|
|
99
|
-
let result;
|
|
100
|
-
try {
|
|
101
|
-
result = await client.editContent(prompt, files);
|
|
102
|
-
} catch (err) {
|
|
103
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
104
|
-
return json({ ok: false, error: message }, 502);
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
// Materialize the draft: re-apply each returned diff to the exact content we sent.
|
|
108
|
-
// Strict + fail-closed per file — a bad diff writes nothing for that file. Theme
|
|
109
|
-
// must still parse as JSON before it may land (a broken draft theme would degrade
|
|
110
|
-
// the whole preview).
|
|
111
|
-
const sent = new Map(files.map((f) => [f.path, f.content]));
|
|
112
|
-
const appliedPaths: string[] = [];
|
|
113
|
-
const canApply = !!kv && !!secret;
|
|
114
|
-
if (canApply) {
|
|
115
|
-
for (const d of result.diffs ?? []) {
|
|
116
|
-
const base = sent.get(d.path);
|
|
117
|
-
if (base == null) continue; // never write a path we didn't offer
|
|
118
|
-
let next: string;
|
|
119
|
-
try {
|
|
120
|
-
next = applyUnifiedDiff(base, d.diff);
|
|
121
|
-
} catch (err) {
|
|
122
|
-
if (err instanceof DiffApplyError) continue;
|
|
123
|
-
throw err;
|
|
124
|
-
}
|
|
125
|
-
if (next === base) continue;
|
|
126
|
-
if (d.path === "theme.json") {
|
|
127
|
-
try {
|
|
128
|
-
JSON.parse(next);
|
|
129
|
-
} catch {
|
|
130
|
-
continue;
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
await kv!.put(devDraftKey(tenant.tenant_id, draftId, d.path), next, {
|
|
134
|
-
expirationTtl: DEV_DRAFT_TTL_SECONDS,
|
|
135
|
-
});
|
|
136
|
-
appliedPaths.push(d.path);
|
|
137
|
-
}
|
|
138
|
-
if (appliedPaths.length > 0) {
|
|
139
|
-
// (Re)issue the signed draft cookie so the middleware overlay serves this draft.
|
|
140
|
-
const token = await signDevDraftToken(
|
|
141
|
-
secret!,
|
|
142
|
-
tenant.tenant_id,
|
|
143
|
-
draftId,
|
|
144
|
-
DEV_DRAFT_TTL_SECONDS,
|
|
145
|
-
nowSeconds,
|
|
146
|
-
);
|
|
147
|
-
context.cookies.set(DEV_DRAFT_COOKIE, token, {
|
|
148
|
-
httpOnly: true,
|
|
149
|
-
secure: true,
|
|
150
|
-
sameSite: "lax",
|
|
151
|
-
path: "/",
|
|
152
|
-
maxAge: DEV_DRAFT_TTL_SECONDS,
|
|
153
|
-
});
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
return json({
|
|
158
|
-
ok: true,
|
|
159
|
-
...result,
|
|
160
|
-
// True when the draft landed and the live preview will show it on reload.
|
|
161
|
-
applied: appliedPaths.length > 0,
|
|
162
|
-
appliedPaths,
|
|
163
|
-
});
|
|
164
|
-
}
|