@warpgogol/werkstatt-shared 0.8.2 → 0.8.5
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/README.md
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
# @warpgogol/werkstatt-shared
|
|
2
|
+
|
|
3
|
+
[Українська](README.uk.md) | English
|
|
4
|
+
|
|
5
|
+
Stack-agnostic shared infrastructure extracted from `werkstatt-site` (RFC-0868). Owns checks, integration, ontology, passport, share, and surface domains consumed by both the Werkstatt engine and the site plugin.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## What this package does
|
|
10
|
+
|
|
11
|
+
This is a **shared infrastructure library** used by the [Werkstatt](https://www.npmjs.com/package/@warpgogol/werkstatt) engine and the `werkstatt-site` plugin. It provides:
|
|
12
|
+
|
|
13
|
+
- **Checks** — content validators, SEO validators, surface expand/bake logic
|
|
14
|
+
- **Integration** — CRM, funnel, hub, sharding, QStash adapters
|
|
15
|
+
- **Ontology** — catalogs, enums, Sternsystem owner types
|
|
16
|
+
- **Passport** — DHT signing, identity signing, schema validation
|
|
17
|
+
- **Share** — slug generation, semantic extraction, URL canonicalization, route filtering, middleware, access protection
|
|
18
|
+
- **Surface** — surface expand/bake helpers and labels
|
|
19
|
+
|
|
20
|
+
You don't use this package on its own — it is consumed by the engine and site plugin as a dependency.
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## Installation
|
|
25
|
+
|
|
26
|
+
```sh
|
|
27
|
+
pnpm add @warpgogol/werkstatt-shared
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
This package is installed automatically when you install `@warpgogol/werkstatt` or `@warpgogol/werkstatt-site`.
|
|
31
|
+
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
## How it fits into the Werkstatt ecosystem
|
|
35
|
+
|
|
36
|
+
| Package | Role |
|
|
37
|
+
| --- | --- |
|
|
38
|
+
| `@warpgogol/forge` | Governance layer — skills, RFC/ADR workflows, CLI, project scaffolding |
|
|
39
|
+
| `@warpgogol/werkstatt` | Runtime engine — missions, releases, deployment, certification, Bordbuch |
|
|
40
|
+
| `@warpgogol/werkstatt-shared` | **This package** — shared infrastructure (checks, integration, ontology, passport, share) |
|
|
41
|
+
| `@warpgogol/werkstatt-site` | Astro site plugin — consumes this package for checks, integration, ontology, passport, share |
|
|
42
|
+
|
|
43
|
+
The engine and site plugin both import from this package. It MUST NOT import from `werkstatt-site` — enforced by `werkstatt.shared.validate`.
|
|
44
|
+
|
|
45
|
+
---
|
|
46
|
+
|
|
47
|
+
## Canonical utilities
|
|
48
|
+
|
|
49
|
+
### Slug generation (RFC-0915, DNA-88)
|
|
50
|
+
|
|
51
|
+
| Export | Purpose |
|
|
52
|
+
| --- | --- |
|
|
53
|
+
| `slugUrl(text, lang?)` | Locale-aware URL slug (German umlauts, Ukrainian transliteration, default) |
|
|
54
|
+
| `slugId(text)` | Semantic block ID slug |
|
|
55
|
+
| `HeadingSlugger` | Stateful heading anchor deduplication |
|
|
56
|
+
|
|
57
|
+
```ts
|
|
58
|
+
import { slugUrl } from "@warpgogol/werkstatt-shared/share/slug";
|
|
59
|
+
|
|
60
|
+
const url = slugUrl("Über uns", "de"); // "ueber-uns"
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### Semantic extraction (RFC-0901)
|
|
64
|
+
|
|
65
|
+
| Export | Purpose |
|
|
66
|
+
| --- | --- |
|
|
67
|
+
| `splitSentences(text, locale?)` | Locale-aware sentence boundary detection (`de`, `uk`, `en`) |
|
|
68
|
+
|
|
69
|
+
### Canonical entity URL (RFC-0910)
|
|
70
|
+
|
|
71
|
+
| Export | Purpose |
|
|
72
|
+
| --- | --- |
|
|
73
|
+
| `canonicalRootUrl(baseUrl)` | Unprefixed root URL for JSON-LD entity identity |
|
|
74
|
+
|
|
75
|
+
### Placeholder route filtering (RFC-0917)
|
|
76
|
+
|
|
77
|
+
| Export | Purpose |
|
|
78
|
+
| --- | --- |
|
|
79
|
+
| `hasPlaceholderRoutes(routes)` | Detect Astro dynamic route templates (`[slug]`, `[version]`) |
|
|
80
|
+
|
|
81
|
+
---
|
|
82
|
+
|
|
83
|
+
## Architecture
|
|
84
|
+
|
|
85
|
+
| Directory | Purpose |
|
|
86
|
+
| --- | --- |
|
|
87
|
+
| `src/index.ts` | Main barrel export |
|
|
88
|
+
| `src/checks/` | Content validators, SEO validators, surface expand/bake |
|
|
89
|
+
| `src/integration/` | CRM, funnel, hub, sharding, QStash adapters |
|
|
90
|
+
| `src/ontology/` | Catalogs, enums, Sternsystem owner types |
|
|
91
|
+
| `src/passport/` | DHT signing, identity signing, schema validation |
|
|
92
|
+
| `src/share/` | Slug, semantic, URL canonicalization, routes, middleware, access protection |
|
|
93
|
+
| `src/surface/` | Surface expand/bake helpers and labels |
|
|
94
|
+
| `src/content/` | SystemManifest types and content schema |
|
|
95
|
+
|
|
96
|
+
---
|
|
97
|
+
|
|
98
|
+
## Boundary rules
|
|
99
|
+
|
|
100
|
+
- This package MUST NOT import from `@warpgogol/werkstatt-site` — enforced by `werkstatt.shared.validate`.
|
|
101
|
+
- This package MAY import from `@warpgogol/werkstatt` (engine) and external packages.
|
|
102
|
+
- Axiom dependencies (`@syrokomskyi/axiom-*`) are `optionalDependencies` — consumers without axiom installed must use type-only imports or guard runtime access.
|
|
103
|
+
|
|
104
|
+
---
|
|
105
|
+
|
|
106
|
+
## Publishing to npm
|
|
107
|
+
|
|
108
|
+
This package is published to the npm registry as `@warpgogol/werkstatt-shared`. Publishing is automated via GitHub Actions CI.
|
|
109
|
+
|
|
110
|
+
### How it works
|
|
111
|
+
|
|
112
|
+
1. The source lives in the [warpgogol/werkstatt](https://github.com/syrokomskyi/werkstatt) monorepo under `packages/werkstatt-shared/`.
|
|
113
|
+
2. [`@warpgogol/repo-extract`](https://github.com/syrokomskyi/repo-extract) extracts the package into the standalone [syrokomskyi/werkstatt-shared](https://github.com/syrokomskyi/werkstatt-shared) repository, flattening it to repo root and stripping workspace dependencies.
|
|
114
|
+
3. The generated GitHub Actions CI workflow runs on every push to `main`: lint → typecheck → build → test → `npm publish --provenance --access public`.
|
|
115
|
+
4. The `NPM_TOKEN` secret must be set in the [repository settings](https://github.com/syrokomskyi/werkstatt-shared/settings/secrets/actions).
|
|
116
|
+
|
|
117
|
+
### Triggering a new release
|
|
118
|
+
|
|
119
|
+
From the werkstatt monorepo root:
|
|
120
|
+
|
|
121
|
+
```sh
|
|
122
|
+
# 1. Bump the version in packages/werkstatt-shared/package.json
|
|
123
|
+
# 2. Run the extraction (extracts + commits + pushes to github.com:syrokomskyi/werkstatt-shared.git)
|
|
124
|
+
pnpm exec repo-extract --config packages/werkstatt-shared/extract.config.yaml --verbose
|
|
125
|
+
|
|
126
|
+
# 3. CI picks up the push and publishes to npm automatically
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
After CI completes, verify the new version on [npmjs.com/package/@warpgogol/werkstatt-shared](https://www.npmjs.com/package/@warpgogol/werkstatt-shared).
|
|
130
|
+
|
|
131
|
+
---
|
|
132
|
+
|
|
133
|
+
## License
|
|
134
|
+
|
|
135
|
+
Apache-2.0
|
package/README.uk.md
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
# @warpgogol/werkstatt-shared
|
|
2
|
+
|
|
3
|
+
Українська | [English](README.md)
|
|
4
|
+
|
|
5
|
+
Спільна інфраструктура, незалежна від стеку, витягнута з `werkstatt-site` (RFC-0868). Володіє доменами checks, integration, ontology, passport, share та surface, які споживають рушій Werkstatt та site-плагін.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Що робить цей пакет
|
|
10
|
+
|
|
11
|
+
Це **бібліотека спільної інфраструктури**, яку використовують рушій [Werkstatt](https://www.npmjs.com/package/@warpgogol/werkstatt) та плагін `werkstatt-site`. Вона надає:
|
|
12
|
+
|
|
13
|
+
- **Checks** — валідатори контенту, SEO-валідатори, логіка surface expand/bake
|
|
14
|
+
- **Integration** — адаптери CRM, funnel, hub, sharding, QStash
|
|
15
|
+
- **Ontology** — каталоги, enum-и, типи Sternsystem owner
|
|
16
|
+
- **Passport** — DHT-підписування, підписування ідентичності, валідація схем
|
|
17
|
+
- **Share** — генерація slug-ів, семантична екстракція, канонізація URL, фільтрація маршрутів, middleware, захист доступу
|
|
18
|
+
- **Surface** — хелпери surface expand/bake та мітки
|
|
19
|
+
|
|
20
|
+
Цей пакет не використовується самостійно — його споживають рушій та site-плагін як залежність.
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## Встановлення
|
|
25
|
+
|
|
26
|
+
```sh
|
|
27
|
+
pnpm add @warpgogol/werkstatt-shared
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Цей пакет встановлюється автоматично при встановленні `@warpgogol/werkstatt` або `@warpgogol/werkstatt-site`.
|
|
31
|
+
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
## Як це вписується в екосистему Werkstatt
|
|
35
|
+
|
|
36
|
+
| Пакет | Роль |
|
|
37
|
+
| --- | --- |
|
|
38
|
+
| `@warpgogol/forge` | Шар управління — навички, RFC/ADR робочі процеси, CLI, скаффолд проєктів |
|
|
39
|
+
| `@warpgogol/werkstatt` | Рушій runtime — місії, релізи, розгортання, сертифікація, Bordbuch |
|
|
40
|
+
| `@warpgogol/werkstatt-shared` | **Цей пакет** — спільна інфраструктура (checks, integration, ontology, passport, share) |
|
|
41
|
+
| `@warpgogol/werkstatt-site` | Astro site-плагін — споживає цей пакет для checks, integration, ontology, passport, share |
|
|
42
|
+
|
|
43
|
+
Рушій та site-плагін імпортують з цього пакунка. Він НЕ ПОВИНЕН імпортувати з `werkstatt-site` — це контролюється `werkstatt.shared.validate`.
|
|
44
|
+
|
|
45
|
+
---
|
|
46
|
+
|
|
47
|
+
## Канонічні утиліти
|
|
48
|
+
|
|
49
|
+
### Генерація slug (RFC-0915, DNA-88)
|
|
50
|
+
|
|
51
|
+
| Експорт | Призначення |
|
|
52
|
+
| --- | --- |
|
|
53
|
+
| `slugUrl(text, lang?)` | Локалізований URL-slug (німецькі умлаути, українська транслітерація, за замовчуванням) |
|
|
54
|
+
| `slugId(text)` | Семантичний slug ID блоку |
|
|
55
|
+
| `HeadingSlugger` | Станковий дедуплікатор якорів заголовків |
|
|
56
|
+
|
|
57
|
+
```ts
|
|
58
|
+
import { slugUrl } from "@warpgogol/werkstatt-shared/share/slug";
|
|
59
|
+
|
|
60
|
+
const url = slugUrl("Über uns", "de"); // "ueber-uns"
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### Семантична екстракція (RFC-0901)
|
|
64
|
+
|
|
65
|
+
| Експорт | Призначення |
|
|
66
|
+
| --- | --- |
|
|
67
|
+
| `splitSentences(text, locale?)` | Локалізоване виявлення меж речень (`de`, `uk`, `en`) |
|
|
68
|
+
|
|
69
|
+
### Канонічний URL сутності (RFC-0910)
|
|
70
|
+
|
|
71
|
+
| Експорт | Призначення |
|
|
72
|
+
| --- | --- |
|
|
73
|
+
| `canonicalRootUrl(baseUrl)` | Непрефіксований кореневий URL для JSON-LD ідентичності сутності |
|
|
74
|
+
|
|
75
|
+
### Фільтрація плейсхолдер-маршрутів (RFC-0917)
|
|
76
|
+
|
|
77
|
+
| Експорт | Призначення |
|
|
78
|
+
| --- | --- |
|
|
79
|
+
| `hasPlaceholderRoutes(routes)` | Виявлення Astro динамічних шаблонів маршрутів (`[slug]`, `[version]`) |
|
|
80
|
+
|
|
81
|
+
---
|
|
82
|
+
|
|
83
|
+
## Архітектура
|
|
84
|
+
|
|
85
|
+
| Директорія | Призначення |
|
|
86
|
+
| --- | --- |
|
|
87
|
+
| `src/index.ts` | Головний barrel-експорт |
|
|
88
|
+
| `src/checks/` | Валідатори контенту, SEO-валідатори, surface expand/bake |
|
|
89
|
+
| `src/integration/` | Адаптери CRM, funnel, hub, sharding, QStash |
|
|
90
|
+
| `src/ontology/` | Каталоги, enum-и, типи Sternsystem owner |
|
|
91
|
+
| `src/passport/` | DHT-підписування, підписування ідентичності, валідація схем |
|
|
92
|
+
| `src/share/` | Slug, семантика, канонізація URL, маршрути, middleware, захист доступу |
|
|
93
|
+
| `src/surface/` | Хелпери surface expand/bake та мітки |
|
|
94
|
+
| `src/content/` | Типи SystemManifest та схема контенту |
|
|
95
|
+
|
|
96
|
+
---
|
|
97
|
+
|
|
98
|
+
## Межі
|
|
99
|
+
|
|
100
|
+
- Цей пакет НЕ ПОВИНЕН імпортувати з `@warpgogol/werkstatt-site` — контролюється `werkstatt.shared.validate`.
|
|
101
|
+
- Цей пакет МОЖЕ імпортувати з `@warpgogol/werkstatt` (рушій) та зовнішніх пакунків.
|
|
102
|
+
- Axiom-залежності (`@syrokomskyi/axiom-*`) — `optionalDependencies`. Споживачі без axiom повинні використовувати type-only імпорти або охороняти runtime-доступ.
|
|
103
|
+
|
|
104
|
+
---
|
|
105
|
+
|
|
106
|
+
## Публікація в npm
|
|
107
|
+
|
|
108
|
+
Цей пакет публікується в реєстр npm як `@warpgogol/werkstatt-shared`. Публікація автоматизована через GitHub Actions CI.
|
|
109
|
+
|
|
110
|
+
### Як це працює
|
|
111
|
+
|
|
112
|
+
1. Вихідний код знаходиться в монорепозиторії [warpgogol/werkstatt](https://github.com/syrokomskyi/werkstatt) у `packages/werkstatt-shared/`.
|
|
113
|
+
2. [`@warpgogol/repo-extract`](https://github.com/syrokomskyi/repo-extract) витягує пакет у автономний репозиторій [syrokomskyi/werkstatt-shared](https://github.com/syrokomskyi/werkstatt-shared), вирівнюючи його до кореня репозиторію та видаляючи залежності робочого простору.
|
|
114
|
+
3. Згенерований GitHub Actions CI-воркфлоу запускається при кожному пуші в `main`: lint → typecheck → build → test → `npm publish --provenance --access public`.
|
|
115
|
+
4. Секрет `NPM_TOKEN` має бути встановлений у [налаштуваннях репозиторію](https://github.com/syrokomskyi/werkstatt-shared/settings/secrets/actions).
|
|
116
|
+
|
|
117
|
+
### Запуск нового релізу
|
|
118
|
+
|
|
119
|
+
З кореня монорепозиторію werkstatt:
|
|
120
|
+
|
|
121
|
+
```sh
|
|
122
|
+
# 1. Підняти версію в packages/werkstatt-shared/package.json
|
|
123
|
+
# 2. Запустити екстракцію (витягує + комітить + пушить в github.com:syrokomskyi/werkstatt-shared.git)
|
|
124
|
+
pnpm exec repo-extract --config packages/werkstatt-shared/extract.config.yaml --verbose
|
|
125
|
+
|
|
126
|
+
# 3. CI підхоплює пуш і публікує в npm автоматично
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
Після завершення CI перевірте нову версію на [npmjs.com/package/@warpgogol/werkstatt-shared](https://www.npmjs.com/package/@warpgogol/werkstatt-shared).
|
|
130
|
+
|
|
131
|
+
---
|
|
132
|
+
|
|
133
|
+
## Ліцензія
|
|
134
|
+
|
|
135
|
+
Apache-2.0
|
package/package.json
CHANGED
|
@@ -219,6 +219,8 @@ export interface ImageVariantEntry {
|
|
|
219
219
|
* stale derived variants (same contract as video.variants.generate RFC-0210).
|
|
220
220
|
*/
|
|
221
221
|
sourceHash?: string;
|
|
222
|
+
/** RFC-0928: WebP quality used by image.variants.generate (minimum 90). */
|
|
223
|
+
quality?: number;
|
|
222
224
|
}
|
|
223
225
|
|
|
224
226
|
/** Top-level manifest written by `image.variants.generate` and read by createBuildPortableProvider. */
|
|
@@ -45,6 +45,16 @@ function isDevOrAltHost(host: string): boolean {
|
|
|
45
45
|
return host.startsWith("dev.") || host.startsWith("alt.");
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
+
/**
|
|
49
|
+
* Paths exempt from access protection on dev/alt subdomains.
|
|
50
|
+
* Browsers fetch these without sending Basic Auth credentials, causing 401 console errors.
|
|
51
|
+
*/
|
|
52
|
+
const PUBLIC_STATIC_EXEMPTIONS = ["/manifest.webmanifest"];
|
|
53
|
+
|
|
54
|
+
function isExemptPath(pathname: string): boolean {
|
|
55
|
+
return PUBLIC_STATIC_EXEMPTIONS.includes(pathname);
|
|
56
|
+
}
|
|
57
|
+
|
|
48
58
|
/**
|
|
49
59
|
* RFC-0899: Check access protection for a request. Called from the Worker entry point
|
|
50
60
|
* (worker.ts) before passing to the Astro handler. This is necessary because Astro
|
|
@@ -66,6 +76,9 @@ export function checkAccessProtection(
|
|
|
66
76
|
const host = request.headers.get("host") ?? "";
|
|
67
77
|
if (!isDevOrAltHost(host)) return null;
|
|
68
78
|
|
|
79
|
+
const url = new URL(request.url);
|
|
80
|
+
if (isExemptPath(url.pathname)) return null;
|
|
81
|
+
|
|
69
82
|
const pin = (env.ACCESS_PIN as string | undefined) ?? undefined;
|
|
70
83
|
|
|
71
84
|
// No PIN set — allow access (caller should add X-Robots-Tag)
|
|
@@ -144,6 +157,13 @@ export const accessProtectionMiddleware = defineMiddleware(async (context: any,
|
|
|
144
157
|
return next();
|
|
145
158
|
}
|
|
146
159
|
|
|
160
|
+
const url = new URL(context.request.url);
|
|
161
|
+
if (isExemptPath(url.pathname)) {
|
|
162
|
+
const response = await next();
|
|
163
|
+
response.headers.set("X-Robots-Tag", NOINDEX_HEADER);
|
|
164
|
+
return response;
|
|
165
|
+
}
|
|
166
|
+
|
|
147
167
|
const pin = await resolveAccessPin();
|
|
148
168
|
|
|
149
169
|
// No PIN set — allow access but still set noindex headers
|
|
@@ -25,7 +25,7 @@ describe("RFC-0899: access protection middleware", () => {
|
|
|
25
25
|
) => Promise<Response>;
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
-
function makeContext(host: string, authHeader?: string) {
|
|
28
|
+
function makeContext(host: string, authHeader?: string, pathname = "/") {
|
|
29
29
|
const headers = new Map<string, string>();
|
|
30
30
|
headers.set("host", host);
|
|
31
31
|
if (authHeader) headers.set("authorization", authHeader);
|
|
@@ -34,6 +34,7 @@ describe("RFC-0899: access protection middleware", () => {
|
|
|
34
34
|
headers: {
|
|
35
35
|
get: (name: string) => headers.get(name.toLowerCase()) ?? null,
|
|
36
36
|
},
|
|
37
|
+
url: `https://${host}${pathname}`,
|
|
37
38
|
},
|
|
38
39
|
};
|
|
39
40
|
}
|
|
@@ -42,10 +43,11 @@ describe("RFC-0899: access protection middleware", () => {
|
|
|
42
43
|
handler: (context: unknown, next: () => Promise<Response>) => Promise<Response>,
|
|
43
44
|
host: string,
|
|
44
45
|
authHeader?: string,
|
|
46
|
+
pathname = "/",
|
|
45
47
|
): Promise<Response & { _nextCalled: boolean }> {
|
|
46
48
|
let nextCalled = false;
|
|
47
49
|
const nextResponse = new Response("page content", { status: 200 });
|
|
48
|
-
const result = await handler(makeContext(host, authHeader), async () => {
|
|
50
|
+
const result = await handler(makeContext(host, authHeader, pathname), async () => {
|
|
49
51
|
nextCalled = true;
|
|
50
52
|
return nextResponse;
|
|
51
53
|
});
|
|
@@ -120,4 +122,21 @@ describe("RFC-0899: access protection middleware", () => {
|
|
|
120
122
|
const res = await runMiddleware(handler, "example.com");
|
|
121
123
|
expect(res.headers.get("X-Robots-Tag")).toBe(null);
|
|
122
124
|
});
|
|
125
|
+
|
|
126
|
+
it("exempts /manifest.webmanifest from auth on dev.* when PIN is set", async () => {
|
|
127
|
+
mockEnv.ACCESS_PIN = "1234";
|
|
128
|
+
const handler = await loadMiddleware();
|
|
129
|
+
const res = await runMiddleware(handler, "dev.example.com", undefined, "/manifest.webmanifest");
|
|
130
|
+
expect(res._nextCalled).toBe(true);
|
|
131
|
+
expect(res.status).toBe(200);
|
|
132
|
+
expect(res.headers.get("X-Robots-Tag")).toBe("noindex, nofollow, noai, noimageai");
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it("exempts /manifest.webmanifest from auth on alt.* when PIN is set", async () => {
|
|
136
|
+
mockEnv.ACCESS_PIN = "1234";
|
|
137
|
+
const handler = await loadMiddleware();
|
|
138
|
+
const res = await runMiddleware(handler, "alt.example.com", undefined, "/manifest.webmanifest");
|
|
139
|
+
expect(res._nextCalled).toBe(true);
|
|
140
|
+
expect(res.status).toBe(200);
|
|
141
|
+
});
|
|
123
142
|
});
|