@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@decocms/blocks",
3
- "version": "7.41.1",
3
+ "version": "7.43.0",
4
4
  "type": "module",
5
5
  "engines": {
6
6
  "node": ">=24"
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 { type RenderJsonModule, serializeRenderJson } from "./renderJson";
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
+ });
@@ -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
+ }