@forumone/throughline-core 0.9.1 → 0.10.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.
Files changed (57) hide show
  1. package/CHANGELOG.md +77 -0
  2. package/README.md +192 -33
  3. package/bin/payload-cli.mjs +443 -0
  4. package/bin/throughline-payload.mjs +6 -0
  5. package/dist/audit/collection.d.ts +3 -0
  6. package/dist/audit/collection.d.ts.map +1 -1
  7. package/dist/audit/collection.js +2 -0
  8. package/dist/audit/collection.js.map +1 -1
  9. package/dist/env/index.d.ts +54 -0
  10. package/dist/env/index.d.ts.map +1 -0
  11. package/dist/env/index.js +113 -0
  12. package/dist/env/index.js.map +1 -0
  13. package/dist/index.d.ts +6 -2
  14. package/dist/index.d.ts.map +1 -1
  15. package/dist/index.js +3 -1
  16. package/dist/index.js.map +1 -1
  17. package/dist/mcp/api-key-access.d.ts +50 -0
  18. package/dist/mcp/api-key-access.d.ts.map +1 -0
  19. package/dist/mcp/api-key-access.js +94 -0
  20. package/dist/mcp/api-key-access.js.map +1 -0
  21. package/dist/mcp/index.d.ts +1 -0
  22. package/dist/mcp/index.d.ts.map +1 -1
  23. package/dist/mcp/index.js +1 -0
  24. package/dist/mcp/index.js.map +1 -1
  25. package/dist/observability/collection.d.ts +13 -0
  26. package/dist/observability/collection.d.ts.map +1 -0
  27. package/dist/observability/collection.js +76 -0
  28. package/dist/observability/collection.js.map +1 -0
  29. package/dist/observability/index.d.ts +9 -0
  30. package/dist/observability/index.d.ts.map +1 -0
  31. package/dist/observability/index.js +5 -0
  32. package/dist/observability/index.js.map +1 -0
  33. package/dist/observability/plugin.d.ts +21 -0
  34. package/dist/observability/plugin.d.ts.map +1 -0
  35. package/dist/observability/plugin.js +51 -0
  36. package/dist/observability/plugin.js.map +1 -0
  37. package/dist/observability/report.d.ts +175 -0
  38. package/dist/observability/report.d.ts.map +1 -0
  39. package/dist/observability/report.js +274 -0
  40. package/dist/observability/report.js.map +1 -0
  41. package/dist/observability/writer.d.ts +24 -0
  42. package/dist/observability/writer.d.ts.map +1 -0
  43. package/dist/observability/writer.js +72 -0
  44. package/dist/observability/writer.js.map +1 -0
  45. package/dist/testing/anonymousAccess.d.ts +111 -0
  46. package/dist/testing/anonymousAccess.d.ts.map +1 -0
  47. package/dist/testing/anonymousAccess.js +185 -0
  48. package/dist/testing/anonymousAccess.js.map +1 -0
  49. package/dist/testing/describeAnonymousAccess.d.ts +26 -0
  50. package/dist/testing/describeAnonymousAccess.d.ts.map +1 -0
  51. package/dist/testing/describeAnonymousAccess.js +76 -0
  52. package/dist/testing/describeAnonymousAccess.js.map +1 -0
  53. package/dist/testing/index.d.ts +11 -0
  54. package/dist/testing/index.d.ts.map +1 -0
  55. package/dist/testing/index.js +9 -0
  56. package/dist/testing/index.js.map +1 -0
  57. package/package.json +30 -7
@@ -0,0 +1,185 @@
1
+ /**
2
+ * The anonymous request an access rule is handed.
3
+ *
4
+ * `req.payload` is a stub that throws on use rather than a mock that answers:
5
+ * a read rule that looks something up to decide is one this cannot evaluate
6
+ * without a database, and it should say so here rather than pass on a guess.
7
+ */
8
+ export function anonymousAccessArgs() {
9
+ const payload = new Proxy({}, {
10
+ get(_target, property) {
11
+ // Not thenable: an `await req.payload` must not hang on a stub.
12
+ if (property === 'then' || typeof property === 'symbol')
13
+ return undefined;
14
+ return () => {
15
+ throw new Error(`An access rule called payload.${property} for an anonymous request. ` +
16
+ 'A rule that needs the database cannot be checked as a value; test it against one.');
17
+ };
18
+ },
19
+ });
20
+ return {
21
+ req: { user: null, headers: new Headers(), payload },
22
+ };
23
+ }
24
+ /**
25
+ * What a read rule answers an anonymous request: `true`, `false`, a `where`
26
+ * query, or `undefined` when there is no rule at all.
27
+ */
28
+ export async function anonymousRead(entity) {
29
+ const read = entity.access?.read;
30
+ if (typeof read !== 'function')
31
+ return undefined;
32
+ return read(anonymousAccessArgs());
33
+ }
34
+ /** Whether an entity keeps drafts, so an anonymous read must be narrowed to published. */
35
+ export function hasDrafts(entity) {
36
+ const versions = entity.versions;
37
+ return typeof versions === 'object' && versions !== null && Boolean(versions.drafts);
38
+ }
39
+ /**
40
+ * Why a render-path entity fails, or `undefined` when it passes.
41
+ *
42
+ * `undefined` is a failure here and not in `privateProblem`, deliberately. A
43
+ * collection with no `read` rule gets Payload's default, which requires a user
44
+ * — so leaving one off is the same outage as writing an editors-only rule, and
45
+ * reads in review as though no decision were needed.
46
+ */
47
+ export async function renderPathProblem(entity) {
48
+ const answer = await anonymousRead(entity);
49
+ if (answer === undefined) {
50
+ return {
51
+ kind: 'render-path-refused',
52
+ message: `${entity.slug} has no read rule, so Payload's default applies and an anonymous read is refused.`,
53
+ };
54
+ }
55
+ if (answer === false) {
56
+ return {
57
+ kind: 'render-path-refused',
58
+ message: `${entity.slug} refuses an anonymous read, so the render path sees nothing.`,
59
+ };
60
+ }
61
+ /*
62
+ A bare `true` on a collection with drafts serves an unpublished document to
63
+ anybody who guesses its slug. A query is what narrows it — usually
64
+ `{ _status: { equals: 'published' } }` — and which query is the site's call.
65
+ */
66
+ if (answer === true && hasDrafts(entity)) {
67
+ return {
68
+ kind: 'drafts-unfiltered',
69
+ message: `${entity.slug} keeps drafts and answers an anonymous read with \`true\`, so drafts reach the internet. Answer with a query that narrows to published documents.`,
70
+ };
71
+ }
72
+ return undefined;
73
+ }
74
+ /**
75
+ * Why a private entity fails, or `undefined` when it passes.
76
+ *
77
+ * `undefined` passes: Payload's default refuses an anonymous read, so a plugin
78
+ * collection with no rule of its own is already closed. What must not happen is
79
+ * `true`, or a query — a query is a successful read of some subset.
80
+ */
81
+ export async function privateProblem(entity) {
82
+ const answer = await anonymousRead(entity);
83
+ if (answer === true) {
84
+ return { kind: 'private-readable', message: `${entity.slug} is readable by anyone.` };
85
+ }
86
+ if (answer !== null && typeof answer === 'object') {
87
+ return {
88
+ kind: 'private-readable',
89
+ message: `${entity.slug} answers an anonymous read with a query, so it succeeds for whatever matches.`,
90
+ };
91
+ }
92
+ return undefined;
93
+ }
94
+ /** Slugs of a bucket, whichever shape it was written in. */
95
+ export function bucketSlugs(bucket) {
96
+ if (!bucket)
97
+ return [];
98
+ return Array.isArray(bucket) ? [...bucket] : Object.keys(bucket);
99
+ }
100
+ /**
101
+ * Every entity in exactly one bucket, and every bucket entry in the config.
102
+ */
103
+ export function classificationFindings(entity, inConfig, renderPath, closed) {
104
+ const findings = [];
105
+ const slugs = new Set(inConfig.map((item) => item.slug));
106
+ const classified = [...renderPath, ...closed];
107
+ const seen = new Set();
108
+ for (const slug of classified) {
109
+ if (seen.has(slug)) {
110
+ findings.push({
111
+ entity,
112
+ slug,
113
+ kind: 'classified-twice',
114
+ message: `${slug} is listed more than once. Each ${entity} belongs in exactly one bucket.`,
115
+ });
116
+ }
117
+ seen.add(slug);
118
+ }
119
+ for (const slug of slugs) {
120
+ if (!seen.has(slug)) {
121
+ findings.push({
122
+ entity,
123
+ slug,
124
+ kind: 'unclassified',
125
+ message: `${slug} is in the config and in no bucket. Decide whether a published page reads it ` +
126
+ `with nobody signed in (renderPath, with where) or the internet must not (private).`,
127
+ });
128
+ }
129
+ }
130
+ for (const slug of seen) {
131
+ if (!slugs.has(slug)) {
132
+ findings.push({
133
+ entity,
134
+ slug,
135
+ kind: 'not-in-config',
136
+ message: `${slug} is in a bucket and not in the config. Remove it, or fix the slug.`,
137
+ });
138
+ }
139
+ }
140
+ return findings;
141
+ }
142
+ /**
143
+ * Every finding for a config and its buckets, empty when it is sound.
144
+ *
145
+ * The pure form of `describeAnonymousAccess`, for a script or a test that
146
+ * wants the list rather than one test per collection.
147
+ */
148
+ export async function checkAnonymousAccess(config, buckets) {
149
+ const resolved = await config;
150
+ const findings = [];
151
+ const sides = [
152
+ {
153
+ entity: 'collection',
154
+ items: resolved.collections ?? [],
155
+ renderPath: bucketSlugs(buckets.renderPath),
156
+ closed: bucketSlugs(buckets.private),
157
+ },
158
+ ];
159
+ if (buckets.globals) {
160
+ sides.push({
161
+ entity: 'global',
162
+ items: resolved.globals ?? [],
163
+ renderPath: bucketSlugs(buckets.globals.renderPath),
164
+ closed: bucketSlugs(buckets.globals.private),
165
+ });
166
+ }
167
+ for (const side of sides) {
168
+ findings.push(...classificationFindings(side.entity, side.items, side.renderPath, side.closed));
169
+ const bySlug = new Map(side.items.map((item) => [item.slug, item]));
170
+ for (const slug of side.renderPath) {
171
+ const item = bySlug.get(slug);
172
+ const problem = item && (await renderPathProblem(item));
173
+ if (problem)
174
+ findings.push({ entity: side.entity, slug, ...problem });
175
+ }
176
+ for (const slug of side.closed) {
177
+ const item = bySlug.get(slug);
178
+ const problem = item && (await privateProblem(item));
179
+ if (problem)
180
+ findings.push({ entity: side.entity, slug, ...problem });
181
+ }
182
+ }
183
+ return findings;
184
+ }
185
+ //# sourceMappingURL=anonymousAccess.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"anonymousAccess.js","sourceRoot":"","sources":["../../src/testing/anonymousAccess.ts"],"names":[],"mappings":"AAsFA;;;;;;GAMG;AACH,MAAM,UAAU,mBAAmB;IACjC,MAAM,OAAO,GAAG,IAAI,KAAK,CACvB,EAAE,EACF;QACE,GAAG,CAAC,OAAO,EAAE,QAAQ;YACnB,gEAAgE;YAChE,IAAI,QAAQ,KAAK,MAAM,IAAI,OAAO,QAAQ,KAAK,QAAQ;gBAAE,OAAO,SAAS,CAAA;YACzE,OAAO,GAAG,EAAE;gBACV,MAAM,IAAI,KAAK,CACb,iCAAiC,QAAQ,6BAA6B;oBACpE,mFAAmF,CACtF,CAAA;YACH,CAAC,CAAA;QACH,CAAC;KACF,CACF,CAAA;IACD,OAAO;QACL,GAAG,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,OAAO,EAAE,EAAE,OAAO,EAAE;KACjB,CAAA;AACvC,CAAC;AAED;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,aAAa,CAAC,MAA2B;IAC7D,MAAM,IAAI,GAAG,MAAM,CAAC,MAAM,EAAE,IAAI,CAAA;IAChC,IAAI,OAAO,IAAI,KAAK,UAAU;QAAE,OAAO,SAAS,CAAA;IAChD,OAAQ,IAAe,CAAC,mBAAmB,EAAE,CAAC,CAAA;AAChD,CAAC;AAED,0FAA0F;AAC1F,MAAM,UAAU,SAAS,CAAC,MAA2B;IACnD,MAAM,QAAQ,GAAG,MAAM,CAAC,QAAsD,CAAA;IAC9E,OAAO,OAAO,QAAQ,KAAK,QAAQ,IAAI,QAAQ,KAAK,IAAI,IAAI,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAA;AACtF,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,CAAC,KAAK,UAAU,iBAAiB,CACrC,MAA2B;IAE3B,MAAM,MAAM,GAAG,MAAM,aAAa,CAAC,MAAM,CAAC,CAAA;IAC1C,IAAI,MAAM,KAAK,SAAS,EAAE,CAAC;QACzB,OAAO;YACL,IAAI,EAAE,qBAAqB;YAC3B,OAAO,EAAE,GAAG,MAAM,CAAC,IAAI,mFAAmF;SAC3G,CAAA;IACH,CAAC;IACD,IAAI,MAAM,KAAK,KAAK,EAAE,CAAC;QACrB,OAAO;YACL,IAAI,EAAE,qBAAqB;YAC3B,OAAO,EAAE,GAAG,MAAM,CAAC,IAAI,8DAA8D;SACtF,CAAA;IACH,CAAC;IACD;;;;MAIE;IACF,IAAI,MAAM,KAAK,IAAI,IAAI,SAAS,CAAC,MAAM,CAAC,EAAE,CAAC;QACzC,OAAO;YACL,IAAI,EAAE,mBAAmB;YACzB,OAAO,EAAE,GAAG,MAAM,CAAC,IAAI,mJAAmJ;SAC3K,CAAA;IACH,CAAC;IACD,OAAO,SAAS,CAAA;AAClB,CAAC;AAED;;;;;;GAMG;AACH,MAAM,CAAC,KAAK,UAAU,cAAc,CAClC,MAA2B;IAE3B,MAAM,MAAM,GAAG,MAAM,aAAa,CAAC,MAAM,CAAC,CAAA;IAC1C,IAAI,MAAM,KAAK,IAAI,EAAE,CAAC;QACpB,OAAO,EAAE,IAAI,EAAE,kBAAkB,EAAE,OAAO,EAAE,GAAG,MAAM,CAAC,IAAI,yBAAyB,EAAE,CAAA;IACvF,CAAC;IACD,IAAI,MAAM,KAAK,IAAI,IAAI,OAAO,MAAM,KAAK,QAAQ,EAAE,CAAC;QAClD,OAAO;YACL,IAAI,EAAE,kBAAkB;YACxB,OAAO,EAAE,GAAG,MAAM,CAAC,IAAI,+EAA+E;SACvG,CAAA;IACH,CAAC;IACD,OAAO,SAAS,CAAA;AAClB,CAAC;AAED,4DAA4D;AAC5D,MAAM,UAAU,WAAW,CACzB,MAAwE;IAExE,IAAI,CAAC,MAAM;QAAE,OAAO,EAAE,CAAA;IACtB,OAAO,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,GAAI,MAA4B,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,CAAA;AACzF,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,sBAAsB,CACpC,MAA+B,EAC/B,QAAwC,EACxC,UAA6B,EAC7B,MAAyB;IAEzB,MAAM,QAAQ,GAAoB,EAAE,CAAA;IACpC,MAAM,KAAK,GAAG,IAAI,GAAG,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAA;IACxD,MAAM,UAAU,GAAG,CAAC,GAAG,UAAU,EAAE,GAAG,MAAM,CAAC,CAAA;IAE7C,MAAM,IAAI,GAAG,IAAI,GAAG,EAAU,CAAA;IAC9B,KAAK,MAAM,IAAI,IAAI,UAAU,EAAE,CAAC;QAC9B,IAAI,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC;YACnB,QAAQ,CAAC,IAAI,CAAC;gBACZ,MAAM;gBACN,IAAI;gBACJ,IAAI,EAAE,kBAAkB;gBACxB,OAAO,EAAE,GAAG,IAAI,mCAAmC,MAAM,iCAAiC;aAC3F,CAAC,CAAA;QACJ,CAAC;QACD,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,CAAA;IAChB,CAAC;IAED,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC;YACpB,QAAQ,CAAC,IAAI,CAAC;gBACZ,MAAM;gBACN,IAAI;gBACJ,IAAI,EAAE,cAAc;gBACpB,OAAO,EACL,GAAG,IAAI,+EAA+E;oBACtF,oFAAoF;aACvF,CAAC,CAAA;QACJ,CAAC;IACH,CAAC;IAED,KAAK,MAAM,IAAI,IAAI,IAAI,EAAE,CAAC;QACxB,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC;YACrB,QAAQ,CAAC,IAAI,CAAC;gBACZ,MAAM;gBACN,IAAI;gBACJ,IAAI,EAAE,eAAe;gBACrB,OAAO,EAAE,GAAG,IAAI,oEAAoE;aACrF,CAAC,CAAA;QACJ,CAAC;IACH,CAAC;IAED,OAAO,QAAQ,CAAA;AACjB,CAAC;AAKD;;;;;GAKG;AACH,MAAM,CAAC,KAAK,UAAU,oBAAoB,CACxC,MAAmB,EACnB,OAA+B;IAE/B,MAAM,QAAQ,GAAG,MAAM,MAAM,CAAA;IAC7B,MAAM,QAAQ,GAAoB,EAAE,CAAA;IAEpC,MAAM,KAAK,GAKN;QACH;YACE,MAAM,EAAE,YAAY;YACpB,KAAK,EAAE,QAAQ,CAAC,WAAW,IAAI,EAAE;YACjC,UAAU,EAAE,WAAW,CAAC,OAAO,CAAC,UAAU,CAAC;YAC3C,MAAM,EAAE,WAAW,CAAC,OAAO,CAAC,OAAO,CAAC;SACrC;KACF,CAAA;IACD,IAAI,OAAO,CAAC,OAAO,EAAE,CAAC;QACpB,KAAK,CAAC,IAAI,CAAC;YACT,MAAM,EAAE,QAAQ;YAChB,KAAK,EAAE,QAAQ,CAAC,OAAO,IAAI,EAAE;YAC7B,UAAU,EAAE,WAAW,CAAC,OAAO,CAAC,OAAO,CAAC,UAAU,CAAC;YACnD,MAAM,EAAE,WAAW,CAAC,OAAO,CAAC,OAAO,CAAC,OAAO,CAAC;SAC7C,CAAC,CAAA;IACJ,CAAC;IAED,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,QAAQ,CAAC,IAAI,CAAC,GAAG,sBAAsB,CAAC,IAAI,CAAC,MAAM,EAAE,IAAI,CAAC,KAAK,EAAE,IAAI,CAAC,UAAU,EAAE,IAAI,CAAC,MAAM,CAAC,CAAC,CAAA;QAC/F,MAAM,MAAM,GAAG,IAAI,GAAG,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,IAAI,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC,CAAC,CAAA;QAEnE,KAAK,MAAM,IAAI,IAAI,IAAI,CAAC,UAAU,EAAE,CAAC;YACnC,MAAM,IAAI,GAAG,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,CAAA;YAC7B,MAAM,OAAO,GAAG,IAAI,IAAI,CAAC,MAAM,iBAAiB,CAAC,IAAI,CAAC,CAAC,CAAA;YACvD,IAAI,OAAO;gBAAE,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,IAAI,CAAC,MAAM,EAAE,IAAI,EAAE,GAAG,OAAO,EAAE,CAAC,CAAA;QACvE,CAAC;QACD,KAAK,MAAM,IAAI,IAAI,IAAI,CAAC,MAAM,EAAE,CAAC;YAC/B,MAAM,IAAI,GAAG,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,CAAA;YAC7B,MAAM,OAAO,GAAG,IAAI,IAAI,CAAC,MAAM,cAAc,CAAC,IAAI,CAAC,CAAC,CAAA;YACpD,IAAI,OAAO;gBAAE,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,IAAI,CAAC,MAAM,EAAE,IAAI,EAAE,GAAG,OAAO,EAAE,CAAC,CAAA;QACvE,CAAC;IACH,CAAC;IAED,OAAO,QAAQ,CAAA;AACjB,CAAC"}
@@ -0,0 +1,26 @@
1
+ import { type AnonymousAccessBuckets, type ConfigInput } from './anonymousAccess.js';
2
+ export interface DescribeAnonymousAccessOptions {
3
+ /** The suite's name. Defaults to "anonymous access". */
4
+ title?: string;
5
+ }
6
+ /**
7
+ * Registers a vitest suite asserting what an anonymous request may read.
8
+ *
9
+ * Call it at the top level of a test file with the site's `payload.config.ts`
10
+ * and its bucket map:
11
+ *
12
+ * ```ts
13
+ * import config from '../payload.config'
14
+ * describeAnonymousAccess(config, {
15
+ * renderPath: { pages: 'the [...slug] route', media: 'every populated upload' },
16
+ * private: ['users', 'audit-events', 'payload-preferences'],
17
+ * })
18
+ * ```
19
+ *
20
+ * One test classifies every collection (and every global, when `globals` is
21
+ * given) into exactly one bucket; then one test per entry checks the rule
22
+ * itself, so a failure names the collection. No database: the config is
23
+ * imported, never connected.
24
+ */
25
+ export declare function describeAnonymousAccess(config: ConfigInput, buckets: AnonymousAccessBuckets, options?: DescribeAnonymousAccessOptions): void;
26
+ //# sourceMappingURL=describeAnonymousAccess.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"describeAnonymousAccess.d.ts","sourceRoot":"","sources":["../../src/testing/describeAnonymousAccess.ts"],"names":[],"mappings":"AACA,OAAO,EAOL,KAAK,sBAAsB,EAC3B,KAAK,WAAW,EACjB,MAAM,sBAAsB,CAAA;AAE7B,MAAM,WAAW,8BAA8B;IAC7C,wDAAwD;IACxD,KAAK,CAAC,EAAE,MAAM,CAAA;CACf;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,uBAAuB,CACrC,MAAM,EAAE,WAAW,EACnB,OAAO,EAAE,sBAAsB,EAC/B,OAAO,GAAE,8BAAmC,GAC3C,IAAI,CAqEN"}
@@ -0,0 +1,76 @@
1
+ import { beforeAll, describe, expect, it } from 'vitest';
2
+ import { bucketSlugs, classificationFindings, privateProblem, renderPathProblem, } from './anonymousAccess.js';
3
+ /**
4
+ * Registers a vitest suite asserting what an anonymous request may read.
5
+ *
6
+ * Call it at the top level of a test file with the site's `payload.config.ts`
7
+ * and its bucket map:
8
+ *
9
+ * ```ts
10
+ * import config from '../payload.config'
11
+ * describeAnonymousAccess(config, {
12
+ * renderPath: { pages: 'the [...slug] route', media: 'every populated upload' },
13
+ * private: ['users', 'audit-events', 'payload-preferences'],
14
+ * })
15
+ * ```
16
+ *
17
+ * One test classifies every collection (and every global, when `globals` is
18
+ * given) into exactly one bucket; then one test per entry checks the rule
19
+ * itself, so a failure names the collection. No database: the config is
20
+ * imported, never connected.
21
+ */
22
+ export function describeAnonymousAccess(config, buckets, options = {}) {
23
+ describe(options.title ?? 'anonymous access', () => {
24
+ let resolved;
25
+ beforeAll(async () => {
26
+ // `buildConfig` is async, and awaiting it is the only way to see the
27
+ // collections the plugins add on top of the site's own.
28
+ resolved = await config;
29
+ });
30
+ const find = (entity, slug) => {
31
+ const items = (entity === 'collection' ? resolved.collections : resolved.globals) ?? [];
32
+ const item = items.find((candidate) => candidate.slug === slug);
33
+ // Reported by the classification test as `not-in-config`; failing here
34
+ // too keeps this entry's own result honest.
35
+ expect(item, `${slug} is not in the config`).toBeDefined();
36
+ return item;
37
+ };
38
+ const sides = [{ entity: 'collection', renderPath: buckets.renderPath, closed: buckets.private }];
39
+ if (buckets.globals) {
40
+ sides.push({
41
+ entity: 'global',
42
+ renderPath: buckets.globals.renderPath ?? {},
43
+ closed: buckets.globals.private,
44
+ });
45
+ }
46
+ for (const side of sides) {
47
+ const plural = `${side.entity}s`;
48
+ it(`puts every ${side.entity} in exactly one bucket`, () => {
49
+ const items = (side.entity === 'collection' ? resolved.collections : resolved.globals) ?? [];
50
+ // A config with nothing in it makes every assertion below vacuous.
51
+ expect(items.length, `the config has no ${plural}`).toBeGreaterThan(0);
52
+ const findings = classificationFindings(side.entity, items, Object.keys(side.renderPath), bucketSlugs(side.closed));
53
+ expect(findings.map((finding) => finding.message)).toEqual([]);
54
+ });
55
+ const renderEntries = Object.entries(side.renderPath);
56
+ if (renderEntries.length > 0) {
57
+ describe(`${plural} the render path reads`, () => {
58
+ it.each(renderEntries)('%s — read by %s', async (slug) => {
59
+ const problem = await renderPathProblem(find(side.entity, slug));
60
+ expect(problem?.message).toBeUndefined();
61
+ });
62
+ });
63
+ }
64
+ const closedSlugs = bucketSlugs(side.closed);
65
+ if (closedSlugs.length > 0) {
66
+ describe(`${plural} the internet must not read`, () => {
67
+ it.each(closedSlugs)('%s refuses an anonymous read', async (slug) => {
68
+ const problem = await privateProblem(find(side.entity, slug));
69
+ expect(problem?.message).toBeUndefined();
70
+ });
71
+ });
72
+ }
73
+ }
74
+ });
75
+ }
76
+ //# sourceMappingURL=describeAnonymousAccess.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"describeAnonymousAccess.js","sourceRoot":"","sources":["../../src/testing/describeAnonymousAccess.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Test helpers for a Throughline site, as `@forumone/throughline-core/testing`.
3
+ *
4
+ * Kept off the package root — which re-exports every other subpath — because
5
+ * these import `vitest`, an optional peer that a running site never loads.
6
+ */
7
+ export { anonymousAccessArgs, anonymousRead, checkAnonymousAccess, hasDrafts, } from './anonymousAccess.js';
8
+ export type { AccessCheckedConfig, AccessCheckedEntity, AccessFinding, AccessFindingKind, AnonymousAccessBuckets, ConfigInput, } from './anonymousAccess.js';
9
+ export { describeAnonymousAccess } from './describeAnonymousAccess.js';
10
+ export type { DescribeAnonymousAccessOptions } from './describeAnonymousAccess.js';
11
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/testing/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,OAAO,EACL,mBAAmB,EACnB,aAAa,EACb,oBAAoB,EACpB,SAAS,GACV,MAAM,sBAAsB,CAAA;AAC7B,YAAY,EACV,mBAAmB,EACnB,mBAAmB,EACnB,aAAa,EACb,iBAAiB,EACjB,sBAAsB,EACtB,WAAW,GACZ,MAAM,sBAAsB,CAAA;AAC7B,OAAO,EAAE,uBAAuB,EAAE,MAAM,8BAA8B,CAAA;AACtE,YAAY,EAAE,8BAA8B,EAAE,MAAM,8BAA8B,CAAA"}
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Test helpers for a Throughline site, as `@forumone/throughline-core/testing`.
3
+ *
4
+ * Kept off the package root — which re-exports every other subpath — because
5
+ * these import `vitest`, an optional peer that a running site never loads.
6
+ */
7
+ export { anonymousAccessArgs, anonymousRead, checkAnonymousAccess, hasDrafts, } from './anonymousAccess.js';
8
+ export { describeAnonymousAccess } from './describeAnonymousAccess.js';
9
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/testing/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,OAAO,EACL,mBAAmB,EACnB,aAAa,EACb,oBAAoB,EACpB,SAAS,GACV,MAAM,sBAAsB,CAAA;AAS7B,OAAO,EAAE,uBAAuB,EAAE,MAAM,8BAA8B,CAAA"}
package/package.json CHANGED
@@ -1,10 +1,13 @@
1
1
  {
2
2
  "name": "@forumone/throughline-core",
3
- "version": "0.9.1",
3
+ "version": "0.10.0",
4
4
  "description": "Core plumbing for Throughline: audit log, MCP authentication and handler, event taxonomy + Inngest client factory, logger, shared utilities.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
7
7
  "types": "./dist/index.d.ts",
8
+ "bin": {
9
+ "throughline-payload": "./bin/throughline-payload.mjs"
10
+ },
8
11
  "exports": {
9
12
  ".": {
10
13
  "types": "./dist/index.d.ts",
@@ -18,6 +21,10 @@
18
21
  "types": "./dist/auth/index.d.ts",
19
22
  "default": "./dist/auth/index.js"
20
23
  },
24
+ "./env": {
25
+ "types": "./dist/env/index.d.ts",
26
+ "default": "./dist/env/index.js"
27
+ },
21
28
  "./events": {
22
29
  "types": "./dist/events/index.d.ts",
23
30
  "default": "./dist/events/index.js"
@@ -25,10 +32,19 @@
25
32
  "./mcp": {
26
33
  "types": "./dist/mcp/index.d.ts",
27
34
  "default": "./dist/mcp/index.js"
35
+ },
36
+ "./observability": {
37
+ "types": "./dist/observability/index.d.ts",
38
+ "default": "./dist/observability/index.js"
39
+ },
40
+ "./testing": {
41
+ "types": "./dist/testing/index.d.ts",
42
+ "default": "./dist/testing/index.js"
28
43
  }
29
44
  },
30
45
  "files": [
31
46
  "dist",
47
+ "bin/*.mjs",
32
48
  "README.md",
33
49
  "CHANGELOG.md"
34
50
  ],
@@ -36,7 +52,8 @@
36
52
  "throughline",
37
53
  "payload",
38
54
  "mcp",
39
- "audit"
55
+ "audit",
56
+ "cli"
40
57
  ],
41
58
  "license": "MIT",
42
59
  "repository": {
@@ -52,17 +69,23 @@
52
69
  "access": "public"
53
70
  },
54
71
  "peerDependencies": {
55
- "payload": "^3.0.0",
56
- "inngest": "^4.0.0"
72
+ "payload": "^3.89.0",
73
+ "inngest": "^4.0.0",
74
+ "vitest": "^3.0.0 || ^4.0.0"
75
+ },
76
+ "peerDependenciesMeta": {
77
+ "vitest": {
78
+ "optional": true
79
+ }
57
80
  },
58
81
  "dependencies": {
59
82
  "zod": "^3.23.0",
60
83
  "zod-to-json-schema": "^3.25.2",
61
- "@forumone/throughline-plugin-contract": "0.4.1"
84
+ "@forumone/throughline-plugin-contract": "0.5.0"
62
85
  },
63
86
  "devDependencies": {
64
87
  "@types/node": "^24.13.2",
65
- "eslint": "^9.15.0",
88
+ "eslint": "^10.11.0",
66
89
  "inngest": "^4.2.0",
67
90
  "payload": "^3.83.0",
68
91
  "typescript": "^5.6.0",
@@ -75,7 +98,7 @@
75
98
  "dev": "tsc -b -w",
76
99
  "clean": "rm -rf dist .turbo *.tsbuildinfo",
77
100
  "typecheck": "tsc --noEmit",
78
- "lint": "eslint src",
101
+ "lint": "eslint src bin",
79
102
  "test": "vitest run"
80
103
  }
81
104
  }