@web-my-money/studio-consumer 2.2.0 → 2.3.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 +19 -1
- package/bin/studio-consumer.mjs +29 -0
- package/cli/claude-step.mjs +48 -0
- package/cli/init.mjs +439 -0
- package/cli/preflight.mjs +78 -0
- package/cli/provisioner.mjs +102 -0
- package/cli/run.mjs +89 -0
- package/cli/state.mjs +37 -0
- package/cli/studio-api.mjs +127 -0
- package/cli/templates.mjs +134 -0
- package/cli/ui.mjs +126 -0
- package/cli/writers.mjs +148 -0
- package/package.json +33 -29
- package/skills/onboard-site/SKILL.md +96 -0
- package/src/analytics/index.ts +37 -37
- package/src/attribution/index.ts +14 -14
- package/src/brand/index.ts +66 -66
- package/src/content/index.ts +27 -27
- package/src/content/manifest-handler.ts +11 -9
- package/src/image/index.ts +14 -14
- package/src/next/index.d.mts +13 -0
- package/src/next/index.mjs +103 -0
package/src/image/index.ts
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
|
-
/** Pure; safe in client components. */
|
|
2
|
-
export const PACKAGE_VERSION = "2.
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* CSS `object-position` for a Studio image value's focal point (spec §6.2).
|
|
6
|
-
* Anything missing or out of range reads as the centre — the browser default —
|
|
7
|
-
* so an image without a focal point renders exactly as before.
|
|
8
|
-
*/
|
|
9
|
-
export function focalObjectPosition(value: unknown): string {
|
|
10
|
-
const f = (value as { focal?: { x?: unknown; y?: unknown } } | null | undefined)?.focal;
|
|
11
|
-
const ok = (n: unknown): n is number => typeof n === "number" && n >= 0 && n <= 1;
|
|
12
|
-
if (!f || !ok(f.x) || !ok(f.y)) return "50% 50%";
|
|
13
|
-
return `${Math.round(f.x * 100)}% ${Math.round(f.y * 100)}%`;
|
|
14
|
-
}
|
|
1
|
+
/** Pure; safe in client components. */
|
|
2
|
+
export const PACKAGE_VERSION = "2.3.0";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* CSS `object-position` for a Studio image value's focal point (spec §6.2).
|
|
6
|
+
* Anything missing or out of range reads as the centre — the browser default —
|
|
7
|
+
* so an image without a focal point renders exactly as before.
|
|
8
|
+
*/
|
|
9
|
+
export function focalObjectPosition(value: unknown): string {
|
|
10
|
+
const f = (value as { focal?: { x?: unknown; y?: unknown } } | null | undefined)?.focal;
|
|
11
|
+
const ok = (n: unknown): n is number => typeof n === "number" && n >= 0 && n <= 1;
|
|
12
|
+
if (!f || !ok(f.x) || !ok(f.y)) return "50% 50%";
|
|
13
|
+
return `${Math.round(f.x * 100)}% ${Math.round(f.y * 100)}%`;
|
|
14
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// Hand-maintained alongside index.mjs.
|
|
2
|
+
|
|
3
|
+
type HeaderRule = { source: string; headers: { key: string; value: string }[] };
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Wrap a site's next.config so Studio can edit it: transpiles this package,
|
|
7
|
+
* sets `frame-ancestors 'self' <studio>` (spliced into an existing CSP), and
|
|
8
|
+
* refuses X-Frame-Options. Wrapping twice is the same as once.
|
|
9
|
+
*/
|
|
10
|
+
export declare function withStudio<T extends object>(
|
|
11
|
+
config: T,
|
|
12
|
+
opts?: { studioOrigins?: string[] },
|
|
13
|
+
): T & { transpilePackages: string[]; headers: () => Promise<HeaderRule[]> };
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
import { studioFrameAncestors } from "../content/headers.mjs";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* `withStudio(nextConfig)` — the one next.config edit the onboarding command
|
|
6
|
+
* makes (spec 2026-10-09 §3.3). Plain ESM for the same reason as headers.mjs:
|
|
7
|
+
* Next loads next.config through native `import()`, which cannot reach the
|
|
8
|
+
* package's `.ts` entries.
|
|
9
|
+
*
|
|
10
|
+
* - transpiles this package (it ships TypeScript source);
|
|
11
|
+
* - lets Studio frame the site's pages for click-to-edit, and nothing else:
|
|
12
|
+
* `frame-ancestors 'self' <studio>` on every page. An existing
|
|
13
|
+
* Content-Security-Policy is spliced into, never duplicated: browsers apply
|
|
14
|
+
* the strictest of several CSP headers, so a second one would silently win
|
|
15
|
+
* or lose;
|
|
16
|
+
* - refuses X-Frame-Options, which has no allowlist and would block Studio.
|
|
17
|
+
*
|
|
18
|
+
* Wrapping twice is the same as wrapping once.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
const PACKAGE = "@web-my-money/studio-consumer";
|
|
22
|
+
const DEFAULT_ORIGINS = ["https://studio.webmymoney.com"];
|
|
23
|
+
const WRAPPED = Symbol.for("wmm.withStudio");
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* @typedef {{ key: string, value: string }} HeaderEntry
|
|
27
|
+
* @typedef {{ source: string, headers: HeaderEntry[] } & Record<string, unknown>} HeaderRule
|
|
28
|
+
* @typedef {{ transpilePackages?: string[], headers?: () => Promise<HeaderRule[]> } & Record<string, unknown>} NextLikeConfig
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* @param {string} csp
|
|
33
|
+
* @param {string} directive
|
|
34
|
+
*/
|
|
35
|
+
function spliceFrameAncestors(csp, directive) {
|
|
36
|
+
const parts = csp
|
|
37
|
+
.split(";")
|
|
38
|
+
.map((p) => p.trim())
|
|
39
|
+
.filter(Boolean);
|
|
40
|
+
const at = parts.findIndex((p) => /^frame-ancestors(\s|$)/i.test(p));
|
|
41
|
+
if (at === -1) parts.push(directive);
|
|
42
|
+
else parts[at] = directive;
|
|
43
|
+
return parts.join("; ");
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* @template {NextLikeConfig} T
|
|
48
|
+
* @param {T} config
|
|
49
|
+
* @param {{ studioOrigins?: string[] }} [opts]
|
|
50
|
+
* @returns {T & { transpilePackages: string[], headers: () => Promise<HeaderRule[]> }}
|
|
51
|
+
*/
|
|
52
|
+
export function withStudio(config, opts = {}) {
|
|
53
|
+
if (/** @type {Record<symbol, unknown>} */ (/** @type {unknown} */ (config))[WRAPPED]) {
|
|
54
|
+
return /** @type {T & { transpilePackages: string[], headers: () => Promise<HeaderRule[]> }} */ (config);
|
|
55
|
+
}
|
|
56
|
+
// Validates the origins now (wildcards, paths), so a bad list fails the build.
|
|
57
|
+
const directive = studioFrameAncestors(opts.studioOrigins ?? DEFAULT_ORIGINS).value;
|
|
58
|
+
const original = config.headers;
|
|
59
|
+
|
|
60
|
+
const transpilePackages = [...(config.transpilePackages ?? [])];
|
|
61
|
+
if (!transpilePackages.includes(PACKAGE)) transpilePackages.push(PACKAGE);
|
|
62
|
+
|
|
63
|
+
/** @returns {Promise<HeaderRule[]>} */
|
|
64
|
+
async function headers() {
|
|
65
|
+
const rules = original ? [...(await original())] : [];
|
|
66
|
+
for (const rule of rules) {
|
|
67
|
+
for (const h of rule.headers ?? []) {
|
|
68
|
+
if (h.key.toLowerCase() === "x-frame-options") {
|
|
69
|
+
throw new Error(
|
|
70
|
+
`withStudio: remove X-Frame-Options from the "${rule.source}" rule. It cannot allow Studio to frame the page; withStudio sets frame-ancestors instead.`,
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
const out = rules.map((rule) => ({
|
|
76
|
+
...rule,
|
|
77
|
+
headers: (rule.headers ?? []).map((h) => {
|
|
78
|
+
if (h.key.toLowerCase() !== "content-security-policy") return h;
|
|
79
|
+
return { key: h.key, value: spliceFrameAncestors(h.value, directive) };
|
|
80
|
+
}),
|
|
81
|
+
}));
|
|
82
|
+
// Every page must carry the directive. A CSP on a narrow route, or on a
|
|
83
|
+
// /:path* rule that only applies sometimes (has/missing), does not cover
|
|
84
|
+
// the rest, so an unconditional /:path* rule gets one too.
|
|
85
|
+
const unconditional = (/** @type {HeaderRule} */ r) => r.source === "/:path*" && !("has" in r) && !("missing" in r);
|
|
86
|
+
const covered = out.some(
|
|
87
|
+
(r) => unconditional(r) && r.headers.some((h) => h.key.toLowerCase() === "content-security-policy"),
|
|
88
|
+
);
|
|
89
|
+
if (!covered) {
|
|
90
|
+
const all = out.find(unconditional);
|
|
91
|
+
if (all) all.headers = [...all.headers, { key: "Content-Security-Policy", value: directive }];
|
|
92
|
+
else out.push({ source: "/:path*", headers: [{ key: "Content-Security-Policy", value: directive }] });
|
|
93
|
+
}
|
|
94
|
+
return out;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const wrapped = { ...config, transpilePackages, headers };
|
|
98
|
+
// Non-enumerable: Next validates config keys and would warn about a marker.
|
|
99
|
+
Object.defineProperty(wrapped, WRAPPED, { value: true, enumerable: false });
|
|
100
|
+
return /** @type {T & { transpilePackages: string[], headers: () => Promise<HeaderRule[]> }} */ (
|
|
101
|
+
/** @type {unknown} */ (wrapped)
|
|
102
|
+
);
|
|
103
|
+
}
|