@tokenoftrust/storefront-runner 1.3.2 → 1.3.4-rc.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/apps/storefront/astro.config.mjs +59 -0
- package/apps/storefront/env.d.ts +24 -8
- package/apps/storefront/package.json +1 -1
- package/apps/storefront/src/components/admin/{AdminFoxyCartSettingsPanel.astro → AdminCheckoutSettingsPanel.astro} +50 -51
- package/apps/storefront/src/components/admin/AdminSettingsTab.astro +2 -2
- package/apps/storefront/src/components/admin/AdminSubscriptionsTab.astro +613 -0
- package/apps/storefront/src/components/admin/orders/OrdersWorkspace.astro +3 -3
- package/apps/storefront/src/components/chrome/SiteHeader.astro +3 -4
- package/apps/storefront/src/components/commerce/{FoxySidecartStyles.astro → CartDrawerStyles.astro} +13 -14
- package/apps/storefront/src/components/commerce/CheckoutLoader.astro +21 -0
- package/apps/storefront/src/components/islands/VariantSelector.tsx +101 -20
- package/apps/storefront/src/config/adminTargetTenant.ts +2 -2
- package/apps/storefront/src/config/devTenantSeed.ts +65 -16
- package/apps/storefront/src/config/resolver.ts +48 -1
- package/apps/storefront/src/layouts/Layout.astro +14 -12
- package/apps/storefront/src/lib/admin/README.md +2 -2
- package/apps/storefront/src/lib/admin/checkoutStyleInput.ts +5 -5
- package/apps/storefront/src/lib/admin/orderClient.ts +20 -20
- package/apps/storefront/src/lib/admin/services/mock/{foxyCartSettings.ts → checkoutSettings.ts} +21 -22
- package/apps/storefront/src/lib/admin/services/mock/index.ts +3 -2
- package/apps/storefront/src/lib/admin/services/mock/settings.ts +1 -1
- package/apps/storefront/src/lib/admin/services/mock/subscriptions.ts +398 -0
- package/apps/storefront/src/lib/admin/services/orders.ts +21 -21
- package/apps/storefront/src/lib/admin/viewmodels/subscriptions.ts +106 -0
- package/apps/storefront/src/lib/auth/loginGate.ts +14 -3
- package/apps/storefront/src/lib/{foxyCommerce.ts → checkoutCommerce.ts} +20 -18
- package/apps/storefront/src/lib/commerce/{foxyCartSettings.ts → checkoutSettings.ts} +65 -66
- package/apps/storefront/src/lib/dev/activityIngestToken.ts +9 -0
- package/apps/storefront/src/lib/dev/activityStore.ts +12 -0
- package/apps/storefront/src/lib/dev/cliSignInCode.ts +23 -4
- package/apps/storefront/src/lib/dev/runtimeStore.ts +193 -22
- package/apps/storefront/src/lib/edgeCache.ts +7 -6
- package/apps/storefront/src/lib/health.ts +53 -0
- package/apps/storefront/src/lib/privacy/emailHint.ts +33 -0
- package/apps/storefront/src/lib/rawMarketingHtml.ts +1 -0
- package/apps/storefront/src/lib/sections.ts +19 -23
- package/apps/storefront/src/lib/securityHeaders.ts +1 -1
- package/apps/storefront/src/lib/storyblok/content-model.ts +3 -2
- package/apps/storefront/src/middleware/index.ts +29 -12
- package/apps/storefront/src/pages/[...slug].astro +5 -6
- package/apps/storefront/src/pages/admin.astro +10 -3
- package/apps/storefront/src/pages/api/admin/orders/[id].ts +4 -4
- package/apps/storefront/src/pages/api/admin/orders.ts +5 -5
- package/apps/storefront/src/pages/api/auth/magic-exchange.ts +14 -7
- package/apps/storefront/src/pages/api/checkout-style.css.ts +5 -5
- package/apps/storefront/src/pages/auth/login.astro +4 -1
- package/apps/storefront/src/pages/auth/magic.astro +263 -43
- package/apps/storefront/src/pages/capabilities.astro +1 -1
- package/apps/storefront/src/pages/cockpit.astro +2365 -0
- package/apps/storefront/src/pages/dashboard/[appDomain]/team.astro +19 -0
- package/apps/storefront/src/pages/index.astro +23 -29
- package/apps/storefront/src/pages/products/[handle].astro +50 -20
- package/package.json +6 -2
- package/packages/public-runtime/src/capabilities.ts +97 -9
- package/packages/public-runtime/src/{foxy-orders.ts → checkout-orders.ts} +28 -27
- package/packages/public-runtime/src/checkout-style-publish.ts +25 -25
- package/packages/public-runtime/src/checkout-style.ts +13 -13
- package/packages/public-runtime/src/{foxy.ts → checkout.ts} +111 -34
- package/packages/public-runtime/src/csp.ts +11 -11
- package/packages/public-runtime/src/customization-scripts.ts +28 -0
- package/packages/public-runtime/src/extension-contract.ts +73 -0
- package/packages/public-runtime/src/index.ts +4 -2
- package/packages/public-runtime/src/product.ts +57 -0
- package/packages/public-runtime/src/raw-html-policy.ts +35 -0
- package/packages/public-runtime/src/tenant.ts +14 -11
- package/pnpm-workspace.yaml +1 -0
- package/scripts/dev/checkout-watch.mjs +9 -0
- package/scripts/dev/file-browser.mjs +5 -1
- package/scripts/dev/transient-files.mjs +34 -0
- package/scripts/tot-dev.mjs +16 -3
- package/apps/storefront/src/components/commerce/FoxyLoader.astro +0 -23
- package/apps/storefront/src/pages/api/dev/activity.ts +0 -124
- package/apps/storefront/src/pages/api/dev/cli-signin-code.ts +0 -73
- package/apps/storefront/src/pages/api/dev/diff.ts +0 -68
- package/apps/storefront/src/pages/api/dev/file.ts +0 -57
- package/apps/storefront/src/pages/api/dev/locate.ts +0 -125
- package/apps/storefront/src/pages/api/dev/publish.ts +0 -128
- package/apps/storefront/src/pages/api/dev/tree.ts +0 -70
- package/apps/storefront/src/pages/api/dev/write.ts +0 -116
|
@@ -1,128 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* POST /api/dev/publish — Phase 3 (cp-publish) of the hosted /dev cockpit
|
|
3
|
-
* (workstream tot-lime-cloud-dev-cockpit). Publishes the developer's current
|
|
4
|
-
* draft content for one file into the tenant's TEST channel — the moment the
|
|
5
|
-
* private draft overlay (cp-write) becomes something OTHER viewers can see
|
|
6
|
-
* (still not live).
|
|
7
|
-
*
|
|
8
|
-
* SCOPE (see decision cp-publish-scope-correction): the card's literal plan
|
|
9
|
-
* ("wire to api/preview/promote.ts") doesn't fit this surface — that route
|
|
10
|
-
* needs a server-to-server bearer secret + a CDP-driven compliance
|
|
11
|
-
* GateReport, and operates on the Gitea-fed preview/live channel, not the
|
|
12
|
-
* dev-draft: overlay. Instead this uses the lower-level primitive the
|
|
13
|
-
* reconcile pipeline itself is built on — `publishToTest`
|
|
14
|
-
* (private-controlplane/customization-versioning.ts) over `KVVersionStore` —
|
|
15
|
-
* writing straight into the SAME published-artifact store
|
|
16
|
-
* `readPublishedArtifact`/`PublishedContentProvider` read. Content-addressed
|
|
17
|
-
* + idempotent by construction (re-publishing identical bytes is a no-op).
|
|
18
|
-
*
|
|
19
|
-
* Deliberately test-only: promoting test -> live is the real external-publish
|
|
20
|
-
* action and isn't reachable from this surface (see decision + the dispatched
|
|
21
|
-
* cp-publish-go-live follow-up). "Publish" here closes write -> preview ->
|
|
22
|
-
* diff -> publish-to-test, honestly, without faking or bypassing the
|
|
23
|
-
* existing compliance gate.
|
|
24
|
-
*/
|
|
25
|
-
import type { APIContext } from "astro";
|
|
26
|
-
import { json } from "@/lib/grants/route.js";
|
|
27
|
-
import { readKv } from "@/lib/env";
|
|
28
|
-
import {
|
|
29
|
-
publishToTest,
|
|
30
|
-
CustomizationError,
|
|
31
|
-
KVVersionStore,
|
|
32
|
-
type CustomizationManifest,
|
|
33
|
-
} from "@tot/private-controlplane";
|
|
34
|
-
import { contentArtifactPath, themeArtifactPath } from "@tot/public-runtime";
|
|
35
|
-
import {
|
|
36
|
-
resolveCockpitContext,
|
|
37
|
-
isCockpitContext,
|
|
38
|
-
isWritablePath,
|
|
39
|
-
pageSlugForPath,
|
|
40
|
-
resolveDraftState,
|
|
41
|
-
} from "@/lib/dev/hostedCockpit";
|
|
42
|
-
|
|
43
|
-
export const prerender = false;
|
|
44
|
-
|
|
45
|
-
// The developer's editable raw-HTML/theme surface, in ARTIFACT-path shape
|
|
46
|
-
// (tenants/<id>/...) — what publishToTest's isEditable check and the
|
|
47
|
-
// published-artifact store both key on. Not the tenant's real
|
|
48
|
-
// declarative-customization manifest (out of scope here): this is
|
|
49
|
-
// defense-in-depth on top of the isWritablePath gate already enforced below.
|
|
50
|
-
const PUBLISH_MANIFEST: CustomizationManifest = {
|
|
51
|
-
version: 1,
|
|
52
|
-
classes: {
|
|
53
|
-
"platform-owned": [],
|
|
54
|
-
"extension-point": [],
|
|
55
|
-
"tenant-owned": ["tenants/<id>/content/home.html", "tenants/<id>/content/pages-html/**", "tenants/<id>/theme.json"],
|
|
56
|
-
},
|
|
57
|
-
immutableCompliance: [],
|
|
58
|
-
};
|
|
59
|
-
|
|
60
|
-
/**
|
|
61
|
-
* Map this route's short content path (content/home.html, theme.json, ...)
|
|
62
|
-
* to the artifact path the published-artifact store actually keys on
|
|
63
|
-
* (contentArtifactPath/themeArtifactPath — the SAME mapping
|
|
64
|
-
* PublishedContentProvider's getHomeHtml/getPageHtml use internally via
|
|
65
|
-
* `publishedRaw`). Only ever called after isWritablePath(path) passed, so
|
|
66
|
-
* every branch here has a defined result.
|
|
67
|
-
*/
|
|
68
|
-
function artifactPathFor(tenantId: string, path: string): string {
|
|
69
|
-
if (path === "theme.json") return themeArtifactPath(tenantId);
|
|
70
|
-
if (path === "content/home.html") return contentArtifactPath(tenantId, "home.html");
|
|
71
|
-
return contentArtifactPath(tenantId, `pages-html/${pageSlugForPath(path)}.html`);
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
interface PublishBody {
|
|
75
|
-
path?: unknown;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
export async function POST(context: APIContext): Promise<Response> {
|
|
79
|
-
const ctx = await resolveCockpitContext(context);
|
|
80
|
-
if (!isCockpitContext(ctx)) return ctx;
|
|
81
|
-
|
|
82
|
-
let body: PublishBody;
|
|
83
|
-
try {
|
|
84
|
-
body = (await context.request.json()) as PublishBody;
|
|
85
|
-
} catch {
|
|
86
|
-
return json({ ok: false, status: "bad_content", message: "invalid JSON body" }, 400);
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
const path = typeof body.path === "string" ? body.path : "";
|
|
90
|
-
if (!path) return json({ ok: false, status: "no_path", message: "path is required" }, 400);
|
|
91
|
-
if (!isWritablePath(path)) {
|
|
92
|
-
return json({ ok: false, status: "not_writable", message: `not in the write allowlist: ${path}` }, 403);
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
const { draftContent } = await resolveDraftState(context, ctx, path);
|
|
96
|
-
if (draftContent === null) {
|
|
97
|
-
return json(
|
|
98
|
-
{ ok: false, status: "no_draft", message: "You have no unpublished edit for this file — nothing to publish." },
|
|
99
|
-
409,
|
|
100
|
-
);
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
const kv = await readKv("TENANT_CACHE");
|
|
104
|
-
if (!kv) {
|
|
105
|
-
// No KV bound (e.g. bare local build) — accept but report unpersisted,
|
|
106
|
-
// matching cp-write's degraded-mode contract rather than a broken call.
|
|
107
|
-
return json({ ok: true, status: "ok", path, applied: false });
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
const store = new KVVersionStore(kv);
|
|
111
|
-
const artifactPath = artifactPathFor(ctx.tenant.tenant_id, path);
|
|
112
|
-
try {
|
|
113
|
-
const result = await publishToTest(store, PUBLISH_MANIFEST, ctx.tenant.tenant_id, artifactPath, draftContent);
|
|
114
|
-
return json({
|
|
115
|
-
ok: true,
|
|
116
|
-
status: "ok",
|
|
117
|
-
path,
|
|
118
|
-
applied: true,
|
|
119
|
-
versionId: result.artifact.versionId,
|
|
120
|
-
channel: "test",
|
|
121
|
-
});
|
|
122
|
-
} catch (err) {
|
|
123
|
-
if (err instanceof CustomizationError) {
|
|
124
|
-
return json({ ok: false, status: err.code, message: err.message }, 409);
|
|
125
|
-
}
|
|
126
|
-
throw err;
|
|
127
|
-
}
|
|
128
|
-
}
|
|
@@ -1,70 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* GET /api/dev/tree — Phase 1 (cp-tree) of the hosted /dev cockpit
|
|
3
|
-
* (workstream tot-lime-cloud-dev-cockpit, decision
|
|
4
|
-
* architecture-environment-aware-cockpit). Lists the signed-in developer's OWN
|
|
5
|
-
* tenant content so the hosted cockpit's Files tab renders the same tree shape
|
|
6
|
-
* the local loop's `/__tot/tree` does (see scripts/dev/file-browser.mjs
|
|
7
|
-
* buildTree/handleTree — this route mirrors its {ok,tree} response contract,
|
|
8
|
-
* consumed unchanged by dev.astro's filesScript via the cp-env apiBase resolver).
|
|
9
|
-
*
|
|
10
|
-
* Unlike the local tree (an arbitrary on-disk directory walk), the hosted
|
|
11
|
-
* Worker has no filesystem — the tree here is SYNTHESIZED from exactly the
|
|
12
|
-
* raw-HTML surface the content provider chain understands + the write
|
|
13
|
-
* allowlist (scripts/dev/safe-path.mjs WRITE_ALLOWLIST): content/home.html,
|
|
14
|
-
* content/pages-html/<slug>.html, and theme.json. A tenant on structured
|
|
15
|
-
* (Storyblok/JSON) content simply has an emptier tree — nothing to browse/edit
|
|
16
|
-
* on this raw-HTML surface yet.
|
|
17
|
-
*/
|
|
18
|
-
import type { APIContext } from "astro";
|
|
19
|
-
import { json } from "@/lib/grants/route.js";
|
|
20
|
-
import { readPublishedArtifact, themeArtifactPath } from "@tot/public-runtime";
|
|
21
|
-
import { resolveCockpitContext, isCockpitContext } from "@/lib/dev/hostedCockpit";
|
|
22
|
-
|
|
23
|
-
export const prerender = false;
|
|
24
|
-
|
|
25
|
-
type TreeNode =
|
|
26
|
-
| { name: string; path: string; type: "file"; language: string | null }
|
|
27
|
-
| { name: string; path: string; type: "dir"; children: TreeNode[] };
|
|
28
|
-
|
|
29
|
-
export async function GET(context: APIContext): Promise<Response> {
|
|
30
|
-
const ctx = await resolveCockpitContext(context);
|
|
31
|
-
if (!isCockpitContext(ctx)) return ctx;
|
|
32
|
-
const { tenant, provider, kvGet } = ctx;
|
|
33
|
-
|
|
34
|
-
const [homeHtml, pageSlugs, themeRaw] = await Promise.all([
|
|
35
|
-
provider.getHomeHtml(),
|
|
36
|
-
provider.listPageHtmlSlugs(),
|
|
37
|
-
readPublishedArtifact(kvGet, tenant.tenant_id, "live", themeArtifactPath(tenant.tenant_id)),
|
|
38
|
-
]);
|
|
39
|
-
|
|
40
|
-
const contentChildren: TreeNode[] = [];
|
|
41
|
-
if (homeHtml !== null) {
|
|
42
|
-
contentChildren.push({ name: "home.html", path: "content/home.html", type: "file", language: "html" });
|
|
43
|
-
}
|
|
44
|
-
if (pageSlugs.length > 0) {
|
|
45
|
-
contentChildren.push({
|
|
46
|
-
name: "pages-html",
|
|
47
|
-
path: "content/pages-html",
|
|
48
|
-
type: "dir",
|
|
49
|
-
children: pageSlugs
|
|
50
|
-
.slice()
|
|
51
|
-
.sort((a, b) => a.localeCompare(b))
|
|
52
|
-
.map((slug) => ({
|
|
53
|
-
name: `${slug}.html`,
|
|
54
|
-
path: `content/pages-html/${slug}.html`,
|
|
55
|
-
type: "file" as const,
|
|
56
|
-
language: "html",
|
|
57
|
-
})),
|
|
58
|
-
});
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
const tree: TreeNode[] = [];
|
|
62
|
-
if (contentChildren.length > 0) {
|
|
63
|
-
tree.push({ name: "content", path: "content", type: "dir", children: contentChildren });
|
|
64
|
-
}
|
|
65
|
-
if (themeRaw !== null) {
|
|
66
|
-
tree.push({ name: "theme.json", path: "theme.json", type: "file", language: "json" });
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
return json({ ok: true, tenantId: tenant.tenant_id, tree });
|
|
70
|
-
}
|
|
@@ -1,116 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* POST /api/dev/write — Phase 2 (cp-write) of the hosted /dev cockpit
|
|
3
|
-
* (workstream tot-lime-cloud-dev-cockpit, decision
|
|
4
|
-
* architecture-environment-aware-cockpit). The heart of cloud editing: a
|
|
5
|
-
* direct text edit (not an AI prompt) written into the developer's PRIVATE
|
|
6
|
-
* draft overlay, reusing /api/content-edit/ai-edit.ts's exact materialization
|
|
7
|
-
* mechanism (dev-draft: KV keys + signed tot_dev_draft cookie the middleware
|
|
8
|
-
* overlay reads) — so the /dev live preview reflects the edit and no other
|
|
9
|
-
* viewer/channel is touched. Same request/response contract as the local
|
|
10
|
-
* loop's `/__tot/write` (scripts/dev/file-writer.mjs handleWrite), including
|
|
11
|
-
* the C6 baseVersion 409 conflict guard — but "version" here is
|
|
12
|
-
* contentVersion() (a content hash), not a filesystem mtime, since the Worker
|
|
13
|
-
* has none; cp-file hands back the same hash so the two agree.
|
|
14
|
-
*
|
|
15
|
-
* Deviates from file-writer.mjs in one place: a theme.json write must parse as
|
|
16
|
-
* JSON before it lands (mirrors ai-edit.ts's guard) — this draft overlay feeds
|
|
17
|
-
* the SAME live preview theme.json ai-edit writes, and a malformed draft would
|
|
18
|
-
* degrade that preview for the developer, unlike a plain on-disk write.
|
|
19
|
-
*
|
|
20
|
-
* When KV or the signing secret is unavailable (e.g. no CUSTOMIZATION_PREVIEW_SECRET
|
|
21
|
-
* configured for this env), the edit is accepted but not persisted —
|
|
22
|
-
* `applied:false`, exactly ai-edit's degraded-mode contract — never a broken call.
|
|
23
|
-
*/
|
|
24
|
-
import type { APIContext } from "astro";
|
|
25
|
-
import { json } from "@/lib/grants/route.js";
|
|
26
|
-
import { DEV_DRAFT_TTL_SECONDS, devDraftKey } from "@/lib/dev/devDraft";
|
|
27
|
-
import {
|
|
28
|
-
resolveCockpitContext,
|
|
29
|
-
isCockpitContext,
|
|
30
|
-
isWritablePath,
|
|
31
|
-
readPublishedPath,
|
|
32
|
-
resolveDraftState,
|
|
33
|
-
contentVersion,
|
|
34
|
-
setDraftCookie,
|
|
35
|
-
} from "@/lib/dev/hostedCockpit";
|
|
36
|
-
|
|
37
|
-
export const prerender = false;
|
|
38
|
-
|
|
39
|
-
const MAX_WRITE_BYTES = 2 * 1024 * 1024;
|
|
40
|
-
|
|
41
|
-
interface WriteBody {
|
|
42
|
-
path?: unknown;
|
|
43
|
-
content?: unknown;
|
|
44
|
-
baseVersion?: unknown;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
export async function POST(context: APIContext): Promise<Response> {
|
|
48
|
-
const ctx = await resolveCockpitContext(context);
|
|
49
|
-
if (!isCockpitContext(ctx)) return ctx;
|
|
50
|
-
|
|
51
|
-
let body: WriteBody;
|
|
52
|
-
try {
|
|
53
|
-
body = (await context.request.json()) as WriteBody;
|
|
54
|
-
} catch {
|
|
55
|
-
return json({ ok: false, status: "bad_content", message: "invalid JSON body" }, 400);
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
const path = typeof body.path === "string" ? body.path : "";
|
|
59
|
-
if (!path) return json({ ok: false, status: "no_path", message: "path is required" }, 400);
|
|
60
|
-
if (!isWritablePath(path)) {
|
|
61
|
-
return json({ ok: false, status: "not_writable", message: `not in the write allowlist: ${path}` }, 403);
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
const content = body.content;
|
|
65
|
-
if (typeof content !== "string") {
|
|
66
|
-
return json({ ok: false, status: "bad_content", message: "content must be a string" }, 400);
|
|
67
|
-
}
|
|
68
|
-
if (new TextEncoder().encode(content).length > MAX_WRITE_BYTES) {
|
|
69
|
-
return json({ ok: false, status: "too_large", message: "content exceeds the write size limit" }, 413);
|
|
70
|
-
}
|
|
71
|
-
if (path === "theme.json") {
|
|
72
|
-
try {
|
|
73
|
-
JSON.parse(content);
|
|
74
|
-
} catch {
|
|
75
|
-
return json({ ok: false, status: "bad_content", message: "theme.json must be valid JSON" }, 400);
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
// Stale-write guard (C6): compare the caller's baseVersion against the
|
|
80
|
-
// developer's CURRENT value for this path (their draft if one exists, else
|
|
81
|
-
// published) — omit baseVersion to force-save, exactly the local contract.
|
|
82
|
-
const { draftId, draftContent } = await resolveDraftState(context, ctx, path);
|
|
83
|
-
const currentContent = draftContent !== null ? draftContent : await readPublishedPath(ctx, path);
|
|
84
|
-
const currentVersion = contentVersion(currentContent);
|
|
85
|
-
const baseVersion = typeof body.baseVersion === "number" ? body.baseVersion : undefined;
|
|
86
|
-
if (baseVersion !== undefined && currentVersion !== baseVersion) {
|
|
87
|
-
return json(
|
|
88
|
-
{
|
|
89
|
-
ok: false,
|
|
90
|
-
status: "conflict",
|
|
91
|
-
current: currentVersion,
|
|
92
|
-
message:
|
|
93
|
-
currentVersion === null
|
|
94
|
-
? `${path} was deleted since you opened it`
|
|
95
|
-
: `${path} changed since you opened it`,
|
|
96
|
-
},
|
|
97
|
-
409,
|
|
98
|
-
);
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
const bytes = new TextEncoder().encode(content).length;
|
|
102
|
-
const mtimeMs = contentVersion(content);
|
|
103
|
-
|
|
104
|
-
if (!ctx.kvPut || !ctx.draftSecret) {
|
|
105
|
-
// Nothing to persist against (e.g. bare local build with no KV/secret) —
|
|
106
|
-
// report success without pretending the draft landed, like ai-edit.
|
|
107
|
-
return json({ ok: true, status: "ok", path, bytes, mtimeMs, applied: false });
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
await ctx.kvPut(devDraftKey(ctx.tenant.tenant_id, draftId, path), content, {
|
|
111
|
-
expirationTtl: DEV_DRAFT_TTL_SECONDS,
|
|
112
|
-
});
|
|
113
|
-
await setDraftCookie(context, ctx, draftId);
|
|
114
|
-
|
|
115
|
-
return json({ ok: true, status: "ok", path, bytes, mtimeMs, applied: true });
|
|
116
|
-
}
|