@forumone/throughline-publishing 0.0.1 → 0.2.1
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/CHANGELOG.md +13 -0
- package/LICENSE +21 -0
- package/README.md +2 -42
- package/dist/checks/alt-text.d.ts +11 -0
- package/dist/checks/alt-text.d.ts.map +1 -0
- package/dist/checks/alt-text.js +53 -0
- package/dist/checks/alt-text.js.map +1 -0
- package/dist/checks/heading-hierarchy.d.ts +11 -0
- package/dist/checks/heading-hierarchy.d.ts.map +1 -0
- package/dist/checks/heading-hierarchy.js +32 -0
- package/dist/checks/heading-hierarchy.js.map +1 -0
- package/dist/checks/index.d.ts +6 -0
- package/dist/checks/index.d.ts.map +1 -0
- package/dist/checks/index.js +13 -0
- package/dist/checks/index.js.map +1 -0
- package/dist/checks/link-labels.d.ts +9 -0
- package/dist/checks/link-labels.d.ts.map +1 -0
- package/dist/checks/link-labels.js +44 -0
- package/dist/checks/link-labels.js.map +1 -0
- package/dist/hooks/block-status-writes.d.ts +12 -0
- package/dist/hooks/block-status-writes.d.ts.map +1 -0
- package/dist/hooks/block-status-writes.js +36 -0
- package/dist/hooks/block-status-writes.js.map +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -0
- package/dist/options.d.ts +65 -0
- package/dist/options.d.ts.map +1 -0
- package/dist/options.js +57 -0
- package/dist/options.js.map +1 -0
- package/dist/pipeline/index.d.ts +15 -0
- package/dist/pipeline/index.d.ts.map +1 -0
- package/dist/pipeline/index.js +62 -0
- package/dist/pipeline/index.js.map +1 -0
- package/dist/pipeline/steps/accessibility.d.ts +9 -0
- package/dist/pipeline/steps/accessibility.d.ts.map +1 -0
- package/dist/pipeline/steps/accessibility.js +32 -0
- package/dist/pipeline/steps/accessibility.js.map +1 -0
- package/dist/pipeline/steps/approval.d.ts +21 -0
- package/dist/pipeline/steps/approval.d.ts.map +1 -0
- package/dist/pipeline/steps/approval.js +48 -0
- package/dist/pipeline/steps/approval.js.map +1 -0
- package/dist/pipeline/steps/composition.d.ts +14 -0
- package/dist/pipeline/steps/composition.d.ts.map +1 -0
- package/dist/pipeline/steps/composition.js +63 -0
- package/dist/pipeline/steps/composition.js.map +1 -0
- package/dist/pipeline/steps/embargo.d.ts +12 -0
- package/dist/pipeline/steps/embargo.d.ts.map +1 -0
- package/dist/pipeline/steps/embargo.js +41 -0
- package/dist/pipeline/steps/embargo.js.map +1 -0
- package/dist/pipeline/steps/execute.d.ts +12 -0
- package/dist/pipeline/steps/execute.d.ts.map +1 -0
- package/dist/pipeline/steps/execute.js +38 -0
- package/dist/pipeline/steps/execute.js.map +1 -0
- package/dist/pipeline/steps/exist.d.ts +9 -0
- package/dist/pipeline/steps/exist.d.ts.map +1 -0
- package/dist/pipeline/steps/exist.js +31 -0
- package/dist/pipeline/steps/exist.js.map +1 -0
- package/dist/pipeline/steps/required-fields.d.ts +9 -0
- package/dist/pipeline/steps/required-fields.d.ts.map +1 -0
- package/dist/pipeline/steps/required-fields.js +63 -0
- package/dist/pipeline/steps/required-fields.js.map +1 -0
- package/dist/pipeline/types.d.ts +45 -0
- package/dist/pipeline/types.d.ts.map +1 -0
- package/dist/pipeline/types.js +2 -0
- package/dist/pipeline/types.js.map +1 -0
- package/dist/plugin.d.ts +4 -0
- package/dist/plugin.d.ts.map +1 -0
- package/dist/plugin.js +87 -0
- package/dist/plugin.js.map +1 -0
- package/dist/tools/get-publish-status.d.ts +9 -0
- package/dist/tools/get-publish-status.d.ts.map +1 -0
- package/dist/tools/get-publish-status.js +55 -0
- package/dist/tools/get-publish-status.js.map +1 -0
- package/dist/tools/index.d.ts +6 -0
- package/dist/tools/index.d.ts.map +1 -0
- package/dist/tools/index.js +6 -0
- package/dist/tools/index.js.map +1 -0
- package/dist/tools/publish.d.ts +11 -0
- package/dist/tools/publish.d.ts.map +1 -0
- package/dist/tools/publish.js +71 -0
- package/dist/tools/publish.js.map +1 -0
- package/dist/tools/rollback.d.ts +11 -0
- package/dist/tools/rollback.d.ts.map +1 -0
- package/dist/tools/rollback.js +78 -0
- package/dist/tools/rollback.js.map +1 -0
- package/dist/tools/schedule-publish.d.ts +11 -0
- package/dist/tools/schedule-publish.d.ts.map +1 -0
- package/dist/tools/schedule-publish.js +94 -0
- package/dist/tools/schedule-publish.js.map +1 -0
- package/dist/tools/unpublish.d.ts +11 -0
- package/dist/tools/unpublish.d.ts.map +1 -0
- package/dist/tools/unpublish.js +61 -0
- package/dist/tools/unpublish.js.map +1 -0
- package/package.json +66 -7
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# @forumone/throughline-publishing
|
|
2
|
+
|
|
3
|
+
## 0.2.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 3ef6f6a: The pipeline's `approvalStep` now falls back to looking up an approval resolver on the Payload instance under `Symbol.for('@forumone/throughline/approvals-resolver')` when no resolver is supplied via `publishingPlugin`'s `options.approvalResolver`. The `@forumone/throughline-approvals` plugin attaches its resolver under that symbol automatically, so adding approvals to a config no longer requires re-wiring the publishing plugin's options. An explicit `options.approvalResolver` still takes precedence when you need to override.
|
|
8
|
+
|
|
9
|
+
## 0.2.0
|
|
10
|
+
|
|
11
|
+
### Minor Changes
|
|
12
|
+
|
|
13
|
+
- [`123d2ea`](https://github.com/forumone/throughline/commit/123d2ea0172d9495b9e2c8e8c6039e623f5fba66) Thanks [@briangraves](https://github.com/briangraves)! - Initial release. Policy-gated publishing server with a seven-step pipeline (exist, composition, accessibility, required-fields, embargo, approval, execute), five MCP tools (publish, unpublish, schedule_publish, get_publish_status, rollback), and a Payload `beforeChange` hook on every publishable collection that rejects direct `_status` writes from anywhere other than the pipeline's execute step. Built-in accessibility checks cover alt text, heading hierarchy, and link labels; clients add custom checks via the `accessibilityChecks` option.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Forum One Communications Corporation
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,45 +1,5 @@
|
|
|
1
1
|
# @forumone/throughline-publishing
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Policy-gated publishing server for Throughline. The trust boundary that decides what's allowed to ship.
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
This is **NOT** a functional package and contains **NO** code or functionality beyond the OIDC setup configuration.
|
|
8
|
-
|
|
9
|
-
## Purpose
|
|
10
|
-
|
|
11
|
-
This package exists to:
|
|
12
|
-
1. Configure OIDC trusted publishing for the package name `@forumone/throughline-publishing`
|
|
13
|
-
2. Enable secure, token-less publishing from CI/CD workflows
|
|
14
|
-
3. Establish provenance for packages published under this name
|
|
15
|
-
|
|
16
|
-
## What is OIDC Trusted Publishing?
|
|
17
|
-
|
|
18
|
-
OIDC trusted publishing allows package maintainers to publish packages directly from their CI/CD workflows without needing to manage npm access tokens. Instead, it uses OpenID Connect to establish trust between the CI/CD provider (like GitHub Actions) and npm.
|
|
19
|
-
|
|
20
|
-
## Setup Instructions
|
|
21
|
-
|
|
22
|
-
To properly configure OIDC trusted publishing for this package:
|
|
23
|
-
|
|
24
|
-
1. Go to [npmjs.com](https://www.npmjs.com/) and navigate to your package settings
|
|
25
|
-
2. Configure the trusted publisher (e.g., GitHub Actions)
|
|
26
|
-
3. Specify the repository and workflow that should be allowed to publish
|
|
27
|
-
4. Use the configured workflow to publish your actual package
|
|
28
|
-
|
|
29
|
-
## DO NOT USE THIS PACKAGE
|
|
30
|
-
|
|
31
|
-
This package is a placeholder for OIDC configuration only. It:
|
|
32
|
-
- Contains no executable code
|
|
33
|
-
- Provides no functionality
|
|
34
|
-
- Should not be installed as a dependency
|
|
35
|
-
- Exists only for administrative purposes
|
|
36
|
-
|
|
37
|
-
## More Information
|
|
38
|
-
|
|
39
|
-
For more details about npm's trusted publishing feature, see:
|
|
40
|
-
- [npm Trusted Publishing Documentation](https://docs.npmjs.com/generating-provenance-statements)
|
|
41
|
-
- [GitHub Actions OIDC Documentation](https://docs.github.com/en/actions/deployment/security-hardening-your-deployments/about-security-hardening-with-openid-connect)
|
|
42
|
-
|
|
43
|
-
---
|
|
44
|
-
|
|
45
|
-
**Maintained for OIDC setup purposes only**
|
|
5
|
+
_Authoring guide and full API reference land in a later commit._
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { AccessibilityCheck } from '../options.js';
|
|
2
|
+
/**
|
|
3
|
+
* Walks the document tree looking for image-shaped objects (anything with
|
|
4
|
+
* a `url` or `filename` property whose `mimeType`, if present, starts with
|
|
5
|
+
* `image/`). Flags any whose `alt` is missing or empty.
|
|
6
|
+
*
|
|
7
|
+
* Heuristic, not exhaustive — clients with non-standard image shapes
|
|
8
|
+
* register their own check via `accessibilityChecks`.
|
|
9
|
+
*/
|
|
10
|
+
export declare const altTextCheck: AccessibilityCheck;
|
|
11
|
+
//# sourceMappingURL=alt-text.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"alt-text.d.ts","sourceRoot":"","sources":["../../src/checks/alt-text.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAsB,MAAM,eAAe,CAAA;AAE3E;;;;;;;GAOG;AACH,eAAO,MAAM,YAAY,EAAE,kBAgB1B,CAAA"}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Walks the document tree looking for image-shaped objects (anything with
|
|
3
|
+
* a `url` or `filename` property whose `mimeType`, if present, starts with
|
|
4
|
+
* `image/`). Flags any whose `alt` is missing or empty.
|
|
5
|
+
*
|
|
6
|
+
* Heuristic, not exhaustive — clients with non-standard image shapes
|
|
7
|
+
* register their own check via `accessibilityChecks`.
|
|
8
|
+
*/
|
|
9
|
+
export const altTextCheck = {
|
|
10
|
+
name: 'alt-text',
|
|
11
|
+
run(doc) {
|
|
12
|
+
const issues = [];
|
|
13
|
+
walkForImages(doc, (image, path) => {
|
|
14
|
+
const alt = image['alt'];
|
|
15
|
+
if (typeof alt !== 'string' || alt.trim() === '') {
|
|
16
|
+
issues.push({
|
|
17
|
+
field: path,
|
|
18
|
+
message: `Image at "${path}" is missing alt text`,
|
|
19
|
+
severity: 'error',
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
});
|
|
23
|
+
return issues;
|
|
24
|
+
},
|
|
25
|
+
};
|
|
26
|
+
function walkForImages(value, visit, path = '') {
|
|
27
|
+
if (!value || typeof value !== 'object')
|
|
28
|
+
return;
|
|
29
|
+
if (Array.isArray(value)) {
|
|
30
|
+
for (let i = 0; i < value.length; i++) {
|
|
31
|
+
walkForImages(value[i], visit, `${path}[${i}]`);
|
|
32
|
+
}
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
const obj = value;
|
|
36
|
+
if (looksLikeImage(obj)) {
|
|
37
|
+
visit(obj, path || '(root)');
|
|
38
|
+
}
|
|
39
|
+
for (const key of Object.keys(obj)) {
|
|
40
|
+
walkForImages(obj[key], visit, path ? `${path}.${key}` : key);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
function looksLikeImage(obj) {
|
|
44
|
+
// Require an explicit image marker: a `filename` (Payload media uploads
|
|
45
|
+
// always have one) or an `image/*` mimeType. A bare `{ url }` is too
|
|
46
|
+
// ambiguous — it's just as likely to be a link, so we leave that to the
|
|
47
|
+
// link-labels check.
|
|
48
|
+
const mime = obj['mimeType'];
|
|
49
|
+
if (typeof mime === 'string')
|
|
50
|
+
return mime.startsWith('image/');
|
|
51
|
+
return typeof obj['filename'] === 'string';
|
|
52
|
+
}
|
|
53
|
+
//# sourceMappingURL=alt-text.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"alt-text.js","sourceRoot":"","sources":["../../src/checks/alt-text.ts"],"names":[],"mappings":"AAEA;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,YAAY,GAAuB;IAC9C,IAAI,EAAE,UAAU;IAChB,GAAG,CAAC,GAAG;QACL,MAAM,MAAM,GAAyB,EAAE,CAAA;QACvC,aAAa,CAAC,GAAG,EAAE,CAAC,KAAK,EAAE,IAAI,EAAE,EAAE;YACjC,MAAM,GAAG,GAAG,KAAK,CAAC,KAAK,CAAC,CAAA;YACxB,IAAI,OAAO,GAAG,KAAK,QAAQ,IAAI,GAAG,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE,CAAC;gBACjD,MAAM,CAAC,IAAI,CAAC;oBACV,KAAK,EAAE,IAAI;oBACX,OAAO,EAAE,aAAa,IAAI,uBAAuB;oBACjD,QAAQ,EAAE,OAAO;iBAClB,CAAC,CAAA;YACJ,CAAC;QACH,CAAC,CAAC,CAAA;QACF,OAAO,MAAM,CAAA;IACf,CAAC;CACF,CAAA;AAED,SAAS,aAAa,CACpB,KAAc,EACd,KAA6D,EAC7D,IAAI,GAAG,EAAE;IAET,IAAI,CAAC,KAAK,IAAI,OAAO,KAAK,KAAK,QAAQ;QAAE,OAAM;IAE/C,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC;QACzB,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,KAAK,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;YACtC,aAAa,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,GAAG,IAAI,IAAI,CAAC,GAAG,CAAC,CAAA;QACjD,CAAC;QACD,OAAM;IACR,CAAC;IAED,MAAM,GAAG,GAAG,KAAgC,CAAA;IAC5C,IAAI,cAAc,CAAC,GAAG,CAAC,EAAE,CAAC;QACxB,KAAK,CAAC,GAAG,EAAE,IAAI,IAAI,QAAQ,CAAC,CAAA;IAC9B,CAAC;IAED,KAAK,MAAM,GAAG,IAAI,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC;QACnC,aAAa,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,KAAK,EAAE,IAAI,CAAC,CAAC,CAAC,GAAG,IAAI,IAAI,GAAG,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,CAAA;IAC/D,CAAC;AACH,CAAC;AAED,SAAS,cAAc,CAAC,GAA4B;IAClD,wEAAwE;IACxE,qEAAqE;IACrE,wEAAwE;IACxE,qBAAqB;IACrB,MAAM,IAAI,GAAG,GAAG,CAAC,UAAU,CAAC,CAAA;IAC5B,IAAI,OAAO,IAAI,KAAK,QAAQ;QAAE,OAAO,IAAI,CAAC,UAAU,CAAC,QAAQ,CAAC,CAAA;IAC9D,OAAO,OAAO,GAAG,CAAC,UAAU,CAAC,KAAK,QAAQ,CAAA;AAC5C,CAAC"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { AccessibilityCheck } from '../options.js';
|
|
2
|
+
/**
|
|
3
|
+
* Structural heading-hierarchy check. Without rendering the page we can't
|
|
4
|
+
* fully audit heading levels, but we can flag the most common structural
|
|
5
|
+
* mistake: multiple Hero blocks (each becomes an h1 in the reference DS).
|
|
6
|
+
*
|
|
7
|
+
* Block types are matched case-insensitively against `'hero'` since the
|
|
8
|
+
* `blockType` field is by convention lower-cased in Payload.
|
|
9
|
+
*/
|
|
10
|
+
export declare const headingHierarchyCheck: AccessibilityCheck;
|
|
11
|
+
//# sourceMappingURL=heading-hierarchy.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"heading-hierarchy.d.ts","sourceRoot":"","sources":["../../src/checks/heading-hierarchy.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAsB,MAAM,eAAe,CAAA;AAE3E;;;;;;;GAOG;AACH,eAAO,MAAM,qBAAqB,EAAE,kBAuBnC,CAAA"}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Structural heading-hierarchy check. Without rendering the page we can't
|
|
3
|
+
* fully audit heading levels, but we can flag the most common structural
|
|
4
|
+
* mistake: multiple Hero blocks (each becomes an h1 in the reference DS).
|
|
5
|
+
*
|
|
6
|
+
* Block types are matched case-insensitively against `'hero'` since the
|
|
7
|
+
* `blockType` field is by convention lower-cased in Payload.
|
|
8
|
+
*/
|
|
9
|
+
export const headingHierarchyCheck = {
|
|
10
|
+
name: 'heading-hierarchy',
|
|
11
|
+
run(doc, collection) {
|
|
12
|
+
const issues = [];
|
|
13
|
+
const layoutValue = doc[collection.layoutField];
|
|
14
|
+
if (!Array.isArray(layoutValue))
|
|
15
|
+
return issues;
|
|
16
|
+
const heroCount = layoutValue.filter((block) => {
|
|
17
|
+
if (!block || typeof block !== 'object')
|
|
18
|
+
return false;
|
|
19
|
+
const blockType = block['blockType'];
|
|
20
|
+
return typeof blockType === 'string' && blockType.toLowerCase() === 'hero';
|
|
21
|
+
}).length;
|
|
22
|
+
if (heroCount > 1) {
|
|
23
|
+
issues.push({
|
|
24
|
+
field: collection.layoutField,
|
|
25
|
+
message: `Page has ${heroCount} Hero blocks but should have exactly one (each Hero becomes an h1)`,
|
|
26
|
+
severity: 'error',
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
return issues;
|
|
30
|
+
},
|
|
31
|
+
};
|
|
32
|
+
//# sourceMappingURL=heading-hierarchy.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"heading-hierarchy.js","sourceRoot":"","sources":["../../src/checks/heading-hierarchy.ts"],"names":[],"mappings":"AAEA;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,qBAAqB,GAAuB;IACvD,IAAI,EAAE,mBAAmB;IACzB,GAAG,CAAC,GAAG,EAAE,UAAU;QACjB,MAAM,MAAM,GAAyB,EAAE,CAAA;QACvC,MAAM,WAAW,GAAG,GAAG,CAAC,UAAU,CAAC,WAAW,CAAC,CAAA;QAC/C,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,WAAW,CAAC;YAAE,OAAO,MAAM,CAAA;QAE9C,MAAM,SAAS,GAAG,WAAW,CAAC,MAAM,CAAC,CAAC,KAAK,EAAE,EAAE;YAC7C,IAAI,CAAC,KAAK,IAAI,OAAO,KAAK,KAAK,QAAQ;gBAAE,OAAO,KAAK,CAAA;YACrD,MAAM,SAAS,GAAI,KAAiC,CAAC,WAAW,CAAC,CAAA;YACjE,OAAO,OAAO,SAAS,KAAK,QAAQ,IAAI,SAAS,CAAC,WAAW,EAAE,KAAK,MAAM,CAAA;QAC5E,CAAC,CAAC,CAAC,MAAM,CAAA;QAET,IAAI,SAAS,GAAG,CAAC,EAAE,CAAC;YAClB,MAAM,CAAC,IAAI,CAAC;gBACV,KAAK,EAAE,UAAU,CAAC,WAAW;gBAC7B,OAAO,EAAE,YAAY,SAAS,oEAAoE;gBAClG,QAAQ,EAAE,OAAO;aAClB,CAAC,CAAA;QACJ,CAAC;QAED,OAAO,MAAM,CAAA;IACf,CAAC;CACF,CAAA"}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export { altTextCheck } from './alt-text.js';
|
|
2
|
+
export { headingHierarchyCheck } from './heading-hierarchy.js';
|
|
3
|
+
export { linkLabelsCheck } from './link-labels.js';
|
|
4
|
+
/** Built-in accessibility checks the pipeline runs before any user-supplied ones. */
|
|
5
|
+
export declare const BUILT_IN_ACCESSIBILITY_CHECKS: readonly [import("../options.js").AccessibilityCheck, import("../options.js").AccessibilityCheck, import("../options.js").AccessibilityCheck];
|
|
6
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/checks/index.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,YAAY,EAAE,MAAM,eAAe,CAAA;AAC5C,OAAO,EAAE,qBAAqB,EAAE,MAAM,wBAAwB,CAAA;AAC9D,OAAO,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAA;AAElD,qFAAqF;AACrF,eAAO,MAAM,6BAA6B,+IAIhC,CAAA"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { altTextCheck } from './alt-text.js';
|
|
2
|
+
import { headingHierarchyCheck } from './heading-hierarchy.js';
|
|
3
|
+
import { linkLabelsCheck } from './link-labels.js';
|
|
4
|
+
export { altTextCheck } from './alt-text.js';
|
|
5
|
+
export { headingHierarchyCheck } from './heading-hierarchy.js';
|
|
6
|
+
export { linkLabelsCheck } from './link-labels.js';
|
|
7
|
+
/** Built-in accessibility checks the pipeline runs before any user-supplied ones. */
|
|
8
|
+
export const BUILT_IN_ACCESSIBILITY_CHECKS = [
|
|
9
|
+
altTextCheck,
|
|
10
|
+
headingHierarchyCheck,
|
|
11
|
+
linkLabelsCheck,
|
|
12
|
+
];
|
|
13
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/checks/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,eAAe,CAAA;AAC5C,OAAO,EAAE,qBAAqB,EAAE,MAAM,wBAAwB,CAAA;AAC9D,OAAO,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAA;AAElD,OAAO,EAAE,YAAY,EAAE,MAAM,eAAe,CAAA;AAC5C,OAAO,EAAE,qBAAqB,EAAE,MAAM,wBAAwB,CAAA;AAC9D,OAAO,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAA;AAElD,qFAAqF;AACrF,MAAM,CAAC,MAAM,6BAA6B,GAAG;IAC3C,YAAY;IACZ,qBAAqB;IACrB,eAAe;CACP,CAAA"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { AccessibilityCheck } from '../options.js';
|
|
2
|
+
/**
|
|
3
|
+
* Walks the document tree for objects with both `url` and `label` fields and
|
|
4
|
+
* flags any where the URL is non-empty but the label is missing or empty.
|
|
5
|
+
* "Click here"-style labels are not flagged (those need a content review,
|
|
6
|
+
* not a link-label check).
|
|
7
|
+
*/
|
|
8
|
+
export declare const linkLabelsCheck: AccessibilityCheck;
|
|
9
|
+
//# sourceMappingURL=link-labels.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"link-labels.d.ts","sourceRoot":"","sources":["../../src/checks/link-labels.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAsB,MAAM,eAAe,CAAA;AAE3E;;;;;GAKG;AACH,eAAO,MAAM,eAAe,EAAE,kBAmB7B,CAAA"}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Walks the document tree for objects with both `url` and `label` fields and
|
|
3
|
+
* flags any where the URL is non-empty but the label is missing or empty.
|
|
4
|
+
* "Click here"-style labels are not flagged (those need a content review,
|
|
5
|
+
* not a link-label check).
|
|
6
|
+
*/
|
|
7
|
+
export const linkLabelsCheck = {
|
|
8
|
+
name: 'link-labels',
|
|
9
|
+
run(doc) {
|
|
10
|
+
const issues = [];
|
|
11
|
+
walkForLinks(doc, (link, path) => {
|
|
12
|
+
const url = link['url'];
|
|
13
|
+
const label = link['label'];
|
|
14
|
+
const hasUrl = typeof url === 'string' && url.trim() !== '';
|
|
15
|
+
const hasLabel = typeof label === 'string' && label.trim() !== '';
|
|
16
|
+
if (hasUrl && !hasLabel) {
|
|
17
|
+
issues.push({
|
|
18
|
+
field: path,
|
|
19
|
+
message: `Link at "${path}" has a URL but no label`,
|
|
20
|
+
severity: 'error',
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
return issues;
|
|
25
|
+
},
|
|
26
|
+
};
|
|
27
|
+
function walkForLinks(value, visit, path = '') {
|
|
28
|
+
if (!value || typeof value !== 'object')
|
|
29
|
+
return;
|
|
30
|
+
if (Array.isArray(value)) {
|
|
31
|
+
for (let i = 0; i < value.length; i++) {
|
|
32
|
+
walkForLinks(value[i], visit, `${path}[${i}]`);
|
|
33
|
+
}
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
const obj = value;
|
|
37
|
+
if ('url' in obj && 'label' in obj) {
|
|
38
|
+
visit(obj, path || '(root)');
|
|
39
|
+
}
|
|
40
|
+
for (const key of Object.keys(obj)) {
|
|
41
|
+
walkForLinks(obj[key], visit, path ? `${path}.${key}` : key);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
//# sourceMappingURL=link-labels.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"link-labels.js","sourceRoot":"","sources":["../../src/checks/link-labels.ts"],"names":[],"mappings":"AAEA;;;;;GAKG;AACH,MAAM,CAAC,MAAM,eAAe,GAAuB;IACjD,IAAI,EAAE,aAAa;IACnB,GAAG,CAAC,GAAG;QACL,MAAM,MAAM,GAAyB,EAAE,CAAA;QACvC,YAAY,CAAC,GAAG,EAAE,CAAC,IAAI,EAAE,IAAI,EAAE,EAAE;YAC/B,MAAM,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,CAAA;YACvB,MAAM,KAAK,GAAG,IAAI,CAAC,OAAO,CAAC,CAAA;YAC3B,MAAM,MAAM,GAAG,OAAO,GAAG,KAAK,QAAQ,IAAI,GAAG,CAAC,IAAI,EAAE,KAAK,EAAE,CAAA;YAC3D,MAAM,QAAQ,GAAG,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,CAAC,IAAI,EAAE,KAAK,EAAE,CAAA;YACjE,IAAI,MAAM,IAAI,CAAC,QAAQ,EAAE,CAAC;gBACxB,MAAM,CAAC,IAAI,CAAC;oBACV,KAAK,EAAE,IAAI;oBACX,OAAO,EAAE,YAAY,IAAI,0BAA0B;oBACnD,QAAQ,EAAE,OAAO;iBAClB,CAAC,CAAA;YACJ,CAAC;QACH,CAAC,CAAC,CAAA;QACF,OAAO,MAAM,CAAA;IACf,CAAC;CACF,CAAA;AAED,SAAS,YAAY,CACnB,KAAc,EACd,KAA4D,EAC5D,IAAI,GAAG,EAAE;IAET,IAAI,CAAC,KAAK,IAAI,OAAO,KAAK,KAAK,QAAQ;QAAE,OAAM;IAE/C,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC;QACzB,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,KAAK,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;YACtC,YAAY,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,GAAG,IAAI,IAAI,CAAC,GAAG,CAAC,CAAA;QAChD,CAAC;QACD,OAAM;IACR,CAAC;IAED,MAAM,GAAG,GAAG,KAAgC,CAAA;IAC5C,IAAI,KAAK,IAAI,GAAG,IAAI,OAAO,IAAI,GAAG,EAAE,CAAC;QACnC,KAAK,CAAC,GAAG,EAAE,IAAI,IAAI,QAAQ,CAAC,CAAA;IAC9B,CAAC;IAED,KAAK,MAAM,GAAG,IAAI,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC;QACnC,YAAY,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,KAAK,EAAE,IAAI,CAAC,CAAC,CAAC,GAAG,IAAI,IAAI,GAAG,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,CAAA;IAC9D,CAAC;AACH,CAAC"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { CollectionBeforeChangeHook } from 'payload';
|
|
2
|
+
/**
|
|
3
|
+
* The trust boundary. This `beforeChange` hook rejects every update that
|
|
4
|
+
* touches `_status` unless the request explicitly carries the
|
|
5
|
+
* `bypassPublishingServer` context flag (set by the pipeline's `executeStep`).
|
|
6
|
+
*
|
|
7
|
+
* Without this hook, the Payload MCP could let Claude flip a document to
|
|
8
|
+
* `published` directly, sidestepping every check the publishing server
|
|
9
|
+
* exists to enforce. With it, the only sanctioned path is the pipeline.
|
|
10
|
+
*/
|
|
11
|
+
export declare function createBlockStatusWritesHook(): CollectionBeforeChangeHook;
|
|
12
|
+
//# sourceMappingURL=block-status-writes.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"block-status-writes.d.ts","sourceRoot":"","sources":["../../src/hooks/block-status-writes.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,SAAS,CAAA;AAEzD;;;;;;;;GAQG;AACH,wBAAgB,2BAA2B,IAAI,0BAA0B,CAqBxE"}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The trust boundary. This `beforeChange` hook rejects every update that
|
|
3
|
+
* touches `_status` unless the request explicitly carries the
|
|
4
|
+
* `bypassPublishingServer` context flag (set by the pipeline's `executeStep`).
|
|
5
|
+
*
|
|
6
|
+
* Without this hook, the Payload MCP could let Claude flip a document to
|
|
7
|
+
* `published` directly, sidestepping every check the publishing server
|
|
8
|
+
* exists to enforce. With it, the only sanctioned path is the pipeline.
|
|
9
|
+
*/
|
|
10
|
+
export function createBlockStatusWritesHook() {
|
|
11
|
+
return ({ data, originalDoc, operation, context, req }) => {
|
|
12
|
+
if (operation !== 'update')
|
|
13
|
+
return data;
|
|
14
|
+
if (!hasStatusChange(data))
|
|
15
|
+
return data;
|
|
16
|
+
// No-op writes (status set to its current value) are harmless.
|
|
17
|
+
if (originalDoc &&
|
|
18
|
+
typeof originalDoc === 'object' &&
|
|
19
|
+
data['_status'] ===
|
|
20
|
+
originalDoc['_status']) {
|
|
21
|
+
return data;
|
|
22
|
+
}
|
|
23
|
+
if (isBypassed(context) || isBypassed(req?.context))
|
|
24
|
+
return data;
|
|
25
|
+
throw new Error('Direct writes to `_status` are not allowed. Use the publishing server (publish / unpublish / rollback) so the policy pipeline runs.');
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
function hasStatusChange(data) {
|
|
29
|
+
return (typeof data === 'object' && data !== null && '_status' in data);
|
|
30
|
+
}
|
|
31
|
+
function isBypassed(context) {
|
|
32
|
+
if (!context || typeof context !== 'object')
|
|
33
|
+
return false;
|
|
34
|
+
return context['bypassPublishingServer'] === true;
|
|
35
|
+
}
|
|
36
|
+
//# sourceMappingURL=block-status-writes.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"block-status-writes.js","sourceRoot":"","sources":["../../src/hooks/block-status-writes.ts"],"names":[],"mappings":"AAEA;;;;;;;;GAQG;AACH,MAAM,UAAU,2BAA2B;IACzC,OAAO,CAAC,EAAE,IAAI,EAAE,WAAW,EAAE,SAAS,EAAE,OAAO,EAAE,GAAG,EAAE,EAAE,EAAE;QACxD,IAAI,SAAS,KAAK,QAAQ;YAAE,OAAO,IAAI,CAAA;QACvC,IAAI,CAAC,eAAe,CAAC,IAAI,CAAC;YAAE,OAAO,IAAI,CAAA;QAEvC,+DAA+D;QAC/D,IACE,WAAW;YACX,OAAO,WAAW,KAAK,QAAQ;YAC9B,IAAgC,CAAC,SAAS,CAAC;gBACzC,WAAuC,CAAC,SAAS,CAAC,EACrD,CAAC;YACD,OAAO,IAAI,CAAA;QACb,CAAC;QAED,IAAI,UAAU,CAAC,OAAO,CAAC,IAAI,UAAU,CAAC,GAAG,EAAE,OAAO,CAAC;YAAE,OAAO,IAAI,CAAA;QAEhE,MAAM,IAAI,KAAK,CACb,qIAAqI,CACtI,CAAA;IACH,CAAC,CAAA;AACH,CAAC;AAED,SAAS,eAAe,CAAC,IAAa;IACpC,OAAO,CACL,OAAO,IAAI,KAAK,QAAQ,IAAI,IAAI,KAAK,IAAI,IAAI,SAAS,IAAI,IAAI,CAC/D,CAAA;AACH,CAAC;AAED,SAAS,UAAU,CAAC,OAAgB;IAClC,IAAI,CAAC,OAAO,IAAI,OAAO,OAAO,KAAK,QAAQ;QAAE,OAAO,KAAK,CAAA;IACzD,OAAQ,OAAmC,CAAC,wBAAwB,CAAC,KAAK,IAAI,CAAA;AAChF,CAAC"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export { publishingPlugin } from './plugin.js';
|
|
2
|
+
export type { AccessibilityCheck, AccessibilityIssue, ActiveApproval, ApprovalResolver, PublishableCollection, PublishingPluginOptions, ResolvedCollection, } from './options.js';
|
|
3
|
+
export type { PipelineContext, PipelineResult, PipelineStep, PipelineStepResult, } from './pipeline/index.js';
|
|
4
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAA;AAE9C,YAAY,EACV,kBAAkB,EAClB,kBAAkB,EAClB,cAAc,EACd,gBAAgB,EAChB,qBAAqB,EACrB,uBAAuB,EACvB,kBAAkB,GACnB,MAAM,cAAc,CAAA;AAErB,YAAY,EACV,eAAe,EACf,cAAc,EACd,YAAY,EACZ,kBAAkB,GACnB,MAAM,qBAAqB,CAAA"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAA"}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import type { Inngest } from 'inngest';
|
|
2
|
+
import type { BaseCorePluginOptions } from '@forumone/throughline-plugin-contract';
|
|
3
|
+
export interface PublishableCollection {
|
|
4
|
+
/** Slug of the collection that can be published through this server. */
|
|
5
|
+
slug: string;
|
|
6
|
+
/** Field name on the document holding composition blocks. Default: `'layout'`. */
|
|
7
|
+
layoutField?: string;
|
|
8
|
+
/** Field name for the SEO group used by required-field checks. Default: `'seo'`. */
|
|
9
|
+
seoField?: string;
|
|
10
|
+
/** Field name for the policy group (`requiresApproval`, `embargoedUntil`). Default: `'policy'`. */
|
|
11
|
+
policyField?: string;
|
|
12
|
+
/** Field name for the slug. Default: `'slug'`. */
|
|
13
|
+
slugField?: string;
|
|
14
|
+
/** Field name for the publish timestamp. Default: `'publishedAt'`. */
|
|
15
|
+
publishedAtField?: string;
|
|
16
|
+
/** Field name for a future scheduled publish time. Default: `'scheduledPublishAt'`. */
|
|
17
|
+
scheduledPublishField?: string;
|
|
18
|
+
/** Per-collection required fields beyond the built-in SEO/slug checks. */
|
|
19
|
+
requiredFields?: Array<{
|
|
20
|
+
path: string;
|
|
21
|
+
message: string;
|
|
22
|
+
}>;
|
|
23
|
+
}
|
|
24
|
+
export interface AccessibilityIssue {
|
|
25
|
+
field?: string | undefined;
|
|
26
|
+
message: string;
|
|
27
|
+
severity: 'error' | 'warning';
|
|
28
|
+
}
|
|
29
|
+
export interface AccessibilityCheck {
|
|
30
|
+
name: string;
|
|
31
|
+
run: (doc: Record<string, unknown>, collection: ResolvedCollection) => Promise<AccessibilityIssue[]> | AccessibilityIssue[];
|
|
32
|
+
}
|
|
33
|
+
export interface ActiveApproval {
|
|
34
|
+
id: string;
|
|
35
|
+
grantedAt: string;
|
|
36
|
+
grantedBy: string;
|
|
37
|
+
version: string;
|
|
38
|
+
}
|
|
39
|
+
export interface ApprovalResolver {
|
|
40
|
+
/** Returns the active granted approval for this document version, or `null`. */
|
|
41
|
+
getActiveApproval: (collection: string, id: string, version: string) => Promise<ActiveApproval | null>;
|
|
42
|
+
}
|
|
43
|
+
export interface PublishingPluginOptions extends BaseCorePluginOptions {
|
|
44
|
+
/** Required: collections that can be published through this server. */
|
|
45
|
+
collections: PublishableCollection[];
|
|
46
|
+
/** Optional: extra accessibility checks beyond the built-ins. */
|
|
47
|
+
accessibilityChecks?: AccessibilityCheck[];
|
|
48
|
+
/** Optional: resolver consulted when a document's policy requires approval. */
|
|
49
|
+
approvalResolver?: ApprovalResolver;
|
|
50
|
+
/** Required: Inngest client used to fire publishing events. */
|
|
51
|
+
inngest: Inngest;
|
|
52
|
+
}
|
|
53
|
+
export type ResolvedCollection = Required<Omit<PublishableCollection, 'requiredFields'>> & Pick<PublishableCollection, 'requiredFields'>;
|
|
54
|
+
/**
|
|
55
|
+
* Validates plugin options at load time. Throws with targeted messages on
|
|
56
|
+
* obvious misconfiguration so deploys fail fast rather than the first publish
|
|
57
|
+
* call returning 503.
|
|
58
|
+
*/
|
|
59
|
+
export declare function validateOptions(options: PublishingPluginOptions): PublishingPluginOptions;
|
|
60
|
+
/**
|
|
61
|
+
* Returns the collection config with all field-name defaults filled in.
|
|
62
|
+
* Throws when the slug isn't registered as publishable.
|
|
63
|
+
*/
|
|
64
|
+
export declare function resolveCollection(options: PublishingPluginOptions, slug: string): ResolvedCollection;
|
|
65
|
+
//# sourceMappingURL=options.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"options.d.ts","sourceRoot":"","sources":["../src/options.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,SAAS,CAAA;AAEtC,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,uCAAuC,CAAA;AAElF,MAAM,WAAW,qBAAqB;IACpC,wEAAwE;IACxE,IAAI,EAAE,MAAM,CAAA;IACZ,kFAAkF;IAClF,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,oFAAoF;IACpF,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,mGAAmG;IACnG,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,kDAAkD;IAClD,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,sEAAsE;IACtE,gBAAgB,CAAC,EAAE,MAAM,CAAA;IACzB,uFAAuF;IACvF,qBAAqB,CAAC,EAAE,MAAM,CAAA;IAC9B,0EAA0E;IAC1E,cAAc,CAAC,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC,CAAA;CAC1D;AAED,MAAM,WAAW,kBAAkB;IACjC,KAAK,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;IAC1B,OAAO,EAAE,MAAM,CAAA;IACf,QAAQ,EAAE,OAAO,GAAG,SAAS,CAAA;CAC9B;AAED,MAAM,WAAW,kBAAkB;IACjC,IAAI,EAAE,MAAM,CAAA;IACZ,GAAG,EAAE,CACH,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC5B,UAAU,EAAE,kBAAkB,KAC3B,OAAO,CAAC,kBAAkB,EAAE,CAAC,GAAG,kBAAkB,EAAE,CAAA;CAC1D;AAED,MAAM,WAAW,cAAc;IAC7B,EAAE,EAAE,MAAM,CAAA;IACV,SAAS,EAAE,MAAM,CAAA;IACjB,SAAS,EAAE,MAAM,CAAA;IACjB,OAAO,EAAE,MAAM,CAAA;CAChB;AAED,MAAM,WAAW,gBAAgB;IAC/B,gFAAgF;IAChF,iBAAiB,EAAE,CACjB,UAAU,EAAE,MAAM,EAClB,EAAE,EAAE,MAAM,EACV,OAAO,EAAE,MAAM,KACZ,OAAO,CAAC,cAAc,GAAG,IAAI,CAAC,CAAA;CACpC;AAED,MAAM,WAAW,uBAAwB,SAAQ,qBAAqB;IACpE,uEAAuE;IACvE,WAAW,EAAE,qBAAqB,EAAE,CAAA;IACpC,iEAAiE;IACjE,mBAAmB,CAAC,EAAE,kBAAkB,EAAE,CAAA;IAC1C,+EAA+E;IAC/E,gBAAgB,CAAC,EAAE,gBAAgB,CAAA;IACnC,+DAA+D;IAC/D,OAAO,EAAE,OAAO,CAAA;CACjB;AAED,MAAM,MAAM,kBAAkB,GAAG,QAAQ,CAAC,IAAI,CAAC,qBAAqB,EAAE,gBAAgB,CAAC,CAAC,GACtF,IAAI,CAAC,qBAAqB,EAAE,gBAAgB,CAAC,CAAA;AAe/C;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,OAAO,EAAE,uBAAuB,GAAG,uBAAuB,CAiBzF;AAED;;;GAGG;AACH,wBAAgB,iBAAiB,CAC/B,OAAO,EAAE,uBAAuB,EAChC,IAAI,EAAE,MAAM,GACX,kBAAkB,CAiBpB"}
|
package/dist/options.js
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
const PublishableCollectionSchema = z.object({
|
|
3
|
+
slug: z.string().min(1),
|
|
4
|
+
layoutField: z.string().optional(),
|
|
5
|
+
seoField: z.string().optional(),
|
|
6
|
+
policyField: z.string().optional(),
|
|
7
|
+
slugField: z.string().optional(),
|
|
8
|
+
publishedAtField: z.string().optional(),
|
|
9
|
+
scheduledPublishField: z.string().optional(),
|
|
10
|
+
requiredFields: z
|
|
11
|
+
.array(z.object({ path: z.string().min(1), message: z.string().min(1) }))
|
|
12
|
+
.optional(),
|
|
13
|
+
});
|
|
14
|
+
/**
|
|
15
|
+
* Validates plugin options at load time. Throws with targeted messages on
|
|
16
|
+
* obvious misconfiguration so deploys fail fast rather than the first publish
|
|
17
|
+
* call returning 503.
|
|
18
|
+
*/
|
|
19
|
+
export function validateOptions(options) {
|
|
20
|
+
if (!options.collections || options.collections.length === 0) {
|
|
21
|
+
throw new Error('publishingPlugin requires at least one collection in options.collections');
|
|
22
|
+
}
|
|
23
|
+
if (!options.inngest) {
|
|
24
|
+
throw new Error('publishingPlugin requires an Inngest client in options.inngest');
|
|
25
|
+
}
|
|
26
|
+
for (const collection of options.collections) {
|
|
27
|
+
const result = PublishableCollectionSchema.safeParse(collection);
|
|
28
|
+
if (!result.success) {
|
|
29
|
+
const issues = result.error.issues
|
|
30
|
+
.map((i) => ` - ${i.path.join('.') || '(root)'}: ${i.message}`)
|
|
31
|
+
.join('\n');
|
|
32
|
+
throw new Error(`Invalid collection config:\n${issues}`);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return options;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Returns the collection config with all field-name defaults filled in.
|
|
39
|
+
* Throws when the slug isn't registered as publishable.
|
|
40
|
+
*/
|
|
41
|
+
export function resolveCollection(options, slug) {
|
|
42
|
+
const config = options.collections.find((c) => c.slug === slug);
|
|
43
|
+
if (!config) {
|
|
44
|
+
throw new Error(`Collection "${slug}" is not registered as publishable. Add it to publishingPlugin's collections option.`);
|
|
45
|
+
}
|
|
46
|
+
return {
|
|
47
|
+
slug: config.slug,
|
|
48
|
+
layoutField: config.layoutField ?? 'layout',
|
|
49
|
+
seoField: config.seoField ?? 'seo',
|
|
50
|
+
policyField: config.policyField ?? 'policy',
|
|
51
|
+
slugField: config.slugField ?? 'slug',
|
|
52
|
+
publishedAtField: config.publishedAtField ?? 'publishedAt',
|
|
53
|
+
scheduledPublishField: config.scheduledPublishField ?? 'scheduledPublishAt',
|
|
54
|
+
...(config.requiredFields ? { requiredFields: config.requiredFields } : {}),
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
//# sourceMappingURL=options.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"options.js","sourceRoot":"","sources":["../src/options.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { PipelineContext, PipelineResult } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Runs the full publish pipeline. Stops at the first failing step and
|
|
4
|
+
* returns its diagnostic; otherwise records success with the publish
|
|
5
|
+
* timestamp the execute step set.
|
|
6
|
+
*/
|
|
7
|
+
export declare function runPublishPipeline(context: PipelineContext): Promise<PipelineResult>;
|
|
8
|
+
/**
|
|
9
|
+
* Runs every preflight step (everything but `execute`). Used by
|
|
10
|
+
* `get_publish_status` to answer "would this publish succeed?" without
|
|
11
|
+
* mutating anything.
|
|
12
|
+
*/
|
|
13
|
+
export declare function runPreflightPipeline(context: PipelineContext): Promise<PipelineResult>;
|
|
14
|
+
export type { PipelineContext, PipelineResult, PipelineStep, PipelineStepResult } from './types.js';
|
|
15
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/pipeline/index.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,eAAe,EAAE,cAAc,EAAgB,MAAM,YAAY,CAAA;AAqB/E;;;;GAIG;AACH,wBAAsB,kBAAkB,CAAC,OAAO,EAAE,eAAe,GAAG,OAAO,CAAC,cAAc,CAAC,CAe1F;AAED;;;;GAIG;AACH,wBAAsB,oBAAoB,CACxC,OAAO,EAAE,eAAe,GACvB,OAAO,CAAC,cAAc,CAAC,CAezB;AAED,YAAY,EAAE,eAAe,EAAE,cAAc,EAAE,YAAY,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAA"}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { existStep } from './steps/exist.js';
|
|
2
|
+
import { compositionStep } from './steps/composition.js';
|
|
3
|
+
import { accessibilityStep } from './steps/accessibility.js';
|
|
4
|
+
import { requiredFieldsStep } from './steps/required-fields.js';
|
|
5
|
+
import { embargoStep } from './steps/embargo.js';
|
|
6
|
+
import { approvalStep } from './steps/approval.js';
|
|
7
|
+
import { executeStep } from './steps/execute.js';
|
|
8
|
+
const PREFLIGHT_STEPS = [
|
|
9
|
+
{ name: 'exist', step: existStep },
|
|
10
|
+
{ name: 'composition', step: compositionStep },
|
|
11
|
+
{ name: 'accessibility', step: accessibilityStep },
|
|
12
|
+
{ name: 'required-fields', step: requiredFieldsStep },
|
|
13
|
+
{ name: 'embargo', step: embargoStep },
|
|
14
|
+
{ name: 'approval', step: approvalStep },
|
|
15
|
+
];
|
|
16
|
+
const ORDERED_STEPS = [
|
|
17
|
+
...PREFLIGHT_STEPS,
|
|
18
|
+
{ name: 'execute', step: executeStep },
|
|
19
|
+
];
|
|
20
|
+
/**
|
|
21
|
+
* Runs the full publish pipeline. Stops at the first failing step and
|
|
22
|
+
* returns its diagnostic; otherwise records success with the publish
|
|
23
|
+
* timestamp the execute step set.
|
|
24
|
+
*/
|
|
25
|
+
export async function runPublishPipeline(context) {
|
|
26
|
+
for (const { name, step } of ORDERED_STEPS) {
|
|
27
|
+
const result = await step(context);
|
|
28
|
+
if (!result.pass) {
|
|
29
|
+
return {
|
|
30
|
+
success: false,
|
|
31
|
+
failedAt: name,
|
|
32
|
+
...(result.reason ? { reason: result.reason } : {}),
|
|
33
|
+
...(result.code ? { code: result.code } : {}),
|
|
34
|
+
...(result.issues ? { issues: result.issues } : {}),
|
|
35
|
+
...(result.suggestion ? { suggestion: result.suggestion } : {}),
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return { success: true, publishedAt: new Date().toISOString() };
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Runs every preflight step (everything but `execute`). Used by
|
|
43
|
+
* `get_publish_status` to answer "would this publish succeed?" without
|
|
44
|
+
* mutating anything.
|
|
45
|
+
*/
|
|
46
|
+
export async function runPreflightPipeline(context) {
|
|
47
|
+
for (const { name, step } of PREFLIGHT_STEPS) {
|
|
48
|
+
const result = await step(context);
|
|
49
|
+
if (!result.pass) {
|
|
50
|
+
return {
|
|
51
|
+
success: false,
|
|
52
|
+
failedAt: name,
|
|
53
|
+
...(result.reason ? { reason: result.reason } : {}),
|
|
54
|
+
...(result.code ? { code: result.code } : {}),
|
|
55
|
+
...(result.issues ? { issues: result.issues } : {}),
|
|
56
|
+
...(result.suggestion ? { suggestion: result.suggestion } : {}),
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return { success: true };
|
|
61
|
+
}
|
|
62
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/pipeline/index.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { PipelineStep } from '../types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Runs every built-in accessibility check followed by user-supplied ones.
|
|
4
|
+
* Errors block publish; warnings are surfaced on the result but don't fail
|
|
5
|
+
* the step. (Warning-only behavior is currently unused by the built-in
|
|
6
|
+
* checks but reserved for richer per-client checks.)
|
|
7
|
+
*/
|
|
8
|
+
export declare const accessibilityStep: PipelineStep;
|
|
9
|
+
//# sourceMappingURL=accessibility.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"accessibility.d.ts","sourceRoot":"","sources":["../../../src/pipeline/steps/accessibility.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAiB,YAAY,EAAE,MAAM,aAAa,CAAA;AAG9D;;;;;GAKG;AACH,eAAO,MAAM,iBAAiB,EAAE,YA2B/B,CAAA"}
|