@actuate-media/cms-core 0.29.0 → 0.30.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/dist/__tests__/actions/seo-fields-update.test.d.ts +2 -0
- package/dist/__tests__/actions/seo-fields-update.test.d.ts.map +1 -0
- package/dist/__tests__/actions/seo-fields-update.test.js +93 -0
- package/dist/__tests__/actions/seo-fields-update.test.js.map +1 -0
- package/dist/__tests__/api/llms-txt.test.d.ts +2 -0
- package/dist/__tests__/api/llms-txt.test.d.ts.map +1 -0
- package/dist/__tests__/api/llms-txt.test.js +77 -0
- package/dist/__tests__/api/llms-txt.test.js.map +1 -0
- package/dist/__tests__/api/seo-read-access.test.d.ts +2 -0
- package/dist/__tests__/api/seo-read-access.test.d.ts.map +1 -0
- package/dist/__tests__/api/seo-read-access.test.js +101 -0
- package/dist/__tests__/api/seo-read-access.test.js.map +1 -0
- package/dist/__tests__/api/seo-summary-ssrf.test.d.ts +2 -0
- package/dist/__tests__/api/seo-summary-ssrf.test.d.ts.map +1 -0
- package/dist/__tests__/api/seo-summary-ssrf.test.js +80 -0
- package/dist/__tests__/api/seo-summary-ssrf.test.js.map +1 -0
- package/dist/__tests__/api/sitemap-pagination.test.d.ts +2 -0
- package/dist/__tests__/api/sitemap-pagination.test.d.ts.map +1 -0
- package/dist/__tests__/api/sitemap-pagination.test.js +100 -0
- package/dist/__tests__/api/sitemap-pagination.test.js.map +1 -0
- package/dist/__tests__/middleware.test.d.ts +2 -0
- package/dist/__tests__/middleware.test.d.ts.map +1 -0
- package/dist/__tests__/middleware.test.js +50 -0
- package/dist/__tests__/middleware.test.js.map +1 -0
- package/dist/__tests__/seo/analysis-markdown.test.d.ts +2 -0
- package/dist/__tests__/seo/analysis-markdown.test.d.ts.map +1 -0
- package/dist/__tests__/seo/analysis-markdown.test.js +57 -0
- package/dist/__tests__/seo/analysis-markdown.test.js.map +1 -0
- package/dist/__tests__/seo/audit-runner.test.d.ts +2 -0
- package/dist/__tests__/seo/audit-runner.test.d.ts.map +1 -0
- package/dist/__tests__/seo/audit-runner.test.js +82 -0
- package/dist/__tests__/seo/audit-runner.test.js.map +1 -0
- package/dist/__tests__/seo/internal-collections.test.d.ts +2 -0
- package/dist/__tests__/seo/internal-collections.test.d.ts.map +1 -0
- package/dist/__tests__/seo/internal-collections.test.js +27 -0
- package/dist/__tests__/seo/internal-collections.test.js.map +1 -0
- package/dist/__tests__/seo/page-meta.test.js +34 -2
- package/dist/__tests__/seo/page-meta.test.js.map +1 -1
- package/dist/__tests__/seo/robots.test.js +23 -2
- package/dist/__tests__/seo/robots.test.js.map +1 -1
- package/dist/actions.d.ts +24 -0
- package/dist/actions.d.ts.map +1 -1
- package/dist/actions.js +63 -0
- package/dist/actions.js.map +1 -1
- package/dist/api/handlers.d.ts.map +1 -1
- package/dist/api/handlers.js +295 -97
- package/dist/api/handlers.js.map +1 -1
- package/dist/config/types.d.ts +7 -0
- package/dist/config/types.d.ts.map +1 -1
- package/dist/middleware.js +12 -2
- package/dist/middleware.js.map +1 -1
- package/dist/seo/analysis.d.ts +9 -0
- package/dist/seo/analysis.d.ts.map +1 -1
- package/dist/seo/analysis.js +45 -6
- package/dist/seo/analysis.js.map +1 -1
- package/dist/seo/audit-runner.d.ts +6 -0
- package/dist/seo/audit-runner.d.ts.map +1 -1
- package/dist/seo/audit-runner.js +32 -3
- package/dist/seo/audit-runner.js.map +1 -1
- package/dist/seo/index.d.ts +1 -1
- package/dist/seo/index.d.ts.map +1 -1
- package/dist/seo/index.js +1 -1
- package/dist/seo/index.js.map +1 -1
- package/dist/seo/page-meta.d.ts +6 -0
- package/dist/seo/page-meta.d.ts.map +1 -1
- package/dist/seo/page-meta.js +22 -5
- package/dist/seo/page-meta.js.map +1 -1
- package/dist/seo/robots.d.ts +7 -0
- package/dist/seo/robots.d.ts.map +1 -1
- package/dist/seo/robots.js +10 -1
- package/dist/seo/robots.js.map +1 -1
- package/dist/seo/score.d.ts +5 -1
- package/dist/seo/score.d.ts.map +1 -1
- package/dist/seo/score.js +7 -4
- package/dist/seo/score.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"seo-fields-update.test.d.ts","sourceRoot":"","sources":["../../../src/__tests__/actions/seo-fields-update.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import { updateDocumentSeoFields } from '../../actions.js';
|
|
3
|
+
import { setActuateConfig } from '../../config/runtime.js';
|
|
4
|
+
/**
|
|
5
|
+
* Regression tests for the `PUT /seo/content` data-layer blocker. SEO edits
|
|
6
|
+
* must run through the data layer so they:
|
|
7
|
+
* - honor per-collection `access.update` (no cross-collection writes),
|
|
8
|
+
* - snapshot a Version row (audit trail), and
|
|
9
|
+
* - preserve undeclared SEO convention keys (canonical, og*, …) that the
|
|
10
|
+
* generic field-access layer would strip.
|
|
11
|
+
*/
|
|
12
|
+
function createMockDB(existing) {
|
|
13
|
+
const versions = [];
|
|
14
|
+
let current = existing
|
|
15
|
+
? { id: 'doc-1', collection: 'pages', data: existing, deletedAt: null }
|
|
16
|
+
: null;
|
|
17
|
+
return {
|
|
18
|
+
document: {
|
|
19
|
+
findFirst: vi.fn(async ({ where }) => {
|
|
20
|
+
if (!current)
|
|
21
|
+
return null;
|
|
22
|
+
if (where.id && current.id !== where.id)
|
|
23
|
+
return null;
|
|
24
|
+
if (where.collection && current.collection !== where.collection)
|
|
25
|
+
return null;
|
|
26
|
+
return current;
|
|
27
|
+
}),
|
|
28
|
+
},
|
|
29
|
+
version: { create: vi.fn(async ({ data }) => ({ id: `v-${versions.length}`, ...data })) },
|
|
30
|
+
$transaction: vi.fn(async (fn) => fn({
|
|
31
|
+
document: {
|
|
32
|
+
update: vi.fn(async ({ data }) => {
|
|
33
|
+
current = { ...current, ...data };
|
|
34
|
+
return current;
|
|
35
|
+
}),
|
|
36
|
+
},
|
|
37
|
+
version: {
|
|
38
|
+
create: vi.fn(async ({ data }) => {
|
|
39
|
+
const v = { id: `v-${versions.length}`, ...data };
|
|
40
|
+
versions.push(v);
|
|
41
|
+
return v;
|
|
42
|
+
}),
|
|
43
|
+
},
|
|
44
|
+
})),
|
|
45
|
+
_versions: versions,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
function ctxFor(db, role = 'ADMIN') {
|
|
49
|
+
return { userId: 'user-1', role, db };
|
|
50
|
+
}
|
|
51
|
+
describe('updateDocumentSeoFields', () => {
|
|
52
|
+
beforeEach(() => {
|
|
53
|
+
setActuateConfig({});
|
|
54
|
+
});
|
|
55
|
+
afterEach(() => {
|
|
56
|
+
setActuateConfig({});
|
|
57
|
+
});
|
|
58
|
+
it('merges SEO fields while preserving existing + undeclared keys', async () => {
|
|
59
|
+
const db = createMockDB({ title: 'About', body: 'Original body' });
|
|
60
|
+
const result = await updateDocumentSeoFields('pages', 'doc-1', { canonical: 'https://example.com/about', ogTitle: 'About Us', metaTitle: 'About' }, ctxFor(db));
|
|
61
|
+
const data = (result.data ?? {});
|
|
62
|
+
expect(data.title).toBe('About');
|
|
63
|
+
expect(data.body).toBe('Original body');
|
|
64
|
+
expect(data.canonical).toBe('https://example.com/about');
|
|
65
|
+
expect(data.ogTitle).toBe('About Us');
|
|
66
|
+
expect(data.metaTitle).toBe('About');
|
|
67
|
+
});
|
|
68
|
+
it('snapshots a Version row on update (audit trail)', async () => {
|
|
69
|
+
const db = createMockDB({ title: 'About' });
|
|
70
|
+
await updateDocumentSeoFields('pages', 'doc-1', { metaTitle: 'About' }, ctxFor(db));
|
|
71
|
+
expect(db._versions.length).toBe(1);
|
|
72
|
+
expect(db._versions[0]).toMatchObject({ documentId: 'doc-1', changeType: 'UPDATE' });
|
|
73
|
+
});
|
|
74
|
+
it('throws for a non-existent document', async () => {
|
|
75
|
+
const db = createMockDB(null);
|
|
76
|
+
await expect(updateDocumentSeoFields('pages', 'missing', { metaTitle: 'x' }, ctxFor(db))).rejects.toThrow('not found');
|
|
77
|
+
});
|
|
78
|
+
it('enforces the collection update access rule (no cross-collection writes)', async () => {
|
|
79
|
+
setActuateConfig({
|
|
80
|
+
collections: {
|
|
81
|
+
pages: {
|
|
82
|
+
slug: 'pages',
|
|
83
|
+
labels: { singular: 'Page', plural: 'Pages' },
|
|
84
|
+
fields: {},
|
|
85
|
+
access: { update: () => false },
|
|
86
|
+
},
|
|
87
|
+
},
|
|
88
|
+
});
|
|
89
|
+
const db = createMockDB({ title: 'About' });
|
|
90
|
+
await expect(updateDocumentSeoFields('pages', 'doc-1', { metaTitle: 'x' }, ctxFor(db, 'AUTHOR'))).rejects.toThrow('Access denied');
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
//# sourceMappingURL=seo-fields-update.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"seo-fields-update.test.js","sourceRoot":"","sources":["../../../src/__tests__/actions/seo-fields-update.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"llms-txt.test.d.ts","sourceRoot":"","sources":["../../../src/__tests__/api/llms-txt.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it } from 'vitest';
|
|
2
|
+
import { handleActuateAPI } from '../../api/index.js';
|
|
3
|
+
import { initDB } from '../../db.js';
|
|
4
|
+
function createMockDB(docs) {
|
|
5
|
+
const findMany = async (args) => {
|
|
6
|
+
const where = args?.where ?? {};
|
|
7
|
+
return docs.filter((d) => {
|
|
8
|
+
if (where.status && d.status !== where.status)
|
|
9
|
+
return false;
|
|
10
|
+
if (where.deletedAt === null && d.deletedAt !== null)
|
|
11
|
+
return false;
|
|
12
|
+
return true;
|
|
13
|
+
});
|
|
14
|
+
};
|
|
15
|
+
return {
|
|
16
|
+
document: { findMany, findFirst: async () => null, count: async () => 0 },
|
|
17
|
+
user: {},
|
|
18
|
+
session: {},
|
|
19
|
+
media: {},
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
const PUBLIC_PAGE = {
|
|
23
|
+
title: 'Public About Page',
|
|
24
|
+
slug: 'about',
|
|
25
|
+
collection: 'pages',
|
|
26
|
+
status: 'PUBLISHED',
|
|
27
|
+
deletedAt: null,
|
|
28
|
+
updatedAt: new Date('2026-03-01T00:00:00Z'),
|
|
29
|
+
data: { metaDescription: 'About us' },
|
|
30
|
+
};
|
|
31
|
+
const SOFT_DELETED = {
|
|
32
|
+
title: 'Deleted Secret Page',
|
|
33
|
+
slug: 'deleted-secret',
|
|
34
|
+
collection: 'pages',
|
|
35
|
+
status: 'PUBLISHED',
|
|
36
|
+
deletedAt: new Date('2026-02-01T00:00:00Z'),
|
|
37
|
+
updatedAt: new Date('2026-02-15T00:00:00Z'),
|
|
38
|
+
data: {},
|
|
39
|
+
};
|
|
40
|
+
const INTERNAL_CONFIG = {
|
|
41
|
+
title: 'SEO Config',
|
|
42
|
+
slug: 'seo-config',
|
|
43
|
+
collection: '__seo_config__',
|
|
44
|
+
status: 'PUBLISHED',
|
|
45
|
+
deletedAt: null,
|
|
46
|
+
updatedAt: new Date('2026-03-10T00:00:00Z'),
|
|
47
|
+
data: {},
|
|
48
|
+
};
|
|
49
|
+
describe('GET /llms.txt', () => {
|
|
50
|
+
beforeEach(() => {
|
|
51
|
+
delete globalThis.__actuateConfig;
|
|
52
|
+
});
|
|
53
|
+
it('lists public content but excludes soft-deleted and internal docs', async () => {
|
|
54
|
+
const docs = [PUBLIC_PAGE, SOFT_DELETED, INTERNAL_CONFIG];
|
|
55
|
+
initDB(createMockDB(docs));
|
|
56
|
+
const handler = handleActuateAPI({
|
|
57
|
+
prismaClient: createMockDB(docs),
|
|
58
|
+
config: {
|
|
59
|
+
seo: { siteUrl: 'https://example.com' },
|
|
60
|
+
collections: {
|
|
61
|
+
pages: { slug: 'pages', labels: { singular: 'Page', plural: 'Pages' }, fields: {} },
|
|
62
|
+
},
|
|
63
|
+
},
|
|
64
|
+
});
|
|
65
|
+
const res = await handler(new Request('https://example.com/api/cms/llms.txt'));
|
|
66
|
+
expect(res.status).toBe(200);
|
|
67
|
+
const text = await res.text();
|
|
68
|
+
expect(text).toContain('Public About Page');
|
|
69
|
+
// Soft-deleted content must never surface publicly.
|
|
70
|
+
expect(text).not.toContain('Deleted Secret Page');
|
|
71
|
+
expect(text).not.toContain('deleted-secret');
|
|
72
|
+
// Internal/system collections must never surface publicly.
|
|
73
|
+
expect(text).not.toContain('__seo_config__');
|
|
74
|
+
expect(text).not.toContain('SEO Config');
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
//# sourceMappingURL=llms-txt.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"llms-txt.test.js","sourceRoot":"","sources":["../../../src/__tests__/api/llms-txt.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"seo-read-access.test.d.ts","sourceRoot":"","sources":["../../../src/__tests__/api/seo-read-access.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it } from 'vitest';
|
|
2
|
+
import { handleActuateAPI } from '../../api/index.js';
|
|
3
|
+
import { initDB } from '../../db.js';
|
|
4
|
+
import { generateApiKey } from '../../security/api-key-enhanced.js';
|
|
5
|
+
/**
|
|
6
|
+
* Regression test for the SEO read-endpoint access blocker. The
|
|
7
|
+
* `/seo/analysis|readability|internal-links|schema|meta` endpoints previously
|
|
8
|
+
* only checked `requireAuth`, so any authenticated principal could pull SEO
|
|
9
|
+
* intelligence for documents in collections they have no read access to. They
|
|
10
|
+
* must now enforce the collection's `access.read` rule.
|
|
11
|
+
*/
|
|
12
|
+
const VALID_SECRET = 'a'.repeat(48);
|
|
13
|
+
async function makeKey(scopes) {
|
|
14
|
+
const { key, keyHash, keyPrefix } = await generateApiKey({ prefix: 'act_sk', scopes });
|
|
15
|
+
return {
|
|
16
|
+
key,
|
|
17
|
+
record: {
|
|
18
|
+
id: 'apikey-1',
|
|
19
|
+
keyHash,
|
|
20
|
+
keyPrefix,
|
|
21
|
+
userId: 'user-1',
|
|
22
|
+
scopes,
|
|
23
|
+
ipRestrictions: null,
|
|
24
|
+
expiresAt: null,
|
|
25
|
+
lastUsedAt: null,
|
|
26
|
+
revokedAt: null,
|
|
27
|
+
},
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
function createMockDB(apiKey, doc) {
|
|
31
|
+
return {
|
|
32
|
+
apiKey: {
|
|
33
|
+
findMany: async () => [apiKey],
|
|
34
|
+
findUnique: async ({ where }) => apiKey.keyHash === where.keyHash ? apiKey : null,
|
|
35
|
+
update: async () => apiKey,
|
|
36
|
+
},
|
|
37
|
+
document: {
|
|
38
|
+
findFirst: async () => doc,
|
|
39
|
+
findMany: async () => [],
|
|
40
|
+
count: async () => 0,
|
|
41
|
+
},
|
|
42
|
+
user: {},
|
|
43
|
+
session: {},
|
|
44
|
+
media: {},
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
const PAGE_DOC = {
|
|
48
|
+
id: 'doc-1',
|
|
49
|
+
collection: 'pages',
|
|
50
|
+
title: 'Hello',
|
|
51
|
+
slug: 'hello',
|
|
52
|
+
data: { content: 'Some body content for analysis.' },
|
|
53
|
+
createdAt: new Date(),
|
|
54
|
+
updatedAt: new Date(),
|
|
55
|
+
};
|
|
56
|
+
describe('GET /seo/analysis access control', () => {
|
|
57
|
+
beforeEach(() => {
|
|
58
|
+
delete globalThis.__actuateConfig;
|
|
59
|
+
process.env.CMS_SECRET = VALID_SECRET;
|
|
60
|
+
});
|
|
61
|
+
it('returns 403 when the collection access.read rule denies the user', async () => {
|
|
62
|
+
const { key, record } = await makeKey({ collections: ['pages'], actions: ['read'] });
|
|
63
|
+
const db = createMockDB(record, PAGE_DOC);
|
|
64
|
+
initDB(db);
|
|
65
|
+
const handler = handleActuateAPI({
|
|
66
|
+
prismaClient: db,
|
|
67
|
+
config: { collections: { pages: { access: { read: () => false } } } },
|
|
68
|
+
});
|
|
69
|
+
const res = await handler(new Request('https://x.com/api/cms/seo/analysis/doc-1', {
|
|
70
|
+
headers: { Authorization: `Bearer ${key}` },
|
|
71
|
+
}));
|
|
72
|
+
expect(res.status).toBe(403);
|
|
73
|
+
});
|
|
74
|
+
it('returns 200 when the collection access.read rule allows the user', async () => {
|
|
75
|
+
const { key, record } = await makeKey({ collections: ['pages'], actions: ['read'] });
|
|
76
|
+
const db = createMockDB(record, PAGE_DOC);
|
|
77
|
+
initDB(db);
|
|
78
|
+
const handler = handleActuateAPI({
|
|
79
|
+
prismaClient: db,
|
|
80
|
+
config: { collections: { pages: { access: { read: () => true } } } },
|
|
81
|
+
});
|
|
82
|
+
const res = await handler(new Request('https://x.com/api/cms/seo/analysis/doc-1', {
|
|
83
|
+
headers: { Authorization: `Bearer ${key}` },
|
|
84
|
+
}));
|
|
85
|
+
expect(res.status).toBe(200);
|
|
86
|
+
});
|
|
87
|
+
it('rejects an API key without read scope on the collection (403)', async () => {
|
|
88
|
+
const { key, record } = await makeKey({ collections: ['posts'], actions: ['read'] });
|
|
89
|
+
const db = createMockDB(record, PAGE_DOC);
|
|
90
|
+
initDB(db);
|
|
91
|
+
const handler = handleActuateAPI({
|
|
92
|
+
prismaClient: db,
|
|
93
|
+
config: { collections: { pages: { access: { read: () => true } } } },
|
|
94
|
+
});
|
|
95
|
+
const res = await handler(new Request('https://x.com/api/cms/seo/analysis/doc-1', {
|
|
96
|
+
headers: { Authorization: `Bearer ${key}` },
|
|
97
|
+
}));
|
|
98
|
+
expect(res.status).toBe(403);
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
//# sourceMappingURL=seo-read-access.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"seo-read-access.test.js","sourceRoot":"","sources":["../../../src/__tests__/api/seo-read-access.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"seo-summary-ssrf.test.d.ts","sourceRoot":"","sources":["../../../src/__tests__/api/seo-summary-ssrf.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it } from 'vitest';
|
|
2
|
+
import { handleActuateAPI } from '../../api/index.js';
|
|
3
|
+
import { initDB } from '../../db.js';
|
|
4
|
+
import { generateApiKey } from '../../security/api-key-enhanced.js';
|
|
5
|
+
/**
|
|
6
|
+
* Regression test for the `/seo/summary` SSRF blocker. The link-health probe
|
|
7
|
+
* issues outbound requests, so the endpoint must be gated to EDITOR+ (mirroring
|
|
8
|
+
* `/seo/link-health`) rather than any authenticated user. The SSRF primitive
|
|
9
|
+
* itself (private-IP blocking via `safeFetch`) is covered by
|
|
10
|
+
* `__tests__/security/safe-fetch.test.ts`.
|
|
11
|
+
*/
|
|
12
|
+
const VALID_SECRET = 'a'.repeat(48);
|
|
13
|
+
async function makeKey(scopes) {
|
|
14
|
+
const { key, keyHash, keyPrefix } = await generateApiKey({ prefix: 'act_sk', scopes });
|
|
15
|
+
return {
|
|
16
|
+
key,
|
|
17
|
+
record: {
|
|
18
|
+
id: 'apikey-1',
|
|
19
|
+
keyHash,
|
|
20
|
+
keyPrefix,
|
|
21
|
+
userId: 'user-1',
|
|
22
|
+
scopes,
|
|
23
|
+
ipRestrictions: null,
|
|
24
|
+
expiresAt: null,
|
|
25
|
+
lastUsedAt: null,
|
|
26
|
+
revokedAt: null,
|
|
27
|
+
},
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
function createMockDB(apiKey) {
|
|
31
|
+
return {
|
|
32
|
+
apiKey: {
|
|
33
|
+
findMany: async () => [apiKey],
|
|
34
|
+
findUnique: async ({ where }) => apiKey.keyHash === where.keyHash ? apiKey : null,
|
|
35
|
+
update: async () => apiKey,
|
|
36
|
+
},
|
|
37
|
+
document: { findMany: async () => [], count: async () => 0, findFirst: async () => null },
|
|
38
|
+
user: {},
|
|
39
|
+
session: {},
|
|
40
|
+
media: {},
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
describe('GET /seo/summary access control', () => {
|
|
44
|
+
beforeEach(() => {
|
|
45
|
+
delete globalThis.__actuateConfig;
|
|
46
|
+
delete process.env.NEXT_PUBLIC_SITE_URL;
|
|
47
|
+
process.env.CMS_SECRET = VALID_SECRET;
|
|
48
|
+
});
|
|
49
|
+
it('rejects a non-privileged (non-admin) API key with 403', async () => {
|
|
50
|
+
const { key, record } = await makeKey({ collections: ['pages'], actions: ['read'] });
|
|
51
|
+
const db = createMockDB(record);
|
|
52
|
+
initDB(db);
|
|
53
|
+
const handler = handleActuateAPI({ prismaClient: db, config: { collections: {} } });
|
|
54
|
+
const res = await handler(new Request('https://x.com/api/cms/seo/summary', {
|
|
55
|
+
headers: { Authorization: `Bearer ${key}` },
|
|
56
|
+
}));
|
|
57
|
+
expect(res.status).toBe(403);
|
|
58
|
+
});
|
|
59
|
+
it('requires authentication', async () => {
|
|
60
|
+
const { record } = await makeKey({ collections: ['pages'], actions: ['read'] });
|
|
61
|
+
const db = createMockDB(record);
|
|
62
|
+
initDB(db);
|
|
63
|
+
const handler = handleActuateAPI({ prismaClient: db, config: { collections: {} } });
|
|
64
|
+
const res = await handler(new Request('https://x.com/api/cms/seo/summary'));
|
|
65
|
+
expect(res.status).toBe(401);
|
|
66
|
+
});
|
|
67
|
+
it('allows an admin key and does not probe external hosts when site URL is unset', async () => {
|
|
68
|
+
const { key, record } = await makeKey({ admin: true });
|
|
69
|
+
const db = createMockDB(record);
|
|
70
|
+
initDB(db);
|
|
71
|
+
const handler = handleActuateAPI({ prismaClient: db, config: { collections: {} } });
|
|
72
|
+
const res = await handler(new Request('https://x.com/api/cms/seo/summary', {
|
|
73
|
+
headers: { Authorization: `Bearer ${key}` },
|
|
74
|
+
}));
|
|
75
|
+
expect(res.status).toBe(200);
|
|
76
|
+
const body = (await res.json());
|
|
77
|
+
expect(body.data.issuesSummary.brokenInternalLinks).toBe(0);
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
//# sourceMappingURL=seo-summary-ssrf.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"seo-summary-ssrf.test.js","sourceRoot":"","sources":["../../../src/__tests__/api/seo-summary-ssrf.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sitemap-pagination.test.d.ts","sourceRoot":"","sources":["../../../src/__tests__/api/sitemap-pagination.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import { handleActuateAPI } from '../../api/index.js';
|
|
3
|
+
import { initDB } from '../../db.js';
|
|
4
|
+
/**
|
|
5
|
+
* Regression test for sitemap pagination. The old per-collection sitemap capped
|
|
6
|
+
* at 5,000 URLs and silently dropped the rest, and the index emitted a single
|
|
7
|
+
* `<sitemap>` per collection regardless of size. Collections larger than the
|
|
8
|
+
* 50k protocol limit must now be split across `?page=N` files referenced from
|
|
9
|
+
* the index.
|
|
10
|
+
*/
|
|
11
|
+
const SITEMAP_PAGE_SIZE = 45000;
|
|
12
|
+
function createMockDB(opts) {
|
|
13
|
+
return {
|
|
14
|
+
document: {
|
|
15
|
+
count: vi.fn(async () => opts.count),
|
|
16
|
+
findMany: opts.findMany,
|
|
17
|
+
findFirst: vi.fn(async () => null),
|
|
18
|
+
},
|
|
19
|
+
user: {},
|
|
20
|
+
session: {},
|
|
21
|
+
media: {},
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
const postsConfig = {
|
|
25
|
+
seo: { siteUrl: 'https://example.com' },
|
|
26
|
+
collections: {
|
|
27
|
+
posts: {
|
|
28
|
+
slug: 'posts',
|
|
29
|
+
labels: { singular: 'Post', plural: 'Posts' },
|
|
30
|
+
urlPrefix: 'blog',
|
|
31
|
+
type: 'post',
|
|
32
|
+
fields: {},
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
};
|
|
36
|
+
describe('sitemap pagination', () => {
|
|
37
|
+
beforeEach(() => {
|
|
38
|
+
delete globalThis.__actuateConfig;
|
|
39
|
+
});
|
|
40
|
+
it('emits one <sitemap> entry per 45k-doc page in the index', async () => {
|
|
41
|
+
const findMany = vi.fn(async () => []);
|
|
42
|
+
const db = createMockDB({ count: SITEMAP_PAGE_SIZE * 2 + 10, findMany });
|
|
43
|
+
initDB(db);
|
|
44
|
+
const handler = handleActuateAPI({ prismaClient: db, config: postsConfig });
|
|
45
|
+
const res = await handler(new Request('https://example.com/api/cms/sitemap.xml'));
|
|
46
|
+
expect(res.status).toBe(200);
|
|
47
|
+
const text = await res.text();
|
|
48
|
+
// 90,010 docs / 45,000 = 3 pages → page 0 (bare) + page 1 + page 2.
|
|
49
|
+
expect(text).toContain('<loc>https://example.com/api/cms/sitemaps/posts.xml</loc>');
|
|
50
|
+
expect(text).toContain('<loc>https://example.com/api/cms/sitemaps/posts.xml?page=1</loc>');
|
|
51
|
+
expect(text).toContain('<loc>https://example.com/api/cms/sitemaps/posts.xml?page=2</loc>');
|
|
52
|
+
expect(text).not.toContain('?page=3');
|
|
53
|
+
});
|
|
54
|
+
it('emits a single entry for a small collection', async () => {
|
|
55
|
+
const findMany = vi.fn(async () => []);
|
|
56
|
+
const db = createMockDB({ count: 12, findMany });
|
|
57
|
+
initDB(db);
|
|
58
|
+
const handler = handleActuateAPI({ prismaClient: db, config: postsConfig });
|
|
59
|
+
const res = await handler(new Request('https://example.com/api/cms/sitemap.xml'));
|
|
60
|
+
const text = await res.text();
|
|
61
|
+
expect(text).toContain('<loc>https://example.com/api/cms/sitemaps/posts.xml</loc>');
|
|
62
|
+
expect(text).not.toContain('?page=1');
|
|
63
|
+
});
|
|
64
|
+
it('applies skip/take when a page is requested', async () => {
|
|
65
|
+
const findMany = vi.fn(async (_args) => []);
|
|
66
|
+
const db = createMockDB({ count: SITEMAP_PAGE_SIZE * 2, findMany });
|
|
67
|
+
initDB(db);
|
|
68
|
+
const handler = handleActuateAPI({ prismaClient: db, config: postsConfig });
|
|
69
|
+
await handler(new Request('https://example.com/api/cms/sitemaps/posts.xml?page=2'));
|
|
70
|
+
expect(findMany).toHaveBeenCalledTimes(1);
|
|
71
|
+
const arg = (findMany.mock.calls[0]?.[0] ?? {});
|
|
72
|
+
expect(arg.skip).toBe(2 * SITEMAP_PAGE_SIZE);
|
|
73
|
+
expect(arg.take).toBe(SITEMAP_PAGE_SIZE);
|
|
74
|
+
});
|
|
75
|
+
it('omits the archive URL on pages after the first', async () => {
|
|
76
|
+
const findMany = vi.fn(async () => []);
|
|
77
|
+
const db = createMockDB({ count: SITEMAP_PAGE_SIZE * 2, findMany });
|
|
78
|
+
initDB(db);
|
|
79
|
+
const handler = handleActuateAPI({
|
|
80
|
+
prismaClient: db,
|
|
81
|
+
config: {
|
|
82
|
+
seo: { siteUrl: 'https://example.com' },
|
|
83
|
+
collections: {
|
|
84
|
+
posts: {
|
|
85
|
+
slug: 'posts',
|
|
86
|
+
labels: { singular: 'Post', plural: 'Posts' },
|
|
87
|
+
urlPrefix: 'blog',
|
|
88
|
+
type: 'post',
|
|
89
|
+
seo: { archivePath: '/blog' },
|
|
90
|
+
fields: {},
|
|
91
|
+
},
|
|
92
|
+
},
|
|
93
|
+
},
|
|
94
|
+
});
|
|
95
|
+
const res = await handler(new Request('https://example.com/api/cms/sitemaps/posts.xml?page=1'));
|
|
96
|
+
const text = await res.text();
|
|
97
|
+
expect(text).not.toContain('<loc>https://example.com/blog</loc>');
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
//# sourceMappingURL=sitemap-pagination.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sitemap-pagination.test.js","sourceRoot":"","sources":["../../../src/__tests__/api/sitemap-pagination.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"middleware.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/middleware.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
2
|
+
import { createCMSMiddleware } from '../middleware.js';
|
|
3
|
+
/**
|
|
4
|
+
* Regression coverage for the admin/API route matching boundary. The guard used
|
|
5
|
+
* `startsWith('/admin')`, which also captured sibling public routes such as
|
|
6
|
+
* `/administrators` or `/admin-news`. Those must NOT be treated as admin routes.
|
|
7
|
+
*/
|
|
8
|
+
describe('createCMSMiddleware route boundary', () => {
|
|
9
|
+
const origSecret = process.env.CMS_SECRET;
|
|
10
|
+
beforeEach(() => {
|
|
11
|
+
process.env.CMS_SECRET = 'x'.repeat(32);
|
|
12
|
+
});
|
|
13
|
+
afterEach(() => {
|
|
14
|
+
process.env.CMS_SECRET = origSecret;
|
|
15
|
+
});
|
|
16
|
+
function req(path) {
|
|
17
|
+
return new Request(`https://example.com${path}`);
|
|
18
|
+
}
|
|
19
|
+
it('does not treat /admin-news as an admin route (IP allowlist not enforced)', async () => {
|
|
20
|
+
const mw = createCMSMiddleware({ admin: { path: '/admin', ipAllowlist: ['10.0.0.1'] } });
|
|
21
|
+
// An admin route with a non-allowlisted IP would be 403; a public route
|
|
22
|
+
// passes through (null). /admin-news must pass through.
|
|
23
|
+
const res = await mw(req('/admin-news'));
|
|
24
|
+
expect(res).toBeNull();
|
|
25
|
+
});
|
|
26
|
+
it('does not treat /administrators as an admin route', async () => {
|
|
27
|
+
const mw = createCMSMiddleware({ admin: { path: '/admin', ipAllowlist: ['10.0.0.1'] } });
|
|
28
|
+
expect(await mw(req('/administrators'))).toBeNull();
|
|
29
|
+
});
|
|
30
|
+
it('still enforces the allowlist on the exact admin path', async () => {
|
|
31
|
+
const mw = createCMSMiddleware({ admin: { path: '/admin', ipAllowlist: ['10.0.0.1'] } });
|
|
32
|
+
const res = await mw(req('/admin'));
|
|
33
|
+
expect(res?.status).toBe(403);
|
|
34
|
+
});
|
|
35
|
+
it('still enforces the allowlist on nested admin paths', async () => {
|
|
36
|
+
const mw = createCMSMiddleware({ admin: { path: '/admin', ipAllowlist: ['10.0.0.1'] } });
|
|
37
|
+
const res = await mw(req('/admin/collections/posts'));
|
|
38
|
+
expect(res?.status).toBe(403);
|
|
39
|
+
});
|
|
40
|
+
it('does not treat /api/cmsx as a CMS API route', async () => {
|
|
41
|
+
const mw = createCMSMiddleware({ admin: { path: '/admin', ipAllowlist: ['10.0.0.1'] } });
|
|
42
|
+
expect(await mw(req('/api/cmsx/thing'))).toBeNull();
|
|
43
|
+
});
|
|
44
|
+
it('enforces the allowlist on /api/cms routes', async () => {
|
|
45
|
+
const mw = createCMSMiddleware({ admin: { path: '/admin', ipAllowlist: ['10.0.0.1'] } });
|
|
46
|
+
const res = await mw(req('/api/cms/documents'));
|
|
47
|
+
expect(res?.status).toBe(403);
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
//# sourceMappingURL=middleware.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"middleware.test.js","sourceRoot":"","sources":["../../src/__tests__/middleware.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"analysis-markdown.test.d.ts","sourceRoot":"","sources":["../../../src/__tests__/seo/analysis-markdown.test.ts"],"names":[],"mappings":""}
|