@vxil/feature-configs 0.7.0 → 0.8.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.
@@ -0,0 +1,73 @@
1
+ import { type Static } from '@sinclair/typebox';
2
+ /** extension → the exact Content-Type served for it. */
3
+ export declare const PUBLIC_ASSET_TYPES: Readonly<Record<string, string>>;
4
+ /** The extension a stored content type publishes under, or null when the type
5
+ * may not be published (html, svg, pdf, zip, octet-stream, …). */
6
+ export declare function publicExtForContentType(contentType: string | null | undefined): string | null;
7
+ /** Raster image extensions a variant preset may transform. */
8
+ export declare const VARIANT_SOURCE_EXTS: ReadonlySet<string>;
9
+ /** Video/audio types served `inline` (playable) by shared links and the public
10
+ * host — media formats a browser plays in a player, never as a document. */
11
+ export declare const INLINE_MEDIA_TYPES: ReadonlySet<string>;
12
+ /** The public-asset URL path shape: `/<tenant_id>/<sha256>.<ext>` with an
13
+ * optional `/v/<preset>` variant suffix. The tenant is bound from THIS path
14
+ * (never a Host header). */
15
+ export declare const PUBLIC_ASSET_PATH_RE: RegExp;
16
+ /** The object key in the public bucket: `<tenant_id>/<sha256>.<ext>` —
17
+ * tenant-prefixed (no cross-tenant dedupe, so no hash-existence oracle across
18
+ * tenants) and content-addressed (immutable: a URL never changes meaning). */
19
+ export declare function publicAssetKey(tenantId: string, sha256: string, ext: string): string;
20
+ /** Most variant presets one tenant may declare (the schema bound; the plan
21
+ * ceiling in control-plane plans.ts may be lower). */
22
+ export declare const MAX_VARIANT_PRESETS = 8;
23
+ /** Most CORS origins one tenant may declare. */
24
+ export declare const MAX_CORS_ORIGINS = 20;
25
+ /** Most object ids one bulk publish accepts (the batch download-urls bound). */
26
+ export declare const MAX_BULK_PUBLISH = 100;
27
+ /** Largest object that may be published (bytes): 512 MiB — the largest
28
+ * object the public host's edge cache holds, so every published object
29
+ * (and every Range of it) is served from the cache after its first read.
30
+ * Bigger media belongs on a streaming/transcoding vendor, not a
31
+ * cache-forever host. */
32
+ export declare const MAX_PUBLISH_OBJECT_BYTES: number;
33
+ /** Most bytes ONE bulk publish call may stream (hashing an object without a
34
+ * recorded checksum reads it once, copying reads it again). Ids past the
35
+ * budget land in `errors[]` as `batch_budget_exceeded` — publish them in a
36
+ * further call. */
37
+ export declare const MAX_BULK_PUBLISH_BYTES: number;
38
+ /** Largest source image a variant preset transforms (bytes). */
39
+ export declare const MAX_VARIANT_SOURCE_BYTES: number;
40
+ /** A CORS origin: `*`, an https origin, or a localhost http origin (dev). No
41
+ * path, no trailing slash, no wildcard sub-domains. */
42
+ export declare const CORS_ORIGIN_PATTERN = "^(\\*|https://[a-z0-9]([a-z0-9-]*[a-z0-9])?(\\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*(:[0-9]{1,5})?|http://(localhost|127\\.0\\.0\\.1)(:[0-9]{1,5})?)$";
43
+ /** A preset name: lower-case, URL-safe, ≤ 32 chars (it is a path segment). */
44
+ export declare const VARIANT_PRESET_NAME_PATTERN = "^[a-z0-9][a-z0-9_-]{0,31}$";
45
+ export declare const VariantPresetSchema: import("@sinclair/typebox").TObject<{
46
+ w: import("@sinclair/typebox").TInteger;
47
+ h: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TInteger>;
48
+ fit: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TUnion<[import("@sinclair/typebox").TLiteral<"scale-down">, import("@sinclair/typebox").TLiteral<"contain">, import("@sinclair/typebox").TLiteral<"cover">, import("@sinclair/typebox").TLiteral<"crop">, import("@sinclair/typebox").TLiteral<"pad">]>>;
49
+ fmt: import("@sinclair/typebox").TUnion<[import("@sinclair/typebox").TLiteral<"webp">, import("@sinclair/typebox").TLiteral<"avif">, import("@sinclair/typebox").TLiteral<"jpeg">, import("@sinclair/typebox").TLiteral<"png">]>;
50
+ q: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TInteger>;
51
+ }>;
52
+ export type VariantPreset = Static<typeof VariantPresetSchema>;
53
+ /** `files.publicAssets` — ONE optional bag (one leaf against the 15-leaf cap).
54
+ * `enabled` is the tenant's own kill switch: false ⇒ publish refuses and the
55
+ * public host answers 404 for every one of the tenant's assets. */
56
+ export declare const PublicAssetsConfigSchema: import("@sinclair/typebox").TObject<{
57
+ enabled: import("@sinclair/typebox").TBoolean;
58
+ corsOrigins: import("@sinclair/typebox").TArray<import("@sinclair/typebox").TString>;
59
+ variants: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TRecord<import("@sinclair/typebox").TString, import("@sinclair/typebox").TObject<{
60
+ w: import("@sinclair/typebox").TInteger;
61
+ h: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TInteger>;
62
+ fit: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TUnion<[import("@sinclair/typebox").TLiteral<"scale-down">, import("@sinclair/typebox").TLiteral<"contain">, import("@sinclair/typebox").TLiteral<"cover">, import("@sinclair/typebox").TLiteral<"crop">, import("@sinclair/typebox").TLiteral<"pad">]>>;
63
+ fmt: import("@sinclair/typebox").TUnion<[import("@sinclair/typebox").TLiteral<"webp">, import("@sinclair/typebox").TLiteral<"avif">, import("@sinclair/typebox").TLiteral<"jpeg">, import("@sinclair/typebox").TLiteral<"png">]>;
64
+ q: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TInteger>;
65
+ }>>>;
66
+ }>;
67
+ export type PublicAssetsConfig = Static<typeof PublicAssetsConfigSchema>;
68
+ /** The MIME type a variant `fmt` outputs. */
69
+ export declare function variantOutputType(fmt: VariantPreset['fmt']): 'image/webp' | 'image/avif' | 'image/jpeg' | 'image/png';
70
+ /** A stable short fingerprint of a preset definition — part of the variant
71
+ * cache key, so editing a preset never serves the old rendition. FNV-1a over
72
+ * the canonical field order (no crypto needed: it only has to change). */
73
+ export declare function presetFingerprint(p: VariantPreset): string;
@@ -0,0 +1,130 @@
1
+ // files PUBLIC ASSET DELIVERY (2026-10-03) — the one definition of
2
+ // what may be published to the public asset host, shared by the publish path
3
+ // (files-v1), the serving Worker (files-cdn), the config schema below and the
4
+ // docs gates. A feature worker never imports another worker's code, so the
5
+ // constants that BOTH sides must agree on live in this shared package.
6
+ //
7
+ // THE CONTENT-TYPE ALLOWLIST is the security boundary of a public, cookie-less,
8
+ // cache-forever host: only types a browser never executes as a document.
9
+ // `text/html` is absent by construction, and `image/svg+xml` is REFUSED (an SVG
10
+ // is a document that can run script; serving it under a CSP sandbox was the
11
+ // alternative, refusing is the smaller surface). The serving Worker derives the
12
+ // served Content-Type from the URL's extension through THIS map — never from
13
+ // stored metadata — and adds `X-Content-Type-Options: nosniff`, so bytes that
14
+ // lie about their type are still never rendered as HTML.
15
+ import { Type } from '@sinclair/typebox';
16
+ /** extension → the exact Content-Type served for it. */
17
+ export const PUBLIC_ASSET_TYPES = {
18
+ png: 'image/png',
19
+ jpg: 'image/jpeg',
20
+ webp: 'image/webp',
21
+ avif: 'image/avif',
22
+ gif: 'image/gif',
23
+ mp4: 'video/mp4',
24
+ webm: 'video/webm',
25
+ mp3: 'audio/mpeg',
26
+ m4a: 'audio/mp4',
27
+ ogg: 'audio/ogg',
28
+ woff2: 'font/woff2',
29
+ woff: 'font/woff',
30
+ json: 'application/json',
31
+ };
32
+ /** Stored content types (lower-cased, parameters stripped) accepted at publish
33
+ * → the extension they are published under. The canonical type of each
34
+ * extension plus a few common aliases uploaders send. */
35
+ const TYPE_TO_EXT = {
36
+ ...Object.fromEntries(Object.entries(PUBLIC_ASSET_TYPES).map(([ext, ct]) => [ct, ext])),
37
+ 'image/jpg': 'jpg',
38
+ 'image/pjpeg': 'jpg',
39
+ 'audio/mp3': 'mp3',
40
+ 'audio/x-m4a': 'm4a',
41
+ 'audio/m4a': 'm4a',
42
+ 'application/font-woff': 'woff',
43
+ 'application/font-woff2': 'woff2',
44
+ };
45
+ /** The extension a stored content type publishes under, or null when the type
46
+ * may not be published (html, svg, pdf, zip, octet-stream, …). */
47
+ export function publicExtForContentType(contentType) {
48
+ if (!contentType)
49
+ return null;
50
+ const base = contentType.split(';')[0].trim().toLowerCase();
51
+ return TYPE_TO_EXT[base] ?? null;
52
+ }
53
+ /** Raster image extensions a variant preset may transform. */
54
+ export const VARIANT_SOURCE_EXTS = new Set(['png', 'jpg', 'webp', 'avif', 'gif']);
55
+ /** Video/audio types served `inline` (playable) by shared links and the public
56
+ * host — media formats a browser plays in a player, never as a document. */
57
+ export const INLINE_MEDIA_TYPES = new Set([
58
+ 'video/mp4', 'video/webm', 'audio/mpeg', 'audio/mp4', 'audio/ogg',
59
+ ]);
60
+ /** The public-asset URL path shape: `/<tenant_id>/<sha256>.<ext>` with an
61
+ * optional `/v/<preset>` variant suffix. The tenant is bound from THIS path
62
+ * (never a Host header). */
63
+ export const PUBLIC_ASSET_PATH_RE = /^\/([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})\/([0-9a-f]{64})\.([a-z0-9]{2,5})(?:\/v\/([a-z0-9][a-z0-9_-]{0,31}))?$/;
64
+ /** The object key in the public bucket: `<tenant_id>/<sha256>.<ext>` —
65
+ * tenant-prefixed (no cross-tenant dedupe, so no hash-existence oracle across
66
+ * tenants) and content-addressed (immutable: a URL never changes meaning). */
67
+ export function publicAssetKey(tenantId, sha256, ext) {
68
+ return `${tenantId}/${sha256}.${ext}`;
69
+ }
70
+ /** Most variant presets one tenant may declare (the schema bound; the plan
71
+ * ceiling in control-plane plans.ts may be lower). */
72
+ export const MAX_VARIANT_PRESETS = 8;
73
+ /** Most CORS origins one tenant may declare. */
74
+ export const MAX_CORS_ORIGINS = 20;
75
+ /** Most object ids one bulk publish accepts (the batch download-urls bound). */
76
+ export const MAX_BULK_PUBLISH = 100;
77
+ /** Largest object that may be published (bytes): 512 MiB — the largest
78
+ * object the public host's edge cache holds, so every published object
79
+ * (and every Range of it) is served from the cache after its first read.
80
+ * Bigger media belongs on a streaming/transcoding vendor, not a
81
+ * cache-forever host. */
82
+ export const MAX_PUBLISH_OBJECT_BYTES = 512 * 1024 * 1024;
83
+ /** Most bytes ONE bulk publish call may stream (hashing an object without a
84
+ * recorded checksum reads it once, copying reads it again). Ids past the
85
+ * budget land in `errors[]` as `batch_budget_exceeded` — publish them in a
86
+ * further call. */
87
+ export const MAX_BULK_PUBLISH_BYTES = 2 * 1024 * 1024 * 1024;
88
+ /** Largest source image a variant preset transforms (bytes). */
89
+ export const MAX_VARIANT_SOURCE_BYTES = 25 * 1024 * 1024;
90
+ /** A CORS origin: `*`, an https origin, or a localhost http origin (dev). No
91
+ * path, no trailing slash, no wildcard sub-domains. */
92
+ export const CORS_ORIGIN_PATTERN = '^(\\*|https://[a-z0-9]([a-z0-9-]*[a-z0-9])?(\\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*(:[0-9]{1,5})?|http://(localhost|127\\.0\\.0\\.1)(:[0-9]{1,5})?)$';
93
+ /** A preset name: lower-case, URL-safe, ≤ 32 chars (it is a path segment). */
94
+ export const VARIANT_PRESET_NAME_PATTERN = '^[a-z0-9][a-z0-9_-]{0,31}$';
95
+ export const VariantPresetSchema = Type.Object({
96
+ w: Type.Integer({ minimum: 1, maximum: 4096 }),
97
+ h: Type.Optional(Type.Integer({ minimum: 1, maximum: 4096 })),
98
+ fit: Type.Optional(Type.Union([
99
+ Type.Literal('scale-down'), Type.Literal('contain'), Type.Literal('cover'),
100
+ Type.Literal('crop'), Type.Literal('pad'),
101
+ ])),
102
+ fmt: Type.Union([Type.Literal('webp'), Type.Literal('avif'), Type.Literal('jpeg'), Type.Literal('png')]),
103
+ q: Type.Optional(Type.Integer({ minimum: 1, maximum: 100 })),
104
+ });
105
+ /** `files.publicAssets` — ONE optional bag (one leaf against the 15-leaf cap).
106
+ * `enabled` is the tenant's own kill switch: false ⇒ publish refuses and the
107
+ * public host answers 404 for every one of the tenant's assets. */
108
+ export const PublicAssetsConfigSchema = Type.Object({
109
+ enabled: Type.Boolean({ default: false }),
110
+ corsOrigins: Type.Array(Type.String({ pattern: CORS_ORIGIN_PATTERN, maxLength: 253 }), {
111
+ default: [], maxItems: MAX_CORS_ORIGINS,
112
+ }),
113
+ variants: Type.Optional(Type.Record(Type.String({ pattern: VARIANT_PRESET_NAME_PATTERN }), VariantPresetSchema, { maxProperties: MAX_VARIANT_PRESETS })),
114
+ });
115
+ /** The MIME type a variant `fmt` outputs. */
116
+ export function variantOutputType(fmt) {
117
+ return `image/${fmt}`;
118
+ }
119
+ /** A stable short fingerprint of a preset definition — part of the variant
120
+ * cache key, so editing a preset never serves the old rendition. FNV-1a over
121
+ * the canonical field order (no crypto needed: it only has to change). */
122
+ export function presetFingerprint(p) {
123
+ const s = `${p.w}|${p.h ?? ''}|${p.fit ?? ''}|${p.fmt}|${p.q ?? ''}`;
124
+ let h = 0x811c9dc5;
125
+ for (let i = 0; i < s.length; i++) {
126
+ h ^= s.charCodeAt(i);
127
+ h = Math.imul(h, 0x01000193) >>> 0;
128
+ }
129
+ return h.toString(16).padStart(8, '0');
130
+ }
@@ -9,7 +9,7 @@ export declare const READ_MODEL_LIMITS: {
9
9
  /**
10
10
  * Structural validation of a declared read-model `spec` — shared by the
11
11
  * config-write gate here and re-exported for anyone needing the pure check.
12
- * `kind` selects the §3.1 (aggregate) or §4.1 (rank) grammar.
12
+ * `kind` selects the aggregate or the rank grammar (guide ch. 4).
13
13
  */
14
14
  export declare function validateReadModelSpecShape(where: string, kind: 'aggregate' | 'rank', spec: unknown): string[];
15
15
  /** Shape of one declared read-model (mirrors the CmsConfigSchema Record). */
@@ -103,7 +103,7 @@ function filterErrors(where, raw) {
103
103
  /**
104
104
  * Structural validation of a declared read-model `spec` — shared by the
105
105
  * config-write gate here and re-exported for anyone needing the pure check.
106
- * `kind` selects the §3.1 (aggregate) or §4.1 (rank) grammar.
106
+ * `kind` selects the aggregate or the rank grammar (guide ch. 4).
107
107
  */
108
108
  export function validateReadModelSpecShape(where, kind, spec) {
109
109
  if (!isObj(spec))
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vxil/feature-configs",
3
- "version": "0.7.0",
3
+ "version": "0.8.0",
4
4
  "description": "The per-feature configuration schemas and validators behind vxil.config.ts (published for @vxil/cli and @vxil/config).",
5
5
  "license": "MIT",
6
6
  "homepage": "https://vxil.com",
package/src/apiState.ts CHANGED
@@ -1,12 +1,10 @@
1
- // DECLARED API STATE — the pure declared-vs-live planner (roadmap §4.11 P0-3,
2
- // 2026-09-23).
1
+ // DECLARED API STATE — the pure declared-vs-live planner (2026-09-23).
3
2
  //
4
3
  // Three datums used to be API state OUTSIDE vxil.config — rows their own /v1
5
4
  // route created, that `vxil push` never converged and a rollback never
6
5
  // restored — so every tenant that wanted a reproducible second environment
7
- // wrote an `ensure-*.ts` script and a nightly assert (the cvskit evaluation,
8
- // §8 P1-7). They are now DECLARED in the feature's config block and CONVERGED
9
- // by `vxil push` and `POST /v1/apply`:
6
+ // wrote an `ensure-*.ts` script and a nightly assert. They are now DECLARED in
7
+ // the feature's config block and CONVERGED by `vxil push` and `POST /v1/apply`:
10
8
  //
11
9
  // rate-limits.policies[] — keyed by `name` (POST/PUT/DELETE /v1/rate-limits/policies)
12
10
  // webhooks.subscriptions[] — keyed by `target_url` (POST/PATCH/DELETE /v1/webhooks/subscriptions)
@@ -307,12 +305,12 @@ export interface ApiStateResponseField extends ApiStateSummary {
307
305
 
308
306
  /** The note every executor reports for a DISABLED feature whose block still
309
307
  * declares rows: the declaration is kept in the manifest and nothing is
310
- * converged (a parked feature is not a propagation window — review finding 2). */
308
+ * converged (a parked feature is not a propagation window). */
311
309
  export const API_STATE_DISABLED_NOTE = 'feature disabled — declaration kept, not converged';
312
310
 
313
311
  /** `GET /v1/rate-limits/policies` is a 100-row HARD cap with no cursor. A list
314
312
  * AT the cap is not a complete live set, so planning creates against it would
315
- * re-POST every declared name the cap hid (review finding 5). The server's
313
+ * re-POST every declared name the cap hid. The server's
316
314
  * converge and the CLI's read-only plan refuse with this message instead. */
317
315
  export function rlPolicyListCapError(liveCount: number): string | null {
318
316
  if (liveCount < RL_POLICY_LIST_CAP) return null;
package/src/hooks.ts CHANGED
@@ -330,7 +330,7 @@ export function validateAst(root: Node): string[] {
330
330
  }
331
331
 
332
332
  // ── evaluator (bounded, deterministic interpreter) ──────────────────────────
333
- /** The VERIFIED caller/session context for read hooks (design §5.6). Read-only;
333
+ /** The VERIFIED caller/session context for read hooks (guide ch. 7). Read-only;
334
334
  * present only on the read path in end-user mode. In server mode / write path
335
335
  * the whole object is null. */
336
336
  export interface HookCaller {
@@ -663,7 +663,7 @@ export function runReadHooks(
663
663
  collection: string,
664
664
  rows: ReadonlyArray<Record<string, unknown>>,
665
665
  now: string,
666
- /** verified caller/session context (design §5.6) — read-only, exposed to
666
+ /** verified caller/session context (guide ch. 7) — read-only, exposed to
667
667
  * expressions as `caller.endUserId` / `caller.principal`. Omitted ⇒ null
668
668
  * (server-caller mode). */
669
669
  caller?: HookCaller | null,