mandrel-platform 0.12.0 → 0.14.2

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.
@@ -0,0 +1,111 @@
1
+ /**
2
+ * cors-hono.mjs — closed-allowlist CORS as a `hono/cors` options factory.
3
+ *
4
+ * `hono`-based consumers (athportal, swarm-os) reach for the built-in
5
+ * `hono/cors` middleware, whose options object takes an `origin` that may be a
6
+ * function `(origin, c) => string | null`. This variant builds exactly that
7
+ * options object — pre-wired with the closed-allowlist resolver — so the
8
+ * consumer writes `app.use("*", cors(createHonoCorsOptions({...})))` and
9
+ * inherits the invariant instead of re-deriving the origin callback.
10
+ *
11
+ * The no-wildcard-with-credentials invariant is inherited *by construction*
12
+ * from `createAllowlist`: building these options with `['*']` +
13
+ * `credentials: true` throws before the app ever starts.
14
+ */
15
+
16
+ import { createAllowlist } from "./allowlist.mjs";
17
+
18
+ /**
19
+ * @typedef {Object} HonoCorsOptions
20
+ * @property {string[]} allowedOrigins
21
+ * Per-env allowed origins (absolute, or a single `*`). Required.
22
+ * @property {boolean} [credentials=false]
23
+ * Maps to the `hono/cors` `credentials` option. Rejected at construction
24
+ * when combined with a wildcard origin.
25
+ * @property {string[]} [methods=["GET","HEAD","POST","PUT","PATCH","DELETE","OPTIONS"]]
26
+ * @property {string[]} [allowedHeaders=["Content-Type","Authorization"]]
27
+ * @property {string[]} [exposedHeaders=[]]
28
+ * @property {number} [maxAge=86400]
29
+ */
30
+
31
+ const DEFAULT_METHODS = [
32
+ "GET",
33
+ "HEAD",
34
+ "POST",
35
+ "PUT",
36
+ "PATCH",
37
+ "DELETE",
38
+ "OPTIONS",
39
+ ];
40
+ const DEFAULT_ALLOWED_HEADERS = ["Content-Type", "Authorization"];
41
+
42
+ /**
43
+ * Build the options object for `hono/cors`'s `cors()` middleware.
44
+ *
45
+ * The returned `origin` is a function so the closed allowlist is applied
46
+ * per-request: it echoes the request origin only when trusted, and returns
47
+ * `null` otherwise (no `Access-Control-Allow-Origin` header — `hono/cors`
48
+ * omits the header for a `null` return, which is the closed-allowlist
49
+ * behaviour). For a wildcard env it returns the literal `*` (credentials are
50
+ * proven `false` by construction in that case).
51
+ *
52
+ * ```ts
53
+ * import { Hono } from "hono";
54
+ * import { cors } from "hono/cors";
55
+ * import { createHonoCorsOptions } from "mandrel-platform/edge-security/cors-hono.mjs";
56
+ *
57
+ * const app = new Hono();
58
+ * app.use(
59
+ * "*",
60
+ * cors(
61
+ * createHonoCorsOptions({
62
+ * allowedOrigins: ["https://athportal.com"],
63
+ * credentials: true,
64
+ * }),
65
+ * ),
66
+ * );
67
+ * ```
68
+ *
69
+ * @param {HonoCorsOptions} options
70
+ * @returns {{
71
+ * origin: (origin: string) => string | null,
72
+ * allowMethods: string[],
73
+ * allowHeaders: string[],
74
+ * exposeHeaders: string[],
75
+ * credentials: boolean,
76
+ * maxAge: number,
77
+ * }}
78
+ */
79
+ export function createHonoCorsOptions(options) {
80
+ if (!options || !Array.isArray(options.allowedOrigins)) {
81
+ throw new TypeError(
82
+ "[edge-security] createHonoCorsOptions({ allowedOrigins }): `allowedOrigins` (string[]) is required",
83
+ );
84
+ }
85
+
86
+ const credentials = options.credentials === true;
87
+ // Constructs the allowlist — throws here on wildcard + credentials.
88
+ const allowlist = createAllowlist(options.allowedOrigins, { credentials });
89
+
90
+ const allowMethods = options.methods ?? DEFAULT_METHODS;
91
+ const allowHeaders = options.allowedHeaders ?? DEFAULT_ALLOWED_HEADERS;
92
+ const exposeHeaders = options.exposedHeaders ?? [];
93
+ const maxAge = options.maxAge ?? 86400;
94
+
95
+ return {
96
+ /**
97
+ * `hono/cors` origin callback. Returns the value for
98
+ * `Access-Control-Allow-Origin`, or `null` when the origin is untrusted.
99
+ * @param {string} requestOrigin
100
+ * @returns {string | null}
101
+ */
102
+ origin(requestOrigin) {
103
+ return allowlist.resolve(requestOrigin);
104
+ },
105
+ allowMethods,
106
+ allowHeaders,
107
+ exposeHeaders,
108
+ credentials,
109
+ maxAge,
110
+ };
111
+ }
@@ -0,0 +1,34 @@
1
+ /**
2
+ * edge-security — reusable per-env edge-security middleware units for
3
+ * mandrel-platform consumers.
4
+ *
5
+ * Covers the three invariants every consumer was hand-rolling per env:
6
+ * - **CORS** (closed allowlist; Astro + hono variants), with the
7
+ * no-wildcard-with-credentials invariant enforced *by construction*.
8
+ * - **Security headers** (CSP / HSTS / XFO / XCTO / Referrer-Policy).
9
+ * - **App-layer rate limiting** (fixed-window; Astro + hono adapters).
10
+ *
11
+ * Distributed through the npm package-export channel (see
12
+ * `mandrel-platform/edge-security/*`), consistent with the platform's other
13
+ * reusable code (`config/*.base.json`, `scripts/*`). Import the barrel for
14
+ * everything, or a single sub-path for one unit.
15
+ *
16
+ * The CORS code legitimately differs by architecture (Astro middleware vs
17
+ * `hono/cors`), so both variants ship — the divergence is preserved, not
18
+ * flattened.
19
+ */
20
+
21
+ export { createAllowlist, normalizeOrigin, WILDCARD } from "./allowlist.mjs";
22
+ export { createAstroCors } from "./cors-astro.mjs";
23
+ export { createHonoCorsOptions } from "./cors-hono.mjs";
24
+ export {
25
+ applySecurityHeaders,
26
+ buildSecurityHeaders,
27
+ } from "./security-headers.mjs";
28
+ export {
29
+ createAstroRateLimit,
30
+ createHonoRateLimit,
31
+ createMemoryStore,
32
+ createRateLimiter,
33
+ rateLimitHeaders,
34
+ } from "./rate-limit.mjs";
@@ -0,0 +1,219 @@
1
+ /**
2
+ * rate-limit.mjs — app-layer rate limiting as a framework-agnostic fixed-window
3
+ * limiter plus thin Astro / hono adapters.
4
+ *
5
+ * Consumers were each hand-rolling an in-memory / KV-backed limiter with the
6
+ * same intent (N requests per window per key). This unit ships the core
7
+ * decision function — `createRateLimiter` — parameterized by limit, window, a
8
+ * key extractor, and a pluggable store, so a consumer swaps the in-memory store
9
+ * for a Cloudflare KV / Durable Object store without re-deriving the limiter
10
+ * logic. The default store is a self-pruning in-memory `Map` suitable for a
11
+ * single-isolate dev / small deployment; production multi-isolate consumers
12
+ * pass a shared store.
13
+ */
14
+
15
+ /**
16
+ * @typedef {Object} RateLimitStore
17
+ * @property {(key: string) => Promise<{ count: number, resetAt: number } | null> | { count: number, resetAt: number } | null} get
18
+ * @property {(key: string, value: { count: number, resetAt: number }) => Promise<void> | void} set
19
+ */
20
+
21
+ /**
22
+ * In-memory fixed-window store. Self-prunes expired buckets on access so it
23
+ * does not leak unboundedly. NOT shared across isolates — fine for dev / single
24
+ * instance; pass a KV-backed store in production.
25
+ * @returns {RateLimitStore}
26
+ */
27
+ export function createMemoryStore() {
28
+ /** @type {Map<string, { count: number, resetAt: number }>} */
29
+ const buckets = new Map();
30
+ return {
31
+ get(key) {
32
+ const bucket = buckets.get(key);
33
+ if (!bucket) {
34
+ return null;
35
+ }
36
+ if (bucket.resetAt <= Date.now()) {
37
+ buckets.delete(key);
38
+ return null;
39
+ }
40
+ return bucket;
41
+ },
42
+ set(key, value) {
43
+ buckets.set(key, value);
44
+ },
45
+ };
46
+ }
47
+
48
+ /**
49
+ * @typedef {Object} RateLimiterOptions
50
+ * @property {number} limit Max requests allowed per window. Required.
51
+ * @property {number} windowMs Window length in milliseconds. Required.
52
+ * @property {(request: Request) => string} [keyExtractor]
53
+ * Derives the rate-limit bucket key from the request. Defaults to the
54
+ * client IP from `CF-Connecting-IP` / `X-Forwarded-For` (first hop), falling
55
+ * back to a constant so a missing IP fails *closed* into one shared bucket
56
+ * rather than bypassing the limit per-request.
57
+ * @property {RateLimitStore} [store] Defaults to `createMemoryStore()`.
58
+ */
59
+
60
+ /**
61
+ * @typedef {Object} RateLimitDecision
62
+ * @property {boolean} allowed
63
+ * @property {number} limit
64
+ * @property {number} remaining
65
+ * @property {number} resetAt Epoch ms when the current window resets.
66
+ * @property {number} retryAfter Seconds until reset (0 when allowed).
67
+ */
68
+
69
+ /**
70
+ * Default key extractor: client IP, failing closed to a shared bucket.
71
+ * @param {Request} request
72
+ * @returns {string}
73
+ */
74
+ function defaultKeyExtractor(request) {
75
+ const cf = request.headers.get("CF-Connecting-IP");
76
+ if (cf) {
77
+ return cf;
78
+ }
79
+ const xff = request.headers.get("X-Forwarded-For");
80
+ if (xff) {
81
+ const first = xff.split(",")[0];
82
+ if (first) {
83
+ return first.trim();
84
+ }
85
+ }
86
+ // No identifiable client — fail closed into one shared bucket rather than
87
+ // handing every anonymous request its own unlimited allowance.
88
+ return "anonymous";
89
+ }
90
+
91
+ /**
92
+ * Build a fixed-window rate limiter. The returned `check(request)` resolves to
93
+ * a decision object the caller turns into a 429 (or passes through).
94
+ *
95
+ * @param {RateLimiterOptions} options
96
+ * @returns {{ check: (request: Request) => Promise<RateLimitDecision> }}
97
+ */
98
+ export function createRateLimiter(options) {
99
+ if (
100
+ !options ||
101
+ typeof options.limit !== "number" ||
102
+ typeof options.windowMs !== "number"
103
+ ) {
104
+ throw new TypeError(
105
+ "[edge-security] createRateLimiter({ limit, windowMs }): numeric `limit` and `windowMs` are required",
106
+ );
107
+ }
108
+ if (options.limit < 1 || options.windowMs < 1) {
109
+ throw new RangeError(
110
+ "[edge-security] createRateLimiter: `limit` and `windowMs` must be >= 1",
111
+ );
112
+ }
113
+ const limit = options.limit;
114
+ const windowMs = options.windowMs;
115
+ const keyExtractor = options.keyExtractor ?? defaultKeyExtractor;
116
+ const store = options.store ?? createMemoryStore();
117
+
118
+ return {
119
+ /**
120
+ * @param {Request} request
121
+ * @returns {Promise<RateLimitDecision>}
122
+ */
123
+ async check(request) {
124
+ const key = keyExtractor(request);
125
+ const now = Date.now();
126
+ const existing = await store.get(key);
127
+
128
+ let count;
129
+ let resetAt;
130
+ if (existing && existing.resetAt > now) {
131
+ count = existing.count + 1;
132
+ resetAt = existing.resetAt;
133
+ } else {
134
+ count = 1;
135
+ resetAt = now + windowMs;
136
+ }
137
+
138
+ await store.set(key, { count, resetAt });
139
+
140
+ const allowed = count <= limit;
141
+ const remaining = Math.max(0, limit - count);
142
+ const retryAfter = allowed ? 0 : Math.ceil((resetAt - now) / 1000);
143
+
144
+ return { allowed, limit, remaining, resetAt, retryAfter };
145
+ },
146
+ };
147
+ }
148
+
149
+ /**
150
+ * Standard rate-limit response headers for a decision. Spread onto a 429 (or a
151
+ * passed-through response) so clients see their budget.
152
+ * @param {RateLimitDecision} decision
153
+ * @returns {Record<string, string>}
154
+ */
155
+ export function rateLimitHeaders(decision) {
156
+ /** @type {Record<string, string>} */
157
+ const headers = {
158
+ "RateLimit-Limit": String(decision.limit),
159
+ "RateLimit-Remaining": String(decision.remaining),
160
+ "RateLimit-Reset": String(Math.ceil((decision.resetAt - Date.now()) / 1000)),
161
+ };
162
+ if (!decision.allowed) {
163
+ headers["Retry-After"] = String(decision.retryAfter);
164
+ }
165
+ return headers;
166
+ }
167
+
168
+ /**
169
+ * Astro middleware adapter: returns `(context, next) => Response`. Short-circuits
170
+ * with a 429 when the limiter denies the request, otherwise annotates the
171
+ * downstream response with the budget headers.
172
+ *
173
+ * @param {RateLimiterOptions} options
174
+ * @returns {(context: { request: Request }, next: () => Promise<Response>) => Promise<Response>}
175
+ */
176
+ export function createAstroRateLimit(options) {
177
+ const limiter = createRateLimiter(options);
178
+ return async function astroRateLimitMiddleware(context, next) {
179
+ const decision = await limiter.check(context.request);
180
+ if (!decision.allowed) {
181
+ return new Response("Too Many Requests", {
182
+ status: 429,
183
+ headers: rateLimitHeaders(decision),
184
+ });
185
+ }
186
+ const response = await next();
187
+ for (const [key, value] of Object.entries(rateLimitHeaders(decision))) {
188
+ response.headers.set(key, value);
189
+ }
190
+ return response;
191
+ };
192
+ }
193
+
194
+ /**
195
+ * hono middleware adapter: returns `(c, next) => Promise<Response | void>`.
196
+ * Short-circuits with `c.text("Too Many Requests", 429)` when denied.
197
+ *
198
+ * ```ts
199
+ * import { createHonoRateLimit } from "mandrel-platform/edge-security/rate-limit.mjs";
200
+ * app.use("*", createHonoRateLimit({ limit: 100, windowMs: 60_000 }));
201
+ * ```
202
+ *
203
+ * @param {RateLimiterOptions} options
204
+ * @returns {(c: any, next: () => Promise<void>) => Promise<Response | void>}
205
+ */
206
+ export function createHonoRateLimit(options) {
207
+ const limiter = createRateLimiter(options);
208
+ return async function honoRateLimitMiddleware(c, next) {
209
+ const decision = await limiter.check(c.req.raw);
210
+ const headers = rateLimitHeaders(decision);
211
+ if (!decision.allowed) {
212
+ return c.text("Too Many Requests", 429, headers);
213
+ }
214
+ await next();
215
+ for (const [key, value] of Object.entries(headers)) {
216
+ c.res.headers.set(key, value);
217
+ }
218
+ };
219
+ }
@@ -0,0 +1,148 @@
1
+ /**
2
+ * security-headers.mjs — the uniform security-header set every consumer was
3
+ * hand-rolling per env: CSP, HSTS, X-Frame-Options, X-Content-Type-Options,
4
+ * and Referrer-Policy.
5
+ *
6
+ * Ships two surfaces so both architectures inherit the same invariant:
7
+ * - `buildSecurityHeaders(options)` — a framework-agnostic `Record<string,string>`
8
+ * a consumer can spread onto any `Headers` / response (Astro, hono, plain
9
+ * Workers `fetch`).
10
+ * - `applySecurityHeaders(headers, options)` — mutates a `Headers` instance
11
+ * in place (the common middleware path).
12
+ *
13
+ * Every header is parameterized but ships a safe default, so a consumer that
14
+ * passes `{}` still inherits a hardened baseline rather than re-deriving the
15
+ * directive strings.
16
+ */
17
+
18
+ /**
19
+ * @typedef {Object} SecurityHeaderOptions
20
+ * @property {string | false} [contentSecurityPolicy]
21
+ * The full CSP string. Defaults to a strict self-only policy. Pass `false`
22
+ * to omit the CSP header entirely (e.g. when a CDN injects it).
23
+ * @property {Object} [hsts]
24
+ * @property {number} [hsts.maxAge=63072000] `max-age` seconds (default 2y).
25
+ * @property {boolean} [hsts.includeSubDomains=true]
26
+ * @property {boolean} [hsts.preload=false]
27
+ * @property {false} [hsts.enabled]
28
+ * Pass `{ enabled: false }`-style by setting `hsts: false` to omit HSTS
29
+ * (e.g. on a non-HTTPS preview host).
30
+ * @property {string | false} [frameOptions="DENY"]
31
+ * `X-Frame-Options`. `false` omits it (rely on CSP `frame-ancestors`).
32
+ * @property {string | false} [contentTypeOptions="nosniff"]
33
+ * `X-Content-Type-Options`. `false` omits it.
34
+ * @property {string | false} [referrerPolicy="strict-origin-when-cross-origin"]
35
+ * `Referrer-Policy`. `false` omits it.
36
+ */
37
+
38
+ const DEFAULT_CSP = [
39
+ "default-src 'self'",
40
+ "base-uri 'self'",
41
+ "frame-ancestors 'none'",
42
+ "object-src 'none'",
43
+ "upgrade-insecure-requests",
44
+ ].join("; ");
45
+
46
+ const DEFAULT_HSTS_MAX_AGE = 63072000; // 2 years
47
+
48
+ /**
49
+ * Build the `Strict-Transport-Security` value from the hsts option, or `null`
50
+ * when HSTS is disabled.
51
+ * @param {SecurityHeaderOptions["hsts"] | false | undefined} hsts
52
+ * @returns {string | null}
53
+ */
54
+ function buildHsts(hsts) {
55
+ if (hsts === false) {
56
+ return null;
57
+ }
58
+ const maxAge =
59
+ hsts && typeof hsts.maxAge === "number" ? hsts.maxAge : DEFAULT_HSTS_MAX_AGE;
60
+ const includeSubDomains = !hsts || hsts.includeSubDomains !== false;
61
+ const preload = Boolean(hsts && hsts.preload);
62
+
63
+ let value = `max-age=${maxAge}`;
64
+ if (includeSubDomains) {
65
+ value += "; includeSubDomains";
66
+ }
67
+ if (preload) {
68
+ value += "; preload";
69
+ }
70
+ return value;
71
+ }
72
+
73
+ /**
74
+ * Build the security-header set as a plain object. Keys are only present when
75
+ * the corresponding header is enabled (a disabled header is omitted, not set
76
+ * empty).
77
+ *
78
+ * @param {SecurityHeaderOptions} [options]
79
+ * @returns {Record<string, string>}
80
+ */
81
+ export function buildSecurityHeaders(options = {}) {
82
+ /** @type {Record<string, string>} */
83
+ const headers = {};
84
+
85
+ const csp =
86
+ options.contentSecurityPolicy === undefined
87
+ ? DEFAULT_CSP
88
+ : options.contentSecurityPolicy;
89
+ if (csp !== false) {
90
+ headers["Content-Security-Policy"] = csp;
91
+ }
92
+
93
+ const hsts = buildHsts(options.hsts);
94
+ if (hsts !== null) {
95
+ headers["Strict-Transport-Security"] = hsts;
96
+ }
97
+
98
+ const frameOptions =
99
+ options.frameOptions === undefined ? "DENY" : options.frameOptions;
100
+ if (frameOptions !== false) {
101
+ headers["X-Frame-Options"] = frameOptions;
102
+ }
103
+
104
+ const contentTypeOptions =
105
+ options.contentTypeOptions === undefined
106
+ ? "nosniff"
107
+ : options.contentTypeOptions;
108
+ if (contentTypeOptions !== false) {
109
+ headers["X-Content-Type-Options"] = contentTypeOptions;
110
+ }
111
+
112
+ const referrerPolicy =
113
+ options.referrerPolicy === undefined
114
+ ? "strict-origin-when-cross-origin"
115
+ : options.referrerPolicy;
116
+ if (referrerPolicy !== false) {
117
+ headers["Referrer-Policy"] = referrerPolicy;
118
+ }
119
+
120
+ return headers;
121
+ }
122
+
123
+ /**
124
+ * Mutate a `Headers` instance in place with the security-header set. Returns
125
+ * the same instance for chaining.
126
+ *
127
+ * ```ts
128
+ * import { applySecurityHeaders } from "mandrel-platform/edge-security/security-headers.mjs";
129
+ * const response = await next();
130
+ * applySecurityHeaders(response.headers);
131
+ * return response;
132
+ * ```
133
+ *
134
+ * @param {Headers} headers
135
+ * @param {SecurityHeaderOptions} [options]
136
+ * @returns {Headers}
137
+ */
138
+ export function applySecurityHeaders(headers, options = {}) {
139
+ if (!headers || typeof headers.set !== "function") {
140
+ throw new TypeError(
141
+ "[edge-security] applySecurityHeaders(headers): `headers` must be a Headers instance",
142
+ );
143
+ }
144
+ for (const [key, value] of Object.entries(buildSecurityHeaders(options))) {
145
+ headers.set(key, value);
146
+ }
147
+ return headers;
148
+ }
@@ -0,0 +1,7 @@
1
+ {
2
+ "$schema": "https://unpkg.com/knip@5/schema.json",
3
+ "ignoreBinaries": ["mandrel"],
4
+ "ignoreDependencies": ["mandrel-platform"],
5
+ "ignoreExportsUsedInFile": true,
6
+ "includeEntryExports": false
7
+ }
@@ -0,0 +1,27 @@
1
+ {
2
+ "_comment": "Shared Lighthouse CI base for mandrel-platform consumers. Lighthouse's `lighthouserc.json` has no whole-file `extends`, so this ships the shared `ci` block (collect settings + category assertions on the recommended preset). Consumers deep-merge it and add repo-specific `ci.collect.url` / `ci.collect.staticDistDir` and any per-repo assertion overrides (see README). Category score floors stay consumer-tunable.",
3
+ "ci": {
4
+ "collect": {
5
+ "numberOfRuns": 3,
6
+ "settings": {
7
+ "preset": "desktop"
8
+ }
9
+ },
10
+ "assert": {
11
+ "preset": "lighthouse:recommended",
12
+ "assertions": {
13
+ "categories:performance": ["error", { "minScore": 0.9 }],
14
+ "categories:accessibility": ["error", { "minScore": 0.9 }],
15
+ "categories:best-practices": ["error", { "minScore": 0.9 }],
16
+ "categories:seo": ["error", { "minScore": 0.9 }],
17
+ "uses-responsive-images": "off",
18
+ "unused-javascript": "warn",
19
+ "unused-css-rules": "warn",
20
+ "csp-xss": "warn"
21
+ }
22
+ },
23
+ "upload": {
24
+ "target": "temporary-public-storage"
25
+ }
26
+ }
27
+ }
@@ -0,0 +1,6 @@
1
+ {
2
+ "_comment": "Shared size-limit check defaults for mandrel-platform consumers. size-limit's own config is a per-entry array whose paths/limits are inherently repo-specific, so this base ships the *shared check options* (gzip sizing, time budget off, import-cost mode) rather than entry paths. Consumers spread it into each entry of their `.size-limit.json` array: `[{ ...base, \"path\": \"dist/index.js\", \"limit\": \"10 kB\" }]` (see README). `path` and `limit` stay consumer-tunable.",
3
+ "gzip": true,
4
+ "brotli": false,
5
+ "running": false
6
+ }
@@ -0,0 +1,15 @@
1
+ {
2
+ "$schema": "https://raw.githubusercontent.com/stryker-mutator/stryker-js/master/packages/api/schema/stryker-core.json",
3
+ "_comment": "Shared Stryker base for mandrel-platform consumers. Consumers extend via stryker.config.json's `extends` (string or array): { \"extends\": [\"mandrel-platform/stryker.base.json\"], \"testRunner\": \"vitest\", \"mutate\": [\"src/**/*.ts\"] }. testRunner, mutate, and per-repo thresholds stay consumer-tunable.",
4
+ "packageManager": "pnpm",
5
+ "reporters": ["html", "clear-text", "progress"],
6
+ "coverageAnalysis": "perTest",
7
+ "ignoreStatic": true,
8
+ "cleanTempDir": true,
9
+ "timeoutMS": 60000,
10
+ "thresholds": {
11
+ "high": 80,
12
+ "low": 60,
13
+ "break": 50
14
+ }
15
+ }
package/package.json CHANGED
@@ -1,8 +1,17 @@
1
1
  {
2
2
  "name": "mandrel-platform",
3
- "version": "0.12.0",
3
+ "version": "0.14.2",
4
4
  "description": "Shared CI/deploy workflows, composite toolchain action, npm config package, Renovate preset, and operator runbook templates.",
5
5
  "license": "MIT",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/dsj1984/mandrel-platform.git"
9
+ },
10
+ "homepage": "https://github.com/dsj1984/mandrel-platform#readme",
11
+ "bugs": {
12
+ "url": "https://github.com/dsj1984/mandrel-platform/issues"
13
+ },
14
+ "packageManager": "pnpm@11.5.2",
6
15
  "engines": {
7
16
  "node": "24.16.0",
8
17
  "pnpm": ">=11.5.2"
@@ -10,6 +19,13 @@
10
19
  "exports": {
11
20
  "./tsconfig.base.json": "./config/tsconfig.base.json",
12
21
  "./biome.base.json": "./config/biome.base.json",
22
+ "./knip.base.json": "./config/knip.base.json",
23
+ "./stryker.base.json": "./config/stryker.base.json",
24
+ "./dependency-cruiser.base.json": "./config/dependency-cruiser.base.json",
25
+ "./size-limit.base.json": "./config/size-limit.base.json",
26
+ "./lighthouse.base.json": "./config/lighthouse.base.json",
27
+ "./edge-security": "./config/edge-security/index.mjs",
28
+ "./edge-security/*": "./config/edge-security/*",
13
29
  "./scripts/*": "./scripts/*"
14
30
  },
15
31
  "files": [
@@ -18,6 +34,10 @@
18
34
  "scripts/",
19
35
  "templates/"
20
36
  ],
37
+ "publishConfig": {
38
+ "access": "public",
39
+ "provenance": true
40
+ },
21
41
  "dependencies": {
22
42
  "mandrel": "^1.78.0"
23
43
  },
@@ -27,8 +47,9 @@
27
47
  "test": "node --test \"scripts/**/*.test.mjs\"",
28
48
  "platform:sync": "node scripts/platform-sync.mjs",
29
49
  "sync:commands": "node .agents/scripts/sync-claude-commands.js",
50
+ "prepare": "node .agents/scripts/sync-claude-commands.js",
30
51
  "bootstrap": "node .agents/scripts/bootstrap.js",
31
52
  "quality:preview": "node .agents/scripts/quality-preview.js --changed-since HEAD",
32
53
  "quality:watch": "node .agents/scripts/quality-watch.js"
33
54
  }
34
- }
55
+ }