@decocms/blocks 7.41.1 → 7.43.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/package.json +1 -1
- package/src/cms/index.ts +1 -1
- package/src/cms/renderJson.test.ts +39 -1
- package/src/cms/renderJson.ts +30 -0
package/package.json
CHANGED
package/src/cms/index.ts
CHANGED
|
@@ -77,7 +77,7 @@ export type {
|
|
|
77
77
|
SerializedSection,
|
|
78
78
|
SerializeOptions,
|
|
79
79
|
} from "./renderJson";
|
|
80
|
-
export { serializeRenderJson } from "./renderJson";
|
|
80
|
+
export { isSecretValue, serializeRenderJson, stringifyWithoutSecrets } from "./renderJson";
|
|
81
81
|
export type {
|
|
82
82
|
AsyncRenderingConfig,
|
|
83
83
|
CommerceLoader,
|
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
import { describe, expect, it } from "vitest";
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
isSecretValue,
|
|
4
|
+
type RenderJsonModule,
|
|
5
|
+
serializeRenderJson,
|
|
6
|
+
stringifyWithoutSecrets,
|
|
7
|
+
} from "./renderJson";
|
|
3
8
|
|
|
4
9
|
const modules: Record<string, RenderJsonModule> = {
|
|
5
10
|
"site/sections/Product/ProductDetails.tsx": {
|
|
@@ -127,3 +132,36 @@ describe("serializeRenderJson", () => {
|
|
|
127
132
|
expect(out).toEqual([{ component: "site/sections/Header.tsx", props: {} }]);
|
|
128
133
|
});
|
|
129
134
|
});
|
|
135
|
+
|
|
136
|
+
describe("isSecretValue / stringifyWithoutSecrets", () => {
|
|
137
|
+
it("detects a resolved Secret ({ get: fn }) and an unresolved secret block", () => {
|
|
138
|
+
expect(isSecretValue({ get: () => "plaintext" })).toBe(true);
|
|
139
|
+
expect(isSecretValue({ __resolveType: "website/loaders/secret.ts", encrypted: "x" })).toBe(true);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it("does not flag content that merely has a `get` method or other shapes", () => {
|
|
143
|
+
expect(isSecretValue({ get: () => 1, other: true })).toBe(false); // extra key
|
|
144
|
+
expect(isSecretValue({ token: "abc" })).toBe(false); // plaintext (undetectable by shape)
|
|
145
|
+
expect(isSecretValue("just a string")).toBe(false);
|
|
146
|
+
expect(isSecretValue(null)).toBe(false);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it("strips Secret-shaped values anywhere in the tree", () => {
|
|
150
|
+
const payload = {
|
|
151
|
+
title: "keep",
|
|
152
|
+
apiKey: { get: () => "SECRET" }, // resolved Secret → dropped
|
|
153
|
+
nested: { token: { get: () => "SECRET2" }, keep: 1 },
|
|
154
|
+
list: [{ get: () => "S" }, { real: "item" }],
|
|
155
|
+
block: { __resolveType: "website/loaders/secret.ts", encrypted: "enc" }, // dropped
|
|
156
|
+
};
|
|
157
|
+
const out = JSON.parse(stringifyWithoutSecrets(payload));
|
|
158
|
+
expect(out).toEqual({
|
|
159
|
+
title: "keep",
|
|
160
|
+
nested: { keep: 1 },
|
|
161
|
+
list: [null, { real: "item" }], // Secret element → null (array slot preserved)
|
|
162
|
+
// apiKey and block are gone entirely
|
|
163
|
+
});
|
|
164
|
+
expect("apiKey" in out).toBe(false);
|
|
165
|
+
expect("block" in out).toBe(false);
|
|
166
|
+
});
|
|
167
|
+
});
|
package/src/cms/renderJson.ts
CHANGED
|
@@ -147,3 +147,33 @@ export function serializeRenderJson(
|
|
|
147
147
|
// Interleave eager + lazy by their position in the page's flat section list.
|
|
148
148
|
return [...eager, ...lazy].sort((a, b) => (a.index ?? 0) - (b.index ?? 0)).map((e) => e.out);
|
|
149
149
|
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* A resolved Deco `Secret` is `{ get: () => string | null }` (apps-website
|
|
153
|
+
* `loaders/secret.ts`) — the plaintext lives behind a function, so JSON.stringify
|
|
154
|
+
* already drops it to `{}`. An unresolved secret block is `{ __resolveType:
|
|
155
|
+
* ".../secret.ts", encrypted, ... }`. Neither has any place in a page-as-JSON
|
|
156
|
+
* payload, so we remove the whole field. Matches by shape, so no content field
|
|
157
|
+
* is caught by accident.
|
|
158
|
+
*
|
|
159
|
+
* Limitation: a *plaintext* secret a loader extracted into a string prop (the
|
|
160
|
+
* deprecated `SecretString` or a manual `secret.get()`) is indistinguishable from
|
|
161
|
+
* content — keep secrets as `Secret`, never resolve them into a serialized prop.
|
|
162
|
+
*/
|
|
163
|
+
export function isSecretValue(value: unknown): boolean {
|
|
164
|
+
if (!value || typeof value !== "object") return false;
|
|
165
|
+
const o = value as Record<string, unknown>;
|
|
166
|
+
// Resolved Secret: an object whose only own key is a `get` function.
|
|
167
|
+
if (typeof o.get === "function" && Object.keys(o).length === 1) return true;
|
|
168
|
+
// Unresolved secret block (defensive — resolved props usually drop __resolveType).
|
|
169
|
+
const rt = o.__resolveType;
|
|
170
|
+
return typeof rt === "string" && /secret/i.test(rt);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* `JSON.stringify` that drops any Secret-shaped value ({@link isSecretValue})
|
|
175
|
+
* anywhere in the tree. Used for both `?renderJson` and `?asJson` bodies.
|
|
176
|
+
*/
|
|
177
|
+
export function stringifyWithoutSecrets(value: unknown): string {
|
|
178
|
+
return JSON.stringify(value, (_key, v) => (isSecretValue(v) ? undefined : v));
|
|
179
|
+
}
|