Site builder: security & data-loss hardening + unified asset picker + audit backlog #3
@@ -1,5 +1,6 @@
|
|||||||
import { describe, test, expect, vi } from 'vitest';
|
import { describe, test, expect, vi } from 'vitest';
|
||||||
import { sanitizeAiTree, __test } from './apply-ai-response';
|
import { sanitizeAiTree, __test } from './apply-ai-response';
|
||||||
|
import type { SitesmithPatchOp } from '../types/sitesmith';
|
||||||
|
|
||||||
describe('findNodeIdByAiNodeId', () => {
|
describe('findNodeIdByAiNodeId', () => {
|
||||||
const query = {
|
const query = {
|
||||||
@@ -173,7 +174,7 @@ describe('applyPatch', () => {
|
|||||||
text: 'Old',
|
text: 'Old',
|
||||||
style: { color: 'red', fontSize: '16px' },
|
style: { color: 'red', fontSize: '16px' },
|
||||||
});
|
});
|
||||||
const ops = [
|
const ops: SitesmithPatchOp[] = [
|
||||||
{
|
{
|
||||||
op: 'update_props',
|
op: 'update_props',
|
||||||
node_id: 'ai-node-1',
|
node_id: 'ai-node-1',
|
||||||
@@ -194,7 +195,7 @@ describe('applyPatch', () => {
|
|||||||
text: 'Old',
|
text: 'Old',
|
||||||
style: { color: 'red', fontSize: '16px' },
|
style: { color: 'red', fontSize: '16px' },
|
||||||
});
|
});
|
||||||
const ops = [
|
const ops: SitesmithPatchOp[] = [
|
||||||
{
|
{
|
||||||
op: 'update_props',
|
op: 'update_props',
|
||||||
node_id: 'ai-node-1',
|
node_id: 'ai-node-1',
|
||||||
@@ -211,7 +212,7 @@ describe('applyPatch', () => {
|
|||||||
const { query, actions, propsById } = makeSingleNodeHarness('craft-1', 'ai-node-1', {
|
const { query, actions, propsById } = makeSingleNodeHarness('craft-1', 'ai-node-1', {
|
||||||
style: { color: 'red', fontSize: '16px' },
|
style: { color: 'red', fontSize: '16px' },
|
||||||
});
|
});
|
||||||
const ops = [
|
const ops: SitesmithPatchOp[] = [
|
||||||
{ op: 'update_props', node_id: 'ai-node-1', props: { style: { color: 'blue' } } },
|
{ op: 'update_props', node_id: 'ai-node-1', props: { style: { color: 'blue' } } },
|
||||||
];
|
];
|
||||||
__test.applyPatch(actions, query, ops);
|
__test.applyPatch(actions, query, ops);
|
||||||
@@ -221,7 +222,7 @@ describe('applyPatch', () => {
|
|||||||
test('an op with an unknown resolvedName tree is skipped without throwing; other ops in the batch still apply', () => {
|
test('an op with an unknown resolvedName tree is skipped without throwing; other ops in the batch still apply', () => {
|
||||||
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
||||||
const { query, actions, propsById } = makeSingleNodeHarness('craft-1', 'ai-node-1', {});
|
const { query, actions, propsById } = makeSingleNodeHarness('craft-1', 'ai-node-1', {});
|
||||||
const ops = [
|
const ops: SitesmithPatchOp[] = [
|
||||||
{
|
{
|
||||||
op: 'insert_after',
|
op: 'insert_after',
|
||||||
node_id: 'ai-node-1',
|
node_id: 'ai-node-1',
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { useEditor } from '@craftjs/core';
|
import { useEditor } from '@craftjs/core';
|
||||||
import type { NodeTree } from '@craftjs/core';
|
import type { NodeTree } from '@craftjs/core';
|
||||||
import { usePages } from '../state/PageContext';
|
import { usePages } from '../state/PageContext';
|
||||||
import { SitesmithResponse, SerializedTreeNode } from '../types/sitesmith';
|
import { SitesmithResponse, SerializedTreeNode, SitesmithPatchOp } from '../types/sitesmith';
|
||||||
import { sanitizeAiTree, flattenTreeForCraft } from './craft-tree';
|
import { sanitizeAiTree, flattenTreeForCraft } from './craft-tree';
|
||||||
|
|
||||||
// Re-exported for existing callers/tests that import sanitizeAiTree from
|
// Re-exported for existing callers/tests that import sanitizeAiTree from
|
||||||
@@ -155,7 +155,7 @@ export function useApplyAiResponse() {
|
|||||||
function applyPatch(
|
function applyPatch(
|
||||||
actions: any,
|
actions: any,
|
||||||
query: any,
|
query: any,
|
||||||
ops: any[],
|
ops: SitesmithPatchOp[],
|
||||||
): { ok: boolean; message?: string } {
|
): { ok: boolean; message?: string } {
|
||||||
for (const op of ops) {
|
for (const op of ops) {
|
||||||
const id = findNodeIdByAiNodeId(query, op.node_id);
|
const id = findNodeIdByAiNodeId(query, op.node_id);
|
||||||
@@ -237,7 +237,12 @@ function applyPatch(
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
console.warn('sitesmith patch: unknown op', (op as any).op);
|
// Every SitesmithPatchOp variant is handled above, so `op` is typed
|
||||||
|
// `never` here at compile time — but the AI response is untrusted
|
||||||
|
// JSON (see useSitesmith.ts's `j: SendResult = await r.json()`), so
|
||||||
|
// a malformed/unrecognized `op` field can still reach this branch at
|
||||||
|
// runtime. Log the raw value rather than assuming a shape.
|
||||||
|
console.warn('sitesmith patch: unknown op', op);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return { ok: true };
|
return { ok: true };
|
||||||
|
|||||||
Reference in New Issue
Block a user