@vxil/feature-configs 0.7.0 → 0.7.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/src/index.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  // Per-feature TypeBox config schemas + the 15-leaf cap analyzer.
2
- // Location note (journaled deviation): the docs place each schema inside its
3
- // feature Worker; the control plane must validate writes against the same
4
- // schema (architecture §7.1 stage 2), so schemas live in this shared package
2
+ // Location note: each schema belongs to its feature, but the config-write
3
+ // path must validate writes against the same schema, so schemas live in this
4
+ // shared package
5
5
  // and feature Workers import from here — one definition, two consumers.
6
6
  import { FormatRegistry, OptionalKind, Type, type Static, type TSchema } from '@sinclair/typebox';
7
7
  import { Value } from '@sinclair/typebox/value';
@@ -15,7 +15,7 @@ export * from './hooks.js';
15
15
  // Re-export the cms-rel read-model/cdc config gates (the pure-mirror split:
16
16
  // grammar validated here at config-write; field existence at runtime).
17
17
  export * from './readmodels.js';
18
- // Re-export the DECLARED API STATE planner (roadmap §4.11 P0-3): pure
18
+ // Re-export the DECLARED API STATE planner: pure
19
19
  // declared-vs-live reconciliation, shared by the control-plane apply path and
20
20
  // the CLI (`vxil plan/diff/push`), so the two push paths can never diverge.
21
21
  export * from './apiState.js';
@@ -29,7 +29,7 @@ if (!FormatRegistry.Has('email')) {
29
29
  FormatRegistry.Set('email', (v) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(v));
30
30
  }
31
31
 
32
- // ── RESERVED (vxil-COGS) CREDIT TYPES — single source of truth (audit F2) ─────
32
+ // ── RESERVED (vxil-COGS) CREDIT TYPES — single source of truth ─────────────────
33
33
  // `fn_cpu_ms` funds vxil's OWN function-compute cost-of-goods (the
34
34
  // functions "recover-by-price" meter). It must NEVER be grantable or consumable
35
35
  // by a tenant's own `payments:write` key, NOR mapped-in via ledger config — a
@@ -40,7 +40,7 @@ if (!FormatRegistry.Has('email')) {
40
40
  // config-write refusal share ONE list. payments-v1/core.ts re-exports these.
41
41
  export const RESERVED_CREDIT_TYPES: ReadonlySet<string> = new Set<string>(['fn_cpu_ms']);
42
42
 
43
- /** F33 (2026-09-25): a function binding's `retry.maxAttempts` ceiling, and the
43
+ /** A function binding's `retry.maxAttempts` ceiling, and the
44
44
  * binding kinds that may carry `retry` — the platform-delivered event lanes
45
45
  * (an http invoke returns its own status; a cron tick's retry would overlap
46
46
  * the next tick). Read by the schema, the deploy clamp and the CLI. */
@@ -53,7 +53,7 @@ export function isReservedCreditType(creditType: string): boolean {
53
53
  return RESERVED_CREDIT_TYPES.has(creditType);
54
54
  }
55
55
 
56
- // ── notifications template catalog (D4 per-locale overrides) ─────────────────
56
+ // ── notifications template catalog (per-locale overrides) ────────────────────
57
57
  // The SHIPPED template ids and the placeholders each body may interpolate. The
58
58
  // renderer itself lives in workers/notifications-v1/src/templates.ts (templates
59
59
  // are code, not rows); this table is the CONFIG-TIME half so a `vxil push` that
@@ -80,7 +80,7 @@ export const NOTIFICATION_TEMPLATE_SLOT_PLACEHOLDERS: Readonly<Record<string, re
80
80
  transactional: ['cta'],
81
81
  };
82
82
 
83
- /** Per-override caps (D4). Bodies are emails, not documents. */
83
+ /** Per-override caps. Bodies are emails, not documents. */
84
84
  export const NOTIF_OVERRIDE_SUBJECT_MAX = 500;
85
85
  export const NOTIF_OVERRIDE_BODY_MAX = 20_000;
86
86
  /** Whole-map caps: keeps one manifest (and the KV row every send reads) small. */
@@ -94,7 +94,7 @@ const NOTIF_PLACEHOLDER_RE = /\{\{\s*([a-zA-Z0-9_]+)\s*\}\}/g;
94
94
  const NOTIF_LOCALE_RE = /^[A-Za-z]{2,3}(?:-[A-Za-z0-9]{2,8}){0,3}$/;
95
95
 
96
96
  /**
97
- * D4 — validate `templates.overrides` at CONFIG-WRITE time. Returns [] when the
97
+ * Validate `templates.overrides` at CONFIG-WRITE time. Returns [] when the
98
98
  * bag is absent or clean. Every rule fails LOUD rather than shipping something
99
99
  * that silently renders wrong in a customer's inbox:
100
100
  * • overrides present with `allowOverride:false` → rejected (never inert);
@@ -189,12 +189,12 @@ export const NotificationsConfigSchema = Type.Object({
189
189
  // need no email account, so the mock path is zero-config. A cross-field
190
190
  // check in validateFeatureConfig requires it only when provider === 'resend'.
191
191
  resendApiKeyRef: Type.Optional(Type.String()),
192
- // Optional per-tenant Resend/Svix ENDPOINT secret ref (public.tenant_secrets,
193
- // envelope-encrypted under KEK_NOTIFICATIONS — same store as resendApiKeyRef).
192
+ // Optional per-tenant Resend/Svix ENDPOINT secret ref (a pointer into the
193
+ // tenant's encrypted secret store — same store as resendApiKeyRef).
194
194
  // When set, inbound Resend webhooks are verified with THIS tenant's secret
195
195
  // instead of the platform-wide PROVIDER_WEBHOOK_SECRET, binding the signature
196
196
  // to the tenant so a signed event for tenant A can never validate at tenant
197
- // B's webhook URL (audit H6 — mirrors payments revenuecat.webhookSecretRef).
197
+ // B's webhook URL (mirrors payments revenuecat.webhookSecretRef).
198
198
  webhookSecretRef: Type.Optional(Type.String()),
199
199
  // 'mock' exists for staging/e2e (deterministic provider); 'resend' is MVP;
200
200
  // 'ses' (2026-09-19) is the second real provider — Amazon SES v2, BYO IAM
@@ -206,7 +206,7 @@ export const NotificationsConfigSchema = Type.Object({
206
206
  // cap rule — funded by folding `rateLimit` into an Optional bag below);
207
207
  // NO `default: {}` so an absent bag stays absent (the auth `security`
208
208
  // precedent). Both refs are `secret:<name>` POINTERS into
209
- // public.tenant_secrets (feature='notifications', KEK_NOTIFICATIONS) —
209
+ // the tenant's encrypted secret store (feature='notifications') —
210
210
  // exactly how resendApiKeyRef resolves; the region is plain config (not a
211
211
  // secret) and is pattern-pinned to the AWS region grammar so a typo fails
212
212
  // at `vxil push` instead of as a DNS error on the first send. Required
@@ -228,7 +228,7 @@ export const NotificationsConfigSchema = Type.Object({
228
228
  // nested objects carry `default: {}` so Value.Default can materialize them
229
229
  // and then recurse into the leaf defaults.
230
230
  // `retry` became an OPTIONAL bag (2 leaves → 1, countLeaves counts an
231
- // Optional object as ONE) to fund `broadcast` below (M21/#4, 2026-07-18).
231
+ // Optional object as ONE) to fund `broadcast` below (2026-07-18).
232
232
  // It KEEPS `default: {}`, which Value.Default still materializes — so every
233
233
  // persisted manifest carries retry.{maxAttempts,backoff} exactly as before
234
234
  // (zero behavioral delta); only the TS type is now optional (workers read
@@ -246,7 +246,7 @@ export const NotificationsConfigSchema = Type.Object({
246
246
  { softBounceThreshold: Type.Integer({ default: 3 }) },
247
247
  { default: {} },
248
248
  ),
249
- // `rateLimit` became an OPTIONAL bag (2 leaves → 1, the M21 `retry` trick)
249
+ // `rateLimit` became an OPTIONAL bag (2 leaves → 1, the `retry` trick)
250
250
  // on 2026-09-19 to fund the `ses` credential bag above. It KEEPS
251
251
  // `default: {}`, so Value.Default still materializes
252
252
  // rateLimit.{perDay,perTenantSec} into every persisted manifest exactly as
@@ -259,8 +259,8 @@ export const NotificationsConfigSchema = Type.Object({
259
259
  },
260
260
  { default: {} },
261
261
  )),
262
- // `templates` became an OPTIONAL bag (1 leaf, the same M21 trick `retry`
263
- // uses) to fund the D4 per-locale `overrides` map WITHOUT moving the count:
262
+ // `templates` became an OPTIONAL bag (1 leaf, the same trick `retry`
263
+ // uses) to fund the per-locale `overrides` map WITHOUT moving the count:
264
264
  // countLeaves scores an Optional object as ONE. `default: {}` is KEPT, so
265
265
  // Value.Default still materializes `templates.allowOverride` into every
266
266
  // persisted manifest exactly as before — zero behavioral delta; only the TS
@@ -268,7 +268,7 @@ export const NotificationsConfigSchema = Type.Object({
268
268
  templates: Type.Optional(Type.Object(
269
269
  {
270
270
  allowOverride: Type.Boolean({ default: false }),
271
- // D4 (2026-09-10) — TENANT-AUTHORED PER-LOCALE OVERRIDES of the SHIPPED
271
+ // (2026-09-10) TENANT-AUTHORED PER-LOCALE OVERRIDES of the SHIPPED
272
272
  // template ids, as config DATA (a Record = 1 leaf, catalog size never
273
273
  // moves the count). Shape: { [templateId]: { [locale]: { subject?,
274
274
  // html?, text? } } }. Same escaped `{{placeholder}}` grammar as the
@@ -297,10 +297,10 @@ export const NotificationsConfigSchema = Type.Object({
297
297
  )),
298
298
  /** in-app inbox channel (send with channel: 'inbox' | 'both') */
299
299
  inboxEnabled: Type.Boolean({ default: false }),
300
- /** §11b.5 broadcast campaigns channel. Optional bag (= 1 leaf): absent means
300
+ /** Broadcast campaigns channel. Optional bag (= 1 leaf): absent means
301
301
  * disabled; per-campaign quiet_hours / freq_cap overrides live on the
302
302
  * notifications.campaigns ROW (tenant data), not here. Folded into the
303
- * canonical schema 2026-07-18 (M21/#4 — Value.Clean previously STRIPPED the
303
+ * canonical schema 2026-07-18 (Value.Clean previously STRIPPED the
304
304
  * worker-local extension, so campaigns 403'd via the real config path). */
305
305
  broadcast: Type.Optional(Type.Object({
306
306
  enabled: Type.Boolean({ default: false }),
@@ -324,28 +324,26 @@ export const NotificationsConfigSchema = Type.Object({
324
324
  // retry (Optional bag = 1), suppression.softBounceThreshold,
325
325
  // rateLimit (Optional bag = 1 — was rateLimit.{perDay,perTenantSec}, collapsed
326
326
  // 2026-09-19 to fund `ses` at zero net cost), templates (Optional bag = 1 — was
327
- // templates.allowOverride, collapsed 2026-09-10 to fund the D4 `overrides` map
327
+ // templates.allowOverride, collapsed 2026-09-10 to fund the `overrides` map
328
328
  // at zero net cost), inboxEnabled, broadcast (Optional bag = 1) → 15. Cap = 15 —
329
329
  // AT the cap; the next flag must collapse something. (`templates.overrides` is
330
330
  // a Record MAP inside the ONE optional templates leaf — DATA, not a flag — so
331
331
  // catalog size never moves it.)
332
332
 
333
333
  export type NotificationsConfig = Static<typeof NotificationsConfigSchema>;
334
- /** The §11b.5 broadcast bag as persisted (present ⇒ leaf defaults applied). */
334
+ /** The broadcast bag as persisted (present ⇒ leaf defaults applied). */
335
335
  export type BroadcastConfig = NonNullable<NotificationsConfig['broadcast']>;
336
336
 
337
337
  // ── jobs `generation` block: the ONE declaration of its defaults and bounds ──
338
- // (roadmap §4.10, 2026-09-25 — "dropping the jobs-v1 GENERATION_DEFAULTS
339
- // copy"). The JobsConfigSchema `generation` leaf reads these for its
338
+ // (2026-09-25 — one copy of the generation defaults, not two). The JobsConfigSchema `generation` leaf reads these for its
340
339
  // `default` / `minimum` / `maximum`, and workers/jobs-v1/src/generation.ts
341
340
  // imports them (jobs-v1 already depends on @vxil/feature-configs; this package
342
341
  // has no @vxil/types dependency, so the shared value lives here — the
343
342
  // RESERVED_CREDIT_TYPES precedent). A future edit changes one object; the
344
- // feature-configs unit test pins schema ↔ constant, and the CI gate
345
- // tests/ci/src/generation-defaults-single-source.test.ts pins that no second
346
- // object-literal declaration of the constant reappears anywhere.
343
+ // feature-configs unit test pins schema ↔ constant, and a CI gate pins that no
344
+ // second object-literal declaration of the constant reappears anywhere.
347
345
 
348
- /** Generation-lifecycle config defaults (jobs.md §11 / §5 ≤15-flag budget). */
346
+ /** Generation-lifecycle config defaults (guide ch. 6, jobs). */
349
347
  export const GENERATION_DEFAULTS = {
350
348
  /** per-tenant in-flight generation cap (separate budget from queue jobs) */
351
349
  maxConcurrent: 20,
@@ -399,7 +397,7 @@ export const JobsConfigSchema = Type.Object({
399
397
  { maxConcurrent: Type.Integer({ default: 10, minimum: 1, maximum: 100 }) },
400
398
  { default: {} },
401
399
  ),
402
- // 2.F6 generation lifecycle knobs (jobs.md §11). The defaults and bounds are
400
+ // Generation lifecycle knobs (guide ch. 6, jobs). The defaults and bounds are
403
401
  // declared ONCE, as GENERATION_DEFAULTS / GENERATION_BOUNDS below this schema
404
402
  // (2026-09-25): jobs-v1's resolveGenerationConfig imports them and clamps a
405
403
  // pre-fold manifest to the same numbers — a hand-mirrored copy used to live
@@ -414,14 +412,14 @@ export const JobsConfigSchema = Type.Object({
414
412
  maxTimeoutMs: Type.Integer({ default: GENERATION_DEFAULTS.maxTimeoutMs, minimum: GENERATION_BOUNDS.maxTimeoutMs.min, maximum: GENERATION_BOUNDS.maxTimeoutMs.max }),
415
413
  /** poll-mode: how many poll cycles before giving up (→ terminal-fail) */
416
414
  pollMaxAttempts: Type.Integer({ default: GENERATION_DEFAULTS.pollMaxAttempts, minimum: GENERATION_BOUNDS.pollMaxAttempts.min, maximum: GENERATION_BOUNDS.pollMaxAttempts.max }),
417
- /** MANDATORY per-hold cap on a generation `reserve_credits.amount` (jobs.md
418
- * §11.8). Every requested amount is CLAMPED to this (never rejected) — a
415
+ /** MANDATORY per-hold cap on a generation `reserve_credits.amount`
416
+ * (guide ch. 6, jobs). Every requested amount is CLAMPED to this (never rejected) — a
419
417
  * conservative default so an untrusted deployed function that carries a
420
418
  * reserve block can never hold more than a bounded amount per run without
421
419
  * any tenant action. */
422
420
  maxReserveCredits: Type.Integer({ default: GENERATION_DEFAULTS.maxReserveCredits, minimum: GENERATION_BOUNDS.maxReserveCredits.min, maximum: GENERATION_BOUNDS.maxReserveCredits.max }),
423
421
  /** MANDATORY per-tenant ceiling on the SUM of un-settled provisional
424
- * reserve holds across all in-flight generation runs (jobs.md §11.8): a
422
+ * reserve holds across all in-flight generation runs (guide ch. 6, jobs): a
425
423
  * reserve whose amount would push the tenant's outstanding-holds total over
426
424
  * this is rejected 429, so a runaway function cannot hold every user at
427
425
  * once. Defaulted so no tenant action is required to be safe. */
@@ -435,43 +433,42 @@ export const JobsConfigSchema = Type.Object({
435
433
  export type JobsConfig = Static<typeof JobsConfigSchema>;
436
434
 
437
435
  // One social-provider's BYO credential block. The *Ref fields are POINTERS into
438
- // public.tenant_secrets (envelope-encrypted) — never raw secrets (features/auth.md
439
- // §6.5). google/github/facebook share the clientId/secret shape; apple is distinct
436
+ // the tenant's encrypted secret store — never raw secrets (guide ch. 6, auth).
437
+ // google/github/facebook share the clientId/secret shape; apple is distinct
440
438
  // (Sign in with Apple has no static secret — it mints an ES256 client_secret from
441
- // the .p8, so it carries servicesId/teamId/keyId + a p8 keyRef, §6.4).
439
+ // the .p8, so it carries servicesId/teamId/keyId + a p8 keyRef).
442
440
  const OAuthRefsSchema = Type.Object({
443
441
  // clientId/secret refs are OPTIONAL at the schema layer: a tenant may stage a
444
442
  // partial block, and the worker enforces presence at use (→ 501 if missing),
445
443
  // matching the worker's OAuthProviderRefs shape (core.ts). They stay POINTERS
446
- // into tenant_secrets — never raw secrets (features/auth.md §6.5).
444
+ // into the tenant's secret store — never raw secrets (guide ch. 6, auth).
447
445
  clientIdRef: Type.Optional(Type.String()),
448
446
  clientSecretRef: Type.Optional(Type.String()),
449
447
  // extra native-aud allow-list entries (iOS/web client ids that differ from the
450
- // primary clientIdRef) — also tenant_secrets refs (features/auth.md §6.4).
448
+ // primary clientIdRef) — also secret-store refs (guide ch. 6, auth).
451
449
  audRefs: Type.Optional(Type.Array(Type.String(), { maxItems: 16 })),
452
450
  });
453
451
  const AppleRefsSchema = Type.Object({
454
452
  servicesId: Type.String(), // the OAuth client_id / native aud (NOT a secret)
455
453
  teamId: Type.String(),
456
454
  keyId: Type.String(),
457
- p8KeyRef: Type.String(), // tenant_secrets ref → envelope-encrypted .p8 PEM
455
+ p8KeyRef: Type.String(), // secret-store ref → the encrypted .p8 PEM
458
456
  // extra native-aud allow-list entries: genuine iOS ASAuthorization id_tokens
459
- // carry the app BUNDLE ID as aud, not the Services ID (auth.md §6.4). Plain
457
+ // carry the app BUNDLE ID as aud, not the Services ID (guide ch. 6, auth). Plain
460
458
  // config values — bundle ids are not secrets. The worker already honors them
461
459
  // (oauthCore.ts nativeAudAllowList); declaring them here is what stops
462
460
  // Value.Clean stripping the field out of PUT /v1/config/auth.
463
461
  bundleIds: Type.Optional(Type.Array(Type.String(), { maxItems: 16 })),
464
462
  });
465
- // RB-2 — the generic OIDC / SSO bridge (roadmap §4.6.B, §1B.5; features/auth.md
466
- // §6.7). ONE tenant-supplied issuer, declared INSIDE the existing `providers`
463
+ // The generic OIDC / SSO bridge (guide ch. 6, auth). ONE tenant-supplied issuer, declared INSIDE the existing `providers`
467
464
  // bag so it spends NO leaf (an Optional object bag = ONE leaf; auth stays 11).
468
465
  // Declarative + opt-in: the block's PRESENCE enables the `oidc` provider (no
469
466
  // methods.* toggle — a `default:false` flag would re-materialize every published
470
- // manifest, breaking the D3 byte-identical fold). The worker discovers the
467
+ // manifest, breaking the byte-identical fold). The worker discovers the
471
468
  // endpoints + JWKS from `{issuer}/.well-known/openid-configuration` and verifies
472
469
  // iss (byte-equal) / aud (= clientId) / nonce / exp / iat strictly, fail-closed.
473
470
  // `clientId` is NOT a secret (it rides every authorize URL); `clientSecretRef`
474
- // is a tenant_secrets POINTER (feature 'auth'), never the value. A SAML IdP
471
+ // is a secret-store POINTER (feature 'auth'), never the value. A SAML IdP
475
472
  // plugs in through a broker (Okta / Entra / Auth0 / WorkOS) that speaks OIDC —
476
473
  // there is deliberately NO native SAML.
477
474
  const OidcProviderSchema = Type.Object({
@@ -500,7 +497,7 @@ const OidcProviderSchema = Type.Object({
500
497
  { maxItems: 32 },
501
498
  )),
502
499
  // default true: an identity whose VERIFIED email matches an existing user is
503
- // linked to it (the shipped §6.4 rule). false: link only by the stable
500
+ // linked to it (the shipped social sign-in rule). false: link only by the stable
504
501
  // (issuer, sub) anchor; a matching email that is not yet linked → 409.
505
502
  autoLink: Type.Optional(Type.Boolean()),
506
503
  });
@@ -510,7 +507,7 @@ const OidcProviderSchema = Type.Object({
510
507
  // on every caller-supplied `redirect_url` / `redirect_uri` (core.ts
511
508
  // `isAllowedRedirectUrl`) must agree on WHICH schemes a sign-in link may be
512
509
  // delivered to, or a tenant could allow-list an origin the worker then refuses
513
- // (or the reverse). The rule (cvskit gap 7, 2026-09-19): `https://` on any
510
+ // (or the reverse). The rule (2026-09-19): `https://` on any
514
511
  // host, plus PLAIN `http://` ONLY on the two loopback names — `localhost` and
515
512
  // `127.0.0.1`, any port — so a local dev server can complete a magic link
516
513
  // without a staging origin. Any other `http://` (a LAN host, `*.local`, a
@@ -521,7 +518,7 @@ const OidcProviderSchema = Type.Object({
521
518
  // with THIS predicate, and auth-v1 calls it on the parsed URL — the parity test
522
519
  // in index.test.ts / redirectRule.test.ts holds the two together. The CLI's
523
520
  // production promotion gate still refuses any `http://` origin on a
524
- // production push (cli-sdk.md), so a loopback entry is a DEV-tenant affair.
521
+ // production push, so a loopback entry is a DEV-tenant affair.
525
522
  export const REDIRECT_ORIGIN_PATTERN =
526
523
  '^(https://[^/?#\\s]+|http://(localhost|127\\.0\\.0\\.1)(:[0-9]{1,5})?)$';
527
524
 
@@ -547,13 +544,13 @@ export function isAllowedRedirectOriginEntry(entry: string): boolean {
547
544
 
548
545
  export const AuthConfigSchema = Type.Object({
549
546
  enabled: Type.Boolean({ default: true }),
550
- // D3 (auth wave 2026-09-10): the six method toggles collapsed into ONE
547
+ // (2026-09-10) the six method toggles collapsed into ONE
551
548
  // Optional bag (6 leaves → 1, countLeaves counts an Optional object as ONE) —
552
549
  // the notifications `retry`/`broadcast` precedent. It KEEPS `default: {}`,
553
550
  // which Value.Default still materializes, so every persisted manifest carries
554
551
  // methods.{emailPassword,magicLink,google,github,apple,facebook} with the
555
552
  // SAME keys and defaults as before — byte-identical folds for existing
556
- // tenants (index.test.ts "D3 fold bytes"). Only the TS type is now optional;
553
+ // tenants (index.test.ts pins the fold bytes). Only the TS type is now optional;
557
554
  // the worker's gate() normalizes an absent bag to the defaults so every
558
555
  // reader (config.methods.<flag>) is unchanged.
559
556
  methods: Type.Optional(Type.Object(
@@ -571,12 +568,12 @@ export const AuthConfigSchema = Type.Object({
571
568
  // rule) keyed by provider, so the four provider blocks (and any future one)
572
569
  // never inflate the flag count — the prior shape spent a leaf per top-level
573
570
  // google/github block. This bag REPLACES those two top-level blocks (−2, +1
574
- // for the bag) and EXTENDS the accepted providers to apple + facebook (§6):
571
+ // for the bag) and EXTENDS the accepted providers to apple + facebook:
575
572
  // • google/github/facebook → { clientIdRef, clientSecretRef, audRefs? }
576
573
  // • apple → { servicesId, teamId, keyId, p8KeyRef }
577
- // All *Ref fields are tenant_secrets POINTERS, never raw secrets (§6.5). The
574
+ // All *Ref fields are secret-store POINTERS, never raw secrets. The
578
575
  // worker reads config.providers?.{google,github,apple,facebook} (oauthCore.ts).
579
- // The §6.4 runtime sign-in flow for apple/facebook is SHIPPED in oauthCore.ts
576
+ // The runtime sign-in flow for apple/facebook is SHIPPED in oauthCore.ts
580
577
  // (id_token/access_token verification + ES256 Apple client_secret minting);
581
578
  // methods.{apple,facebook} above are the enable toggles it gates on.
582
579
  providers: Type.Optional(Type.Object({
@@ -584,11 +581,11 @@ export const AuthConfigSchema = Type.Object({
584
581
  github: Type.Optional(OAuthRefsSchema),
585
582
  apple: Type.Optional(AppleRefsSchema),
586
583
  facebook: Type.Optional(OAuthRefsSchema),
587
- // RB-2: the generic OIDC / SSO issuer (presence = enabled; see above)
584
+ // the generic OIDC / SSO issuer (presence = enabled; see above)
588
585
  oidc: Type.Optional(OidcProviderSchema),
589
586
  })),
590
587
  // session/password are OPTIONAL bags (= ONE leaf each per the cap rule) since
591
- // the OTP/anonymous/orgClaims wave — the `{ default: {} }` keeps the inner
588
+ // the OTP/anonymous/orgClaims release — the `{ default: {} }` keeps the inner
592
589
  // defaults materializing on publish, so the worker still reads fully-populated
593
590
  // manifests; its `config.session?.ttlMinutes ?? 60` fallbacks cover sparse
594
591
  // hand-built manifests only.
@@ -596,7 +593,7 @@ export const AuthConfigSchema = Type.Object({
596
593
  {
597
594
  ttlMinutes: Type.Integer({ default: 60, minimum: 5, maximum: 1440 }),
598
595
  refreshTtlDays: Type.Integer({ default: 30, minimum: 1, maximum: 365 }),
599
- // A1 (auth wave 2026-09-10): concurrent-session cap per user with
596
+ // (2026-09-10) concurrent-session cap per user with
600
597
  // TAKE-OVER — a new sign-in revokes the OLDEST sessions past the cap
601
598
  // (revoked_reason 'device_cap', edge cache written) and reports them as
602
599
  // `took_over: [session_id…]`. Optional WITHOUT a default so existing
@@ -612,15 +609,15 @@ export const AuthConfigSchema = Type.Object({
612
609
  },
613
610
  { default: {} },
614
611
  )),
615
- // Deviation (journaled): emailVerification.tokenTtlHours was DROPPED —
612
+ // Note: emailVerification.tokenTtlHours was DROPPED —
616
613
  // consumer-less (grep-verified: only this schema + dist mentioned it; the
617
614
  // verify-email token flow it would bound was never built). Same precedent as
618
- // the removed `redirects` block below. Its leaf funds the OTP wave.
615
+ // the removed `redirects` block below. Its leaf funds the OTP sign-in bag.
619
616
  emailVerification: Type.Object(
620
617
  { required: Type.Boolean({ default: false }) },
621
618
  { default: {} },
622
619
  ),
623
- // P1-16 (2026-09-18): magicLink is now an OPTIONAL bag (= ONE leaf however
620
+ // (2026-09-18) magicLink is now an OPTIONAL bag (= ONE leaf however
624
621
  // many knobs it holds — the methods/session/password precedent) so the
625
622
  // request cooldown could land without spending a second leaf. It KEEPS
626
623
  // `default: {}`, so Value.Default still materializes
@@ -629,7 +626,7 @@ export const AuthConfigSchema = Type.Object({
629
626
  magicLink: Type.Optional(Type.Object(
630
627
  {
631
628
  tokenTtlMinutes: Type.Integer({ default: 15, minimum: 5, maximum: 60 }),
632
- // P1-16: the per-(tenant, identifier) magic-link REQUEST cooldown — the
629
+ // the per-(tenant, identifier) magic-link REQUEST cooldown — the
633
630
  // `otp.resendCooldownSec` twin, same bounds so the two knobs read the
634
631
  // same. A second request for the same address inside the window is a 429
635
632
  // `magic_link_rate_limited` with a Retry-After header. Type.Optional with
@@ -640,19 +637,19 @@ export const AuthConfigSchema = Type.Object({
640
637
  },
641
638
  { default: {} },
642
639
  )),
643
- // P1-5 identity continuity (2026-09-18) — OPT-IN registry adoption. When
640
+ // Identity continuity (2026-09-18) — OPT-IN registry adoption. When
644
641
  // true, a sign-in by a method that PROVES control of the address — magic
645
642
  // link, email OTP, or OAuth with a provider-verified address — reuses the id
646
643
  // of the one matching pre-registered end-user (POST /v1/users) that has no
647
644
  // account yet, instead of minting a fresh `user_<ulid>` and leaving the
648
645
  // tenant with two records for one person. Password SIGN-UP never adopts: it
649
- // proves nothing about the address (auth.md §8.1 PROVEN_ADOPT_METHODS).
646
+ // proves nothing about the address (guide ch. 6, auth).
650
647
  // OFF by default, and Type.Optional with NO default: adoption means whoever
651
648
  // proves control of a pre-registered address becomes that record — a change
652
649
  // of security semantics for a tenant that bulk-imports contacts, whose
653
650
  // addresses vxil never verified.
654
651
  registryAdopt: Type.Optional(Type.Boolean()),
655
- // Email OTP sign-in (roadmap Tier-0) + the knobs step-up re-auth shares.
652
+ // Email OTP sign-in + the knobs step-up re-auth shares.
656
653
  // OPTIONAL bag = 1 leaf; absent ⇒ disabled (the worker gates on
657
654
  // otp?.enabled === true).
658
655
  otp: Type.Optional(Type.Object({
@@ -660,7 +657,7 @@ export const AuthConfigSchema = Type.Object({
660
657
  codeTtlMinutes: Type.Integer({ default: 10, minimum: 1, maximum: 60 }),
661
658
  maxAttempts: Type.Integer({ default: 5, minimum: 3, maximum: 10 }),
662
659
  resendCooldownSec: Type.Integer({ default: 60, minimum: 0, maximum: 600 }),
663
- // F4-29 (auth wave 2026-09-10): test recipients — an OTP / step-up / email-
660
+ // (2026-09-10) test recipients — an OTP / step-up / email-
664
661
  // claim request whose address matches an entry sends NO mail and returns
665
662
  // the code as `test_code` (audit auth.otp.test_issued). Entries: an exact
666
663
  // email, a `*@domain` glob, or a +E.164 number (accepted for the SMS
@@ -671,11 +668,11 @@ export const AuthConfigSchema = Type.Object({
671
668
  Type.String({ minLength: 3, maxLength: 320 }), { maxItems: 20 },
672
669
  )),
673
670
  })),
674
- // Anonymous (guest) sign-in (roadmap Tier-0). OPTIONAL bag = 1 leaf.
671
+ // Anonymous (guest) sign-in. OPTIONAL bag = 1 leaf.
675
672
  anonymous: Type.Optional(Type.Object({
676
673
  enabled: Type.Boolean({ default: false }),
677
674
  })),
678
- // Org claims embedded in session JWTs at mint/refresh (roadmap Tier-1C):
675
+ // Org claims embedded in session JWTs at mint/refresh:
679
676
  // when enabled, auth-v1 fetches the user's active-org membership from orgs
680
677
  // over the EDGE and embeds { org_id, role, perms[] } as the `org` claim.
681
678
  // Fail-open: an orgs outage mints WITHOUT claims (sign-in never breaks).
@@ -684,23 +681,22 @@ export const AuthConfigSchema = Type.Object({
684
681
  orgClaims: Type.Optional(Type.Object({
685
682
  enabled: Type.Boolean({ default: false }),
686
683
  })),
687
- // Deviation (journaled): the doc's §4 `redirects` block was DROPPED entirely —
684
+ // Note: the earlier `redirects` block was DROPPED entirely —
688
685
  // it had zero consumers (grep-verified: no worker reads config.redirects) and
689
686
  // its leaf was spent on the methods.{apple,facebook} toggles the shipped
690
- // §6.4 sign-in flow actually gates on. Re-adding it requires headroom or a
687
+ // social sign-in flow actually gates on. Re-adding it requires headroom or a
691
688
  // collapse elsewhere.
692
689
  //
693
- // Account-security controls (auth wave 2026-09-10, P0-3; features/auth.md
694
- // §4b). ONE optional bag = 1 leaf; NO `default: {}` so an absent bag stays
690
+ // Account-security controls (2026-09-10; guide ch. 6, auth). ONE optional bag = 1 leaf; NO `default: {}` so an absent bag stays
695
691
  // absent (existing manifests fold byte-identically) and every control is
696
692
  // opt-in:
697
693
  // • lockout — present ⇒ per-(tenant, identifier) failure lockout on password
698
694
  // sign-in + OTP verify (429 account_locked + Retry-After); a bounded
699
- // counter row in auth.lockouts (migration 0088) survives restarts.
695
+ // stored counter survives restarts.
700
696
  // • breachedPasswords — HIBP k-anonymity range check (first 5 SHA-1 hex
701
697
  // chars leave the worker, never the password) at sign-up / reset-confirm;
702
698
  // fail-OPEN on network error → 422 password_breached.
703
- // • captchaSecretRef — a tenant_secrets ref (feature 'auth') holding the
699
+ // • captchaSecretRef — a secret-store ref (feature 'auth') holding the
704
700
  // Turnstile secret; when set, sign-up / OTP request / magic-link request
705
701
  // require `captcha_token` (403 captcha_failed otherwise).
706
702
  // • allowedRedirectOrigins — when non-empty, EVERY caller-supplied
@@ -725,24 +721,24 @@ export const AuthConfigSchema = Type.Object({
725
721
  });
726
722
  /** The security bag as persisted (present ⇒ leaf defaults applied). */
727
723
  export type AuthSecurityConfig = NonNullable<Static<typeof AuthConfigSchema>['security']>;
728
- /** The RB-2 generic OIDC / SSO issuer block (`providers.oidc`). */
724
+ /** The generic OIDC / SSO issuer block (`providers.oidc`). */
729
725
  export type AuthOidcConfig = Static<typeof OidcProviderSchema>;
730
- // Leaves (auth wave 2026-09-10, D3): enabled(1) + methods(1, optional bag —
726
+ // Leaves (2026-09-10): enabled(1) + methods(1, optional bag —
731
727
  // was 6) + providers(1, optional bag holding the four google/github/apple/
732
- // facebook credential blocks + the RB-2 `oidc` issuer block) + session(1, optional bag) + password(1,
728
+ // facebook credential blocks + the `oidc` issuer block) + session(1, optional bag) + password(1,
733
729
  // optional bag) + emailVerification(1) + magicLink(1, optional bag) + otp(1, optional bag)
734
730
  // + anonymous(1, optional bag) + orgClaims(1, optional bag) + security(1,
735
- // optional bag) + registryAdopt(1, P1-5) = 12. Cap = 15 — 3 leaves of
731
+ // optional bag) + registryAdopt(1) = 12. Cap = 15 — 3 leaves of
736
732
  // headroom. Knobs added inside an existing bag (session.maxConcurrent,
737
733
  // otp.testRecipients, magicLink.resendCooldownSec, the security sub-bags)
738
734
  // never move the count.
739
735
 
740
736
  export type AuthConfig = Static<typeof AuthConfigSchema>;
741
737
 
742
- // ── DECLARED API STATE (roadmap §4.11 P0-3, 2026-09-23) ──────────────────────
738
+ // ── DECLARED API STATE (2026-09-23) ──────────────────────────────────────────
743
739
  // A rate-limit policy is a ROW in the feature's own KV store, written by
744
740
  // POST/PUT/DELETE /v1/rate-limits/policies. It used to be API state OUTSIDE
745
- // vxil.config (a journaled deviation: "one writer per datum, and the writer is
741
+ // vxil.config (by the old rule "one writer per datum, and the writer is
746
742
  // the route"), which made a second environment non-reproducible — every tenant
747
743
  // ended up with an `ensure-rate-limit-policies.ts` + a nightly assert. The
748
744
  // rule is unchanged — one writer per datum — but the writer is now THE
@@ -793,7 +789,7 @@ export type RateLimitsConfig = Static<typeof RateLimitsConfigSchema>;
793
789
 
794
790
  export const FilesConfigSchema = Type.Object({
795
791
  enabled: Type.Boolean({ default: true }),
796
- // (F8-54) `bucketRef` was DELETED: the object-storage bucket is a platform
792
+ // `bucketRef` was DELETED (2026-09-11): the object-storage bucket is a platform
797
793
  // binding on files-v1, never tenant-selectable, and no code ever read the
798
794
  // leaf — declaring it invited "point files at my own bucket", which vxil does
799
795
  // not offer. A persisted manifest that still carries it folds (Value.Clean).
@@ -802,8 +798,8 @@ export const FilesConfigSchema = Type.Object({
802
798
  quotas: Type.Object(
803
799
  {
804
800
  // maximum caps are a defense-in-depth ceiling on tenant-editable storage —
805
- // object storage is cheap but the SQL-database-resident metadata + abuse aren't (pricing re-audit
806
- // 2026-07-10). 5 GiB/object, 1 TiB/tenant absolute; true PER-TIER clamps
801
+ // object storage is cheap but the database-resident metadata + abuse aren't
802
+ // (pricing review 2026-07-10). 5 GiB/object, 1 TiB/tenant absolute; true PER-TIER clamps
807
803
  // (Free/Dev 10 GB · Team 50 GB · Business 256 GB) are a follow-up needing the
808
804
  // tenant tier threaded to files-v1 (plan tiers).
809
805
  maxObjectBytes: Type.Integer({ default: 100 * 1024 * 1024, minimum: 1, maximum: 5 * 1024 * 1024 * 1024 }),
@@ -813,7 +809,7 @@ export const FilesConfigSchema = Type.Object({
813
809
  { default: {} },
814
810
  ),
815
811
  allowedContentTypes: Type.Array(Type.String(), { default: ['*'], maxItems: 100 }),
816
- // (F8-54) the `contentScan` bag ({enabled, quarantineOnFail}) was DELETED:
812
+ // (2026-09-11) the `contentScan` bag ({enabled, quarantineOnFail}) was DELETED:
817
813
  // there is no malware/content scanner in files-v1 (it was a "V1.5" placeholder
818
814
  // neither leaf was ever read), so the knob promised quarantine that never
819
815
  // happened. Re-declare it in the same change as a real scanner, not before.
@@ -824,7 +820,7 @@ export const FilesConfigSchema = Type.Object({
824
820
  },
825
821
  { default: {} },
826
822
  ),
827
- // Wave-2 extensions (features/files.md §1.1 OCR + §1.2 TTL) — the merge of
823
+ // Wave-2 extensions (guide ch. 6, files: OCR + TTL) — the merge of
828
824
  // workers/files-v1/src/ext.ts FilesExtensionsConfigSchema promised by its
829
825
  // 'wiring phase' comment. Each is an OPTIONAL bag (= ONE leaf per the cap
830
826
  // rule); files-v1 already reads both defensively (FilesConfigWithExt), so
@@ -842,21 +838,20 @@ export const FilesConfigSchema = Type.Object({
842
838
  [Type.Literal('gcv'), Type.Literal('textract'), Type.Literal('azure-di'), Type.Literal('mock')],
843
839
  { default: 'mock' },
844
840
  ),
845
- // provider key is BYO + envelope-encrypted in public.tenant_secrets — NOT a
846
- // config flag. keyRef names the tenant_secrets row (like ai's keyRefs).
841
+ // provider key is BYO + encrypted in the tenant's secret store — NOT a
842
+ // config flag. keyRef names the stored secret (like ai's keyRefs).
847
843
  keyRef: Type.Optional(Type.String()),
848
844
  asyncOverJobs: Type.Boolean({ default: true }), // large/multi-page → jobs
849
845
  boundingBoxes: Type.Boolean({ default: false }),
850
846
  })),
851
847
  });
852
848
  // Leaves: 9 + ttl(1, optional bag) + extractText(1, optional bag) = 11. Cap = 15.
853
- // (was 14 — F8-54 deleted the inert bucketRef leaf and the inert contentScan bag.)
849
+ // (was 14 — 2026-09-11 deleted the inert bucketRef leaf and the inert contentScan bag.)
854
850
 
855
851
  export type FilesConfig = Static<typeof FilesConfigSchema>;
856
852
 
857
853
  // webhooks-out (wishlist feature): outbound event fan-out over the jobs delivery
858
- // engine. Subscriptions live in their own table (§1 contract — one writer
859
- // per datum); config is just the capability gate + a cap.
854
+ // engine. Subscriptions live in their own store (one writer per datum); config is just the capability gate + a cap.
860
855
  export const DeclaredWebhookSubscriptionSchema = Type.Object({
861
856
  target_url: Type.String({ minLength: 9, maxLength: 2000 }),
862
857
  event_prefixes: Type.Optional(Type.Array(Type.String({ minLength: 1, maxLength: 100 }), { maxItems: 20 })),
@@ -867,14 +862,14 @@ export const WebhooksConfigSchema = Type.Object({
867
862
  enabled: Type.Boolean({ default: true }),
868
863
  maxSubscriptions: Type.Integer({ default: 20, minimum: 1, maximum: 200 }),
869
864
  maxSources: Type.Integer({ default: 20, minimum: 1, maximum: 200 }),
870
- // FAILURE-ALERT DIGEST (P0-2). Outbound subscriptions are the real-time
865
+ // FAILURE-ALERT DIGEST. Outbound subscriptions are the real-time
871
866
  // channel; this is the "nobody is consuming them yet" fallback — a periodic
872
867
  // e-mail summary of the tenant's FAILURE-class audit events (the level:
873
868
  // 'failure' rows of the generated event catalog). Read by
874
869
  // workers/control-plane/src/alertDigest.ts on the minute cron.
875
870
  //
876
- // RECIPIENTS ARE DELIBERATELY NOT CONFIGURABLE, and this deviation is
877
- // journaled here the way RateLimitsConfigSchema journals its own: an
871
+ // RECIPIENTS ARE DELIBERATELY NOT CONFIGURABLE, and this decision is
872
+ // recorded here the way RateLimitsConfigSchema records its own: an
878
873
  // arbitrary `to` would turn vxil's own sending identity into a relay for
879
874
  // tenant-authored content and open a PII egress path out of the audit trail.
880
875
  // The digest goes to the OWNER-role dashboard accounts of the project (cap
@@ -884,7 +879,7 @@ export const WebhooksConfigSchema = Type.Object({
884
879
  //
885
880
  // An Optional object bag counts as ONE leaf (the countLeaves rule).
886
881
  //
887
- // `digestMinutes: 0` is IMMEDIATE (roadmap §4.11 P0-4c, 2026-09-23): the
882
+ // `digestMinutes: 0` is IMMEDIATE (2026-09-23): the
888
883
  // pass runs every minute for the tenant and mails the `error`-level failure
889
884
  // rows that landed since its last mail — at most one mail per minute, still
890
885
  // to the owner accounts. 1–4 are clamped up to 5 by the reader.
@@ -893,11 +888,11 @@ export const WebhooksConfigSchema = Type.Object({
893
888
  minLevel: Type.Union([Type.Literal('warn'), Type.Literal('error')], { default: 'error' }),
894
889
  digestMinutes: Type.Integer({ default: 15, minimum: 0, maximum: 1440 }),
895
890
  })),
896
- // DECLARED API STATE (roadmap §4.11 P0-3, 2026-09-23): the tenant's OUTBOUND
891
+ // DECLARED API STATE (2026-09-23): the tenant's OUTBOUND
897
892
  // subscriptions as config. Keyed by `target_url` — the only stable identity a
898
893
  // subscription has (there is no name column). A changed prefix set is an
899
894
  // in-place update — PATCH /v1/webhooks/subscriptions/:subId, same sub_id and
900
- // cursor (2026-09-25). `vxil push` / `POST /v1/apply` converge public.webhook_subscriptions
895
+ // cursor (2026-09-25). `vxil push` / `POST /v1/apply` converge the live subscriptions
901
896
  // onto this list (handlers/apiState.ts); undeclared live rows are LEFT and
902
897
  // reported (deleted only under --allow-destructive). Rows on the platform's
903
898
  // signed function-delivery lanes (/v1/internal/fn/…) are NEVER declared here
@@ -922,16 +917,16 @@ export const CommentsConfigSchema = Type.Object({
922
917
 
923
918
  export type CommentsConfig = Static<typeof CommentsConfigSchema>;
924
919
 
925
- // content feature (cms-feature-analysis.md §4.2): flags govern LIMITS, never the
920
+ // content feature (guide ch. 4): flags govern LIMITS, never the
926
921
  // content model — the model itself is data (cms.collections / cms.fields via
927
- // the REST surface). versioning/localization/publicRead land later waves.
922
+ // the REST surface). versioning/localization/publicRead land later.
928
923
  export const CmsConfigSchema = Type.Object({
929
924
  enabled: Type.Boolean({ default: true }),
930
925
  draftPublish: Type.Boolean({ default: true }),
931
- // cms end-user default-deny fail-safe (path-to-100 §3.2, Feature B). When ON,
926
+ // cms end-user default-deny fail-safe (guide ch. 4). When ON,
932
927
  // a VERIFIED end-user key (owner-scope mode) is DENIED access to any
933
928
  // collection that declares no owner_field — `403 server_only` on read AND
934
- // write (finding 29, 2026-09-20; reads used to be a 404) — instead of the
929
+ // write (2026-09-20; reads used to be a 404) — instead of the
935
930
  // default tenant-wide-shared behavior. Server-caller mode is a
936
931
  // byte-for-byte no-op. Default OFF preserves today's shared semantics
937
932
  // (owner.int.test.ts's shared-collection invariant). A collection that DOES
@@ -953,7 +948,7 @@ export const CmsConfigSchema = Type.Object({
953
948
  },
954
949
  { default: {} },
955
950
  ),
956
- // cms ENRICHMENT (cms.md §6.4): the read-time relation budget. The worker
951
+ // cms ENRICHMENT (guide ch. 4): the read-time relation budget. The worker
957
952
  // clamps via resolveRelationsConfig (enrich.ts) with the SAME defaults +
958
953
  // hard ceilings, so an out-of-range value can never widen the bound.
959
954
  relations: Type.Object(
@@ -993,7 +988,7 @@ export const CmsConfigSchema = Type.Object({
993
988
  }),
994
989
  ),
995
990
  ),
996
- // Declarative relational read-models (cms-relational-depth §3 B2/B3/B5).
991
+ // Declarative relational read-models (guide ch. 4).
997
992
  // Each is a NAMED, closed-grammar aggregate/rank spec, optionally
998
993
  // materialized to a rollup collection on the EXISTING jobs cron (the
999
994
  // fn-cron:* reconciler idiom → cms-rollup:* schedules). Grammar is validated
@@ -1004,7 +999,7 @@ export const CmsConfigSchema = Type.Object({
1004
999
  Type.Object({
1005
1000
  collection: Type.String({ maxLength: 64 }),
1006
1001
  kind: Type.Union([Type.Literal('aggregate'), Type.Literal('rank')]),
1007
- // the §3.1/§4.1 body minus limit — Type.Unknown so Value.Clean keeps it
1002
+ // the aggregate/rank query body minus limit — Type.Unknown so Value.Clean keeps it
1008
1003
  // (the functions `signature` idiom); shape checked by the cross-field rule.
1009
1004
  spec: Type.Unknown(),
1010
1005
  materialize: Type.Optional(Type.Object({
@@ -1039,7 +1034,7 @@ export const CmsConfigSchema = Type.Object({
1039
1034
 
1040
1035
  export type CmsConfig = Static<typeof CmsConfigSchema>;
1041
1036
 
1042
- // mcp feature (mcp.md §5): the aggregation surface's own knobs. Default-enabled —
1037
+ // mcp feature (guide ch. 10): the aggregation surface's own knobs. Default-enabled —
1043
1038
  // a tenant with no mcp config row still gets the aggregated tool list.
1044
1039
  export const McpConfigSchema = Type.Object({
1045
1040
  enabled: Type.Boolean({ default: true }),
@@ -1048,7 +1043,7 @@ export const McpConfigSchema = Type.Object({
1048
1043
  { default: 'all' },
1049
1044
  ),
1050
1045
  allowToolList: Type.Optional(Type.Array(Type.String(), { maxItems: 200 })),
1051
- // (F8-54) the `scopedKey.perAgentKeys` leaf was DELETED: it was documented as
1046
+ // (2026-09-11) the `scopedKey.perAgentKeys` leaf was DELETED: it was documented as
1052
1047
  // a "dashboard-UI hint only" that no dashboard ever read, and key minting is a
1053
1048
  // control-plane concern independent of MCP exposure — per-agent keys already
1054
1049
  // work for every tenant, gated by nothing here.
@@ -1064,7 +1059,7 @@ export const McpConfigSchema = Type.Object({
1064
1059
  { default: {} },
1065
1060
  ),
1066
1061
 
1067
- // Tenant-authored CUSTOM tools (mcp.md §6.5): name → tenant-owned https
1062
+ // Tenant-authored CUSTOM tools (guide ch. 10): name → tenant-owned https
1068
1063
  // endpoint. mcp-v1 lists each as `custom_<name>` and POSTs the tool
1069
1064
  // arguments to `url`, HMAC-signed with the per-tenant key from
1070
1065
  // GET /v1/mcp/signing-secret (X-Vxil-Mcp-Signature; the caller's vxil bearer
@@ -1084,7 +1079,7 @@ export const McpConfigSchema = Type.Object({
1084
1079
  }),
1085
1080
  )),
1086
1081
 
1087
- // Config-declared MCP PROMPTS (mcp.md §11 closure): name → template with
1082
+ // Config-declared MCP PROMPTS (guide ch. 10): name → template with
1088
1083
  // {{placeholder}} interpolation. Served verbatim by mcp-v1 prompts/list +
1089
1084
  // prompts/get. ONE Type.Record leaf; placeholder ↔ arguments consistency is
1090
1085
  // a cross-field rule below.
@@ -1104,7 +1099,7 @@ export const McpConfigSchema = Type.Object({
1104
1099
  // Leaves: 8 by countLeaves (enabled, exposureLevel, allowToolList,
1105
1100
  // rateLimits.toolCallsPerMin, branding.serverName, branding.serverInstructions
1106
1101
  // = 6, + customTools + prompts as ONE Type.Record leaf each). Cap = 15.
1107
- // (was 9 — F8-54 deleted the inert scopedKey.perAgentKeys leaf.)
1102
+ // (was 9 — 2026-09-11 deleted the inert scopedKey.perAgentKeys leaf.)
1108
1103
 
1109
1104
  export type McpConfig = Static<typeof McpConfigSchema>;
1110
1105
 
@@ -1136,7 +1131,7 @@ export const OrgsConfigSchema = Type.Object({
1136
1131
 
1137
1132
  export type OrgsConfig = Static<typeof OrgsConfigSchema>;
1138
1133
 
1139
- // activity-feed feature (features/activity-feed.md §3): a GetStream-class activity-
1134
+ // activity-feed feature (guide ch. 6, activity-feed): a GetStream-class activity-
1140
1135
  // streams engine + a Knock/Novu-class in-app notification FEED. Flags govern
1141
1136
  // the fan-out throttle, the follow/aggregation caps, and the cross-channel /
1142
1137
  // realtime gates — NEVER the verb vocabulary or the personalized ranker (the
@@ -1156,8 +1151,8 @@ export const ActivityFeedConfigSchema = Type.Object({
1156
1151
  Type.Literal('aggregated'),
1157
1152
  Type.Literal('notification'),
1158
1153
  ]),
1159
- aggregation: Type.Optional(Type.String()), // group-format rule (§7); required for aggregated/notification
1160
- ranking: Type.Optional(Type.String()), // 'chronological' | 'decay' (§8); flat-only; default chronological
1154
+ aggregation: Type.Optional(Type.String()), // group-format rule; required for aggregated/notification
1155
+ ranking: Type.Optional(Type.String()), // 'chronological' | 'decay'; flat-only; default chronological
1161
1156
  }),
1162
1157
  {
1163
1158
  default: {
@@ -1175,7 +1170,7 @@ export const ActivityFeedConfigSchema = Type.Object({
1175
1170
  {
1176
1171
  celebrityThreshold: Type.Integer({ default: 10_000, minimum: 0 }), // ≥ → pull (read-side); < → push (write-side)
1177
1172
  maxFanoutPerJob: Type.Integer({ default: 1000, minimum: 1, maximum: 10_000 }), // follower batch size per jobs task
1178
- maxConcurrentTasks: Type.Integer({ default: 20, minimum: 1, maximum: 1000 }), // per-tenant in-flight cap (LOCAL throttle §5)
1173
+ maxConcurrentTasks: Type.Integer({ default: 20, minimum: 1, maximum: 1000 }), // per-tenant in-flight cap (local throttle)
1179
1174
  pendingCeiling: Type.Integer({ default: 50_000, minimum: 1 }), // pending-fan-out-depth back-pressure ceiling
1180
1175
  },
1181
1176
  { default: {} },
@@ -1189,7 +1184,7 @@ export const ActivityFeedConfigSchema = Type.Object({
1189
1184
  { default: {} },
1190
1185
  ),
1191
1186
 
1192
- // (F8-54) the `aggregation.maxGroupActivities` leaf was DELETED: nothing kept
1187
+ // (2026-09-11) the `aggregation.maxGroupActivities` leaf was DELETED: nothing kept
1193
1188
  // or returned a per-group activity LIST — an aggregated read returns the group
1194
1189
  // rollup (activity_count/actor_count/last_actor), so there was never an N to
1195
1190
  // bound and no code read the leaf. Re-declare it with a group-detail route.
@@ -1203,7 +1198,7 @@ export const ActivityFeedConfigSchema = Type.Object({
1203
1198
 
1204
1199
  crossChannel: Type.Object(
1205
1200
  {
1206
- enabled: Type.Boolean({ default: false }), // master gate for the notifications push/email trigger (§10)
1201
+ enabled: Type.Boolean({ default: false }), // master gate for the notifications push/email trigger
1207
1202
  digestCadence: Type.Union(
1208
1203
  [Type.Literal('off'), Type.Literal('hourly'), Type.Literal('daily')],
1209
1204
  { default: 'off' },
@@ -1212,7 +1207,7 @@ export const ActivityFeedConfigSchema = Type.Object({
1212
1207
  { default: {} },
1213
1208
  ),
1214
1209
 
1215
- // (F8-54) the `rateLimit.addPerSec` leaf was DELETED: activity-feed-v1 has no
1210
+ // (2026-09-11) the `rateLimit.addPerSec` leaf was DELETED: activity-feed-v1 has no
1216
1211
  // rate-limiter binding and never read it, so the declared per-tenant write
1217
1212
  // burst was enforced by nothing (the edge front-door limiter and the
1218
1213
  // per-tenant request meter are the real bounds). Re-declare it together with
@@ -1222,11 +1217,11 @@ export const ActivityFeedConfigSchema = Type.Object({
1222
1217
  // maxFanoutPerJob, maxConcurrentTasks, pendingCeiling}(+4=6), follow.{copyLimit,
1223
1218
  // maxFollowing}(+2=8), realtime.enabled(9), crossChannel.{enabled, digestCadence}(+2=11).
1224
1219
  // countLeaves → 11. Cap = 15. (feedGroups is Type.Record → patternProperties, ONE leaf.)
1225
- // (was 13 — F8-54 deleted the inert aggregation and rateLimit bags.)
1220
+ // (was 13 — 2026-09-11 deleted the inert aggregation and rateLimit bags.)
1226
1221
 
1227
1222
  export type ActivityFeedConfig = Static<typeof ActivityFeedConfigSchema>;
1228
1223
 
1229
- // vector-search feature (features/vector-search.md §4). Re-declared here to match the
1224
+ // vector-search feature (guide ch. 6, vector-search). Re-declared here to match the
1230
1225
  // schema the worker EXPORTS from workers/vector-search-v1/src/config.ts — the
1231
1226
  // control plane validates writes against this shared copy (the same
1232
1227
  // one-definition / two-consumers note as the other features above; this package owns
@@ -1235,7 +1230,7 @@ export type ActivityFeedConfig = Static<typeof ActivityFeedConfigSchema>;
1235
1230
  // synthesis / relevance tuning (the tenant's moat). An OPTIONAL leaf = ONE flag.
1236
1231
  export const VectorSearchConfigSchema = Type.Object({
1237
1232
  enabled: Type.Boolean({ default: true }),
1238
- // 'auto' resolves to the default managed vector backend for the tier (#147).
1233
+ // 'auto' resolves to the default managed vector backend for the tier.
1239
1234
  backend: Type.Union(
1240
1235
  [Type.Literal('auto'), Type.Literal('lakebase'), Type.Literal('pgvector')],
1241
1236
  { default: 'auto' },
@@ -1293,7 +1288,7 @@ export const VectorSearchConfigSchema = Type.Object({
1293
1288
  ),
1294
1289
  model: Type.Optional(Type.String({ maxLength: 128 })), // cohere 'rerank-v3.5' / voyage 'rerank-2'
1295
1290
  topN: Type.Optional(Type.Integer({ default: 50, minimum: 1, maximum: 200 })),
1296
- apiKeyRef: Type.Optional(Type.String({ maxLength: 128 })), // 'secret:<name>' under KEK_VECTOR_SEARCH
1291
+ apiKeyRef: Type.Optional(Type.String({ maxLength: 128 })), // 'secret:<name>' in the tenant's secret store
1297
1292
  }),
1298
1293
  ),
1299
1294
  // Config-driven auto-embedding sync from cms collections: the control plane
@@ -1326,12 +1321,12 @@ export const VectorSearchConfigSchema = Type.Object({
1326
1321
 
1327
1322
  export type VectorSearchConfig = Static<typeof VectorSearchConfigSchema>;
1328
1323
 
1329
- // ai feature (features/ai.md §4). Re-declared to match workers/ai-v1/src/core.ts's
1324
+ // ai feature (guide ch. 6, ai). Re-declared to match workers/ai-v1/src/core.ts's
1330
1325
  // exported AiConfigSchema. vxil owns the call SCAFFOLDING (routing, streaming,
1331
1326
  // token accounting, caching, the reserve→settle budget); the tenant owns the
1332
1327
  // intelligence (prompt TEMPLATES are config-as-code, stored/rendered but never
1333
1328
  // authored). 'mock' is the deterministic default until a BYO key is provisioned;
1334
- // the real providers route via tenant_secrets keyRefs.
1329
+ // the real providers route via secret-store keyRefs.
1335
1330
  export const AI_TEMPLATE_NAME_PATTERN = '^[a-zA-Z0-9_.\\-]+$';
1336
1331
  export const AI_MAX_DECLARED_TEMPLATES = 50;
1337
1332
  export const DeclaredAiTemplateSchema = Type.Object({
@@ -1352,7 +1347,7 @@ export const AiConfigSchema = Type.Object({
1352
1347
  Type.Literal('gemini'), Type.Literal('azure'), Type.Literal('openrouter')],
1353
1348
  { default: 'mock' },
1354
1349
  ),
1355
- // BYO keyRefs → public.tenant_secrets (envelope-encrypted). The block is NOT
1350
+ // BYO keyRefs → the tenant's encrypted secret store. The block is NOT
1356
1351
  // optional (the worker declares it plain), so its three optional refs each count
1357
1352
  // as a leaf. The nested blocks carry `default: {}` (this package's convention)
1358
1353
  // so Value.Default materializes them + recurses into the leaf defaults when a
@@ -1364,7 +1359,7 @@ export const AiConfigSchema = Type.Object({
1364
1359
  geminiKeyRef: Type.Optional(Type.String()),
1365
1360
  // The openai-compatible extension surface — ONE optional object = ONE config
1366
1361
  // leaf (countLeaves collapses optional objects; 15-leaf cap discipline).
1367
- // compat.openrouterKeyRef: BYO OpenRouter key (tenant_secrets ref, KEK_AI).
1362
+ // compat.openrouterKeyRef: BYO OpenRouter key (a secret-store ref).
1368
1363
  // compat.openaiBaseUrl: point the openai adapter at ANY openai-compatible
1369
1364
  // host (DeepSeek, vLLM, an Azure-compatible proxy). Public-https validated
1370
1365
  // at config WRITE (publicHttpsUrlError below) AND at USE (@vxil/runtime
@@ -1384,7 +1379,7 @@ export const AiConfigSchema = Type.Object({
1384
1379
  tokensPerUserPerDay: Type.Integer({ default: 0, minimum: 0 }), // 0 = unlimited
1385
1380
  consumeCredits: Type.Boolean({ default: false }), // LIVE: reserve→settle against the payments credit ledger (a job-routed generation reserves pre-generation and 402s insufficient_credits)
1386
1381
  }, { default: {} }),
1387
- // `streaming` became an OPTIONAL bag (3 leaves → 1, the M21 `retry` trick)
1382
+ // `streaming` became an OPTIONAL bag (3 leaves → 1, the `retry` trick)
1388
1383
  // on 2026-09-23 to fund the declared `templates[]` below. It KEEPS
1389
1384
  // `default: {}`, so Value.Default still materializes
1390
1385
  // streaming.{enabled,replayBufferFrames,flushMs} into every persisted
@@ -1393,12 +1388,11 @@ export const AiConfigSchema = Type.Object({
1393
1388
  // here is optional.
1394
1389
  streaming: Type.Optional(Type.Object({
1395
1390
  enabled: Type.Boolean({ default: true }),
1396
- replayBufferFrames: Type.Integer({ default: 256, minimum: 1 }), // §2a ring-buffer depth
1397
- flushMs: Type.Integer({ default: 50, minimum: 0 }), // §2a/#148 token→frame coalesce window
1391
+ replayBufferFrames: Type.Integer({ default: 256, minimum: 1 }), // replay ring-buffer depth
1392
+ flushMs: Type.Integer({ default: 50, minimum: 0 }), // token→frame coalesce window
1398
1393
  }, { default: {} })),
1399
- // DECLARED API STATE (roadmap §4.11 P0-3, 2026-09-23): the tenant's stored
1400
- // prompt templates as config. vxil STORES + versions, never authors (ai.md
1401
- // §0) — declaring them here changes WHO writes the row (the repository, via
1394
+ // DECLARED API STATE (2026-09-23): the tenant's stored
1395
+ // prompt templates as config. vxil STORES + versions, never authors — declaring them here changes WHO writes the row (the repository, via
1402
1396
  // `vxil push`), not what vxil does with it. Converged by CONTENT: the
1403
1397
  // control-plane hashes each declared entry (@vxil/runtime
1404
1398
  // aiTemplateContentSha256) against the `content_sha256` GET /v1/ai/templates
@@ -1407,7 +1401,7 @@ export const AiConfigSchema = Type.Object({
1407
1401
  // Item shape mirrors ai-v1 core.ts TemplateBody exactly (`template` is the
1408
1402
  // name). Stored templates the config does not declare are reported and left
1409
1403
  // in place — RETIRED (soft: hidden from list + render, history kept) only
1410
- // under --allow-destructive (cvskit F67, 2026-10-01). Bounded to 50 entries: the
1404
+ // under --allow-destructive (2026-10-01). Bounded to 50 entries: the
1411
1405
  // manifest rides the 1 MiB config body cap. An Optional ARRAY is ONE leaf.
1412
1406
  templates: Type.Optional(Type.Array(DeclaredAiTemplateSchema, { maxItems: AI_MAX_DECLARED_TEMPLATES })),
1413
1407
  });
@@ -1420,7 +1414,7 @@ export const AiConfigSchema = Type.Object({
1420
1414
 
1421
1415
  export type AiConfig = Static<typeof AiConfigSchema>;
1422
1416
 
1423
- // rag feature (features/rag.md §2). Re-declared to match workers/rag-v1/src/config.ts's
1417
+ // rag feature (guide ch. 6, rag). Re-declared to match workers/rag-v1/src/config.ts's
1424
1418
  // exported RagConfigSchema. rag owns the PIPELINE knobs only — retrieval budget,
1425
1419
  // context budget + strategy + tokenizer, the citation/stream gates — NEVER the
1426
1420
  // prompt, the synthesis, or relevance tuning (the tenant's `ai` template owns those).
@@ -1444,7 +1438,7 @@ export const RagConfigSchema = Type.Object({
1444
1438
  { default: {} },
1445
1439
  ),
1446
1440
  // declarative per-metadata-field relevance boosts applied in rag AFTER
1447
- // retrieval, BEFORE minScore/budget/grounding (rag.md §2f). ONE Type.Record
1441
+ // retrieval, BEFORE minScore/budget/grounding (guide ch. 6, rag). ONE Type.Record
1448
1442
  // leaf (the activity-feed feedGroups precedent).
1449
1443
  boosts: Type.Record(Type.String(), Type.Union([
1450
1444
  Type.Object({
@@ -1459,13 +1453,13 @@ export const RagConfigSchema = Type.Object({
1459
1453
  ]), { default: {} }),
1460
1454
  context: Type.Object(
1461
1455
  {
1462
- // bounded context budget — enforced via the §2c tokenizer, BEFORE the ai call.
1456
+ // bounded context budget — enforced via the tokenizer, BEFORE the ai call.
1463
1457
  maxTokens: Type.Integer({ default: 4000, minimum: 1, maximum: 1_000_000 }),
1464
1458
  strategy: Type.Union([Type.Literal('topk'), Type.Literal('mmr')], {
1465
1459
  default: 'topk',
1466
1460
  }),
1467
1461
  // 'provider' = the resolved ai provider's tokenizer; 'heuristic' = portable
1468
- // ~chars/4 with a safety margin (§2c, #149).
1462
+ // ~chars/4 with a safety margin.
1469
1463
  tokenizer: Type.Union([Type.Literal('provider'), Type.Literal('heuristic')], {
1470
1464
  default: 'provider',
1471
1465
  }),
@@ -1484,7 +1478,7 @@ export const RagConfigSchema = Type.Object({
1484
1478
 
1485
1479
  export type RagConfig = Static<typeof RagConfigSchema>;
1486
1480
 
1487
- // payments feature (features/payments.md §5). Re-declared to match the schema the
1481
+ // payments feature (guide ch. 6, payments). Re-declared to match the schema the
1488
1482
  // worker EXPORTS from workers/payments-v1/src/core.ts — the control plane
1489
1483
  // validates writes against this shared copy (one-definition / two-consumers,
1490
1484
  // like the other features). Two co-equal pillars: (A) provider payments (the
@@ -1503,9 +1497,9 @@ export const PaymentsConfigSchema = Type.Object({
1503
1497
  Type.Literal('revenuecat'), Type.Literal('paypal')],
1504
1498
  { default: 'mock' },
1505
1499
  ),
1506
- // BYO-key credential blocks → public.tenant_secrets (envelope-encrypted).
1500
+ // BYO-key credential blocks → the tenant's encrypted secret store.
1507
1501
  // Each OPTIONAL object counts as ONE leaf (the tenant's decision is
1508
- // "configure it or not", not each inner ref — features/auth.md §4).
1502
+ // "configure it or not", not each inner ref — guide ch. 6, auth).
1509
1503
  stripe: Type.Optional(Type.Object({
1510
1504
  secretKeyRef: Type.String(),
1511
1505
  webhookSecretRef: Type.String(),
@@ -1520,25 +1514,24 @@ export const PaymentsConfigSchema = Type.Object({
1520
1514
  projectId: Type.String(),
1521
1515
  publicSdkKey: Type.String(),
1522
1516
  secretApiKeyRef: Type.String(),
1523
- // Per-tenant webhook secret ref (public.tenant_secrets). Inbound RevenueCat
1517
+ // Per-tenant webhook secret ref (secret store). Inbound RevenueCat
1524
1518
  // webhooks are verified against THIS ref and nothing else: there is no
1525
1519
  // platform-wide PROVIDER_WEBHOOK_SECRET fallback for a real payment provider
1526
- // (that fallback WAS the multi-tenant RC webhook-forgery vector; it is now
1527
- // frozen out by tests/ci/src/provider-webhook-secret-fallback.test.ts, which
1520
+ // (that fallback WAS a cross-tenant webhook-forgery vector; a CI gate now
1528
1521
  // permits `secrets.webhookSecret` only in makeProvider's mock/default arm).
1529
1522
  // Optional at the SCHEMA level only — leaving it unset does not disable
1530
1523
  // verification, it fails CLOSED: every delivery is 401 bad_signature with a
1531
1524
  // `sig_failed` row that can never be reprocessed. Mirrors stripe/paddle
1532
1525
  // webhookSecretRef.
1533
1526
  webhookSecretRef: Type.Optional(Type.String()),
1534
- // Environment integrity (money-path wave F1-4/D5). RevenueCat posts SANDBOX
1527
+ // Environment integrity. RevenueCat posts SANDBOX
1535
1528
  // and PRODUCTION events to the SAME webhook with the same auth header, so a
1536
1529
  // sandbox purchase would otherwise fold into production entitlements. A
1537
1530
  // sandbox event is persisted as outcome 'rejected_environment' (200, never
1538
1531
  // folded) unless the tenant opts in here. Stripe/Paddle/PayPal separate
1539
1532
  // environments by signing secret / API base, so only RC carries this knob.
1540
1533
  acceptSandbox: Type.Boolean({ default: false }),
1541
- // (2026-10-01 §4.13 A18) Store-review purchases on a PRODUCTION tenant:
1534
+ // (2026-10-01) Store-review purchases on a PRODUCTION tenant:
1542
1535
  // the reviewer accounts' RevenueCat `app_user_id`s (≤ 20). A SANDBOX event
1543
1536
  // whose subject (and, for a TRANSFER, every source user) is listed here
1544
1537
  // folds — recorded `environment: 'sandbox'` on the delivery, the
@@ -1559,9 +1552,9 @@ export const PaymentsConfigSchema = Type.Object({
1559
1552
  // Where a provider-hosted flow sends the payer back (Stripe billing-portal
1560
1553
  // return, PayPal approval return/cancel) — the TENANT's own app URL,
1561
1554
  // https-only. Absent ⇒ the worker's WEB_BASE_URL env (vxil's site), never a
1562
- // hardcoded host (audit 2026-07-10: the old fallback pointed at a dead apex).
1555
+ // hardcoded host (2026-07-10: the old fallback pointed at a dead apex).
1563
1556
  returnUrl: Type.Optional(Type.String({ pattern: '^https://', maxLength: 512 })),
1564
- // NB (F8-54, 2026-09-11): the former `prices.catalogRef` leaf was DELETED —
1557
+ // NB (2026-09-11): the former `prices.catalogRef` leaf was DELETED —
1565
1558
  // it named nothing (prices resolve from ledger.priceMap; no code path ever
1566
1559
  // read it). A persisted manifest that still carries `prices` folds:
1567
1560
  // validateFeatureConfig's Value.Clean strips the stray key.
@@ -1579,11 +1572,11 @@ export const PaymentsConfigSchema = Type.Object({
1579
1572
  // product_id → what the purchase GRANTS. ONE Type.Record leaf (the rag
1580
1573
  // `boosts` Record-of-Union precedent) with two rule shapes:
1581
1574
  // { creditType, amount, period } a credit grant (the original rule)
1582
- // { tier, durationDays } (2026-09-25 F35+) a TIME-BOXED
1575
+ // { tier, durationDays } (2026-09-25) a TIME-BOXED
1583
1576
  // ENTITLEMENT: the buyer gets `tier` (a tierMap key — cross-checked
1584
1577
  // below) for `durationDays`, as a charge-linked manual-style row that
1585
1578
  // STACKS behind the user's live same-tier manual rows that have an end
1586
- // (earlier passes AND comp grants since 2026-09-25 F38 — never a row
1579
+ // (earlier passes AND comp grants since 2026-09-25 — never a row
1587
1580
  // linked to the same charge, an open-ended grant or a provider
1588
1581
  // subscription) and is ENDED by that charge's full refund / chargeback.
1589
1582
  // No defaults in either shape, so an existing manifest is
@@ -1591,7 +1584,7 @@ export const PaymentsConfigSchema = Type.Object({
1591
1584
  productMap: Type.Record(Type.String(), Type.Union([
1592
1585
  Type.Object({
1593
1586
  creditType: Type.String({ minLength: 1 }),
1594
- amount: Type.Integer({ minimum: 1 }), // #128: a grant only ADDS
1587
+ amount: Type.Integer({ minimum: 1 }), // a grant only ADDS
1595
1588
  period: Type.Union([Type.Literal('once'), Type.Literal('monthly'),
1596
1589
  Type.Literal('annual')]),
1597
1590
  }),
@@ -1602,13 +1595,13 @@ export const PaymentsConfigSchema = Type.Object({
1602
1595
  ])),
1603
1596
  tierMap: Type.Record(Type.String(), Type.Object({ // tier → entitlement/quota/grant
1604
1597
  entitlements: Type.Array(Type.String()),
1605
- quotas: Type.Record(Type.String(), Type.Integer({ minimum: 0 })), // #128: no negative quota
1606
- rank: Type.Optional(Type.Integer({ minimum: 0 })), // precedence for the multi-sub fold (#125)
1598
+ quotas: Type.Record(Type.String(), Type.Integer({ minimum: 0 })), // no negative quota
1599
+ rank: Type.Optional(Type.Integer({ minimum: 0 })), // precedence for the multi-sub fold
1607
1600
  grants: Type.Optional(Type.Array(Type.Object({
1608
1601
  creditType: Type.String({ minLength: 1 }),
1609
- amount: Type.Integer({ minimum: 1 }), // #128
1602
+ amount: Type.Integer({ minimum: 1 }), // a grant only ADDS
1610
1603
  period: Type.String(),
1611
- // (2026-10-01 §4.13 W9) 'add' (the reader's default, today's
1604
+ // (2026-10-01) 'add' (the reader's default, today's
1612
1605
  // behaviour) ADDS `amount` each period; 'reset' makes the period's
1613
1606
  // grant REPLACE what is left: the unspent available balance of
1614
1607
  // `creditType` is written off as one `expire` ledger row and `amount`
@@ -1624,7 +1617,7 @@ export const PaymentsConfigSchema = Type.Object({
1624
1617
  // fold (refoldEntitlements WHERE tier IS NOT NULL) reflects the subscription.
1625
1618
  priceMap: Type.Optional(Type.Record(Type.String(), Type.String())),
1626
1619
  autoRefundOnJobFailure: Type.Boolean({ default: true }), // consume(jobId) reverses on DLQ/timeout
1627
- // Grace window (money-path wave F1-7): a `past_due` subscription stays
1620
+ // Grace window: a `past_due` subscription stays
1628
1621
  // entitled for this many days AFTER its current_period_end (the dunning
1629
1622
  // window the provider is retrying inside). 0 = today's behaviour (a past_due
1630
1623
  // row is never entitled). ONE predicate in the fold — NOT a dunning ladder:
@@ -1632,8 +1625,7 @@ export const PaymentsConfigSchema = Type.Object({
1632
1625
  grace: Type.Optional(Type.Object({
1633
1626
  pastDueDays: Type.Integer({ default: 0, minimum: 0, maximum: 90 }),
1634
1627
  })),
1635
- // Opt-in period-end enforcement (money-path operations wave, decision D2
1636
- // option a). ABSENT (the default) = today's behaviour: a subscription whose
1628
+ // Opt-in period-end enforcement. ABSENT (the default) = today's behaviour: a subscription whose
1637
1629
  // current_period_end passed with no provider event stays entitled forever
1638
1630
  // (the provider is the only clock). PRESENT = the nightly reconcile sweep
1639
1631
  // flips an `active`/`trialing` row whose current_period_end + slackHours
@@ -1668,10 +1660,10 @@ export const PaymentsConfigSchema = Type.Object({
1668
1660
  // (`config.ledger?.unmappedProduct ?? 'error'`), which is the one definition.
1669
1661
  unmappedProduct: Type.Optional(Type.Union([Type.Literal('error'), Type.Literal('ignore')])),
1670
1662
  })),
1671
- // NB (money-path wave F3-19): the former `webhooks.forwardToTenantUrl` leaf
1663
+ // NB: the former `webhooks.forwardToTenantUrl` leaf
1672
1664
  // was DELETED — it had zero readers (never forwarded anything). Outbound
1673
- // delivery of payments state changes rides the audit_event → webhooks-out
1674
- // spine: subscribe to the `payments.` event prefix (payments.md §7b).
1665
+ // delivery of payments state changes rides the audit stream → outbound
1666
+ // webhooks: subscribe to the `payments.` event prefix (guide ch. 6, payments).
1675
1667
  });
1676
1668
  // Leaves: enabled(1), provider(2), stripe?(3), paddle?(4), revenuecat?(5),
1677
1669
  // paypal?(6), returnUrl(7), defaults.{currency,trialDays}(+2=9), ledger?(10).
@@ -1683,24 +1675,24 @@ export const PaymentsConfigSchema = Type.Object({
1683
1675
 
1684
1676
  export type PaymentsConfig = Static<typeof PaymentsConfigSchema>;
1685
1677
 
1686
- // functions feature (vxil-functions-design §4.c). Tenant-deployed backend edge
1678
+ // functions feature (guide ch. 8). Tenant-deployed backend edge
1687
1679
  // functions on the managed serverless runtime. The FUNCTION owns its identity (bundle via
1688
1680
  // scriptRef, scopes, secrets, egress, limits, runtime) + a SET of trigger
1689
1681
  // bindings; every other surface (e.g. cms.hooks) REFERENCES a function BY NAME and
1690
1682
  // never re-embeds deploy config. The per-function bag is ONE Type.Record leaf
1691
1683
  // (cms.hooks / payments.ledger precedent), so any number of deployed functions
1692
- // never grows the flag cap. This is the §7.3 crossing — paid, tier-walled,
1684
+ // never grows the flag cap. Tenant code on the platform — paid, tier-walled,
1693
1685
  // opt-in (enabled defaults to false), egress-guarded.
1694
1686
  export const FunctionsConfigSchema = Type.Object({
1695
1687
  enabled: Type.Boolean({ default: false }),
1696
- // (F8-54) `runtime` ('isolate' | 'container') was DELETED: the container lane
1697
- // is design-only, nothing read the leaf, and accepting 'container' silently
1688
+ // (2026-09-11) `runtime` ('isolate' | 'container') was DELETED: the container lane
1689
+ // is not built, nothing read the leaf, and accepting 'container' silently
1698
1690
  // ran the isolate anyway. It comes back with the lane, not before.
1699
1691
  defaultLimits: Type.Object(
1700
1692
  {
1701
1693
  // cpuMs is the ONLY per-dispatch limit the managed runtime accepts and the
1702
1694
  // only one anything reads (functions-v1 meter.ts + the dispatch cap).
1703
- // (F8-54) `timeoutMs` and `memoryMb` were DELETED: memory is fixed by the
1695
+ // (2026-09-11) `timeoutMs` and `memoryMb` were DELETED: memory is fixed by the
1704
1696
  // runtime and not tenant-selectable, and no wall-clock abort was ever
1705
1697
  // applied — a declared 10s default that nothing enforced.
1706
1698
  cpuMs: Type.Integer({ default: 50, minimum: 5, maximum: 300_000 }),
@@ -1751,7 +1743,7 @@ export const FunctionsConfigSchema = Type.Object({
1751
1743
  source: Type.Optional(Type.String()), // webhook/queue: source/queue id
1752
1744
  collection: Type.Optional(Type.String()), // cmsHook: the CMS collection slug
1753
1745
  event: Type.Optional(Type.String()), // cmsHook: beforeCreate|beforeUpdate|beforeWrite · authHook: 'user.created'
1754
- // F33 (2026-09-25): the per-binding opt-in to re-delivery on
1746
+ // (2026-09-25) the per-binding opt-in to re-delivery on
1755
1747
  // queue / webhook / cmsHook / authHook (the cross-field rule
1756
1748
  // rejects it on http / cron). Absent = the ACK-200 default. The
1757
1749
  // receiver answers a failed attempt as an enveloped 503 (ladder)
@@ -1778,7 +1770,7 @@ export const FunctionsConfigSchema = Type.Object({
1778
1770
  // the settings it was deployed and tested with, not today's. Absent =
1779
1771
  // the legacy settings (@vxil/types FUNCTIONS_RUNTIME_LEGACY). Unbounded
1780
1772
  // strings on purpose (the deploy writes them from the one constant).
1781
- // cpuMs (F2, 2026-10-01): the per-invoke CPU limit the script was
1773
+ // cpuMs (2026-10-01): the per-invoke CPU limit the script was
1782
1774
  // uploaded with (limits.cpu_ms = min(declared limits.cpuMs, the tier's
1783
1775
  // cpuMsPerInvoke, FN_MAX_CPU_MS)) — server-set; the nightly plan pass
1784
1776
  // rewrites it after a tier change. Absent = the platform default.
@@ -1819,10 +1811,9 @@ export const FunctionsConfigSchema = Type.Object({
1819
1811
  // egress guard as an outbound parameter. NOT an invocation
1820
1812
  // budget: the INVOCATION is bounded by the platform's own
1821
1813
  // FN_MAX_INVOKE_MS deadline (default >= 5 min), which a
1822
- // bigger per-fetch budget widens with you (audit FN-3).
1823
- // (F8-54's standing "strip the twins" note is DISCHARGED here: `memoryMb`
1824
- // is deleted — memory is fixed by the managed runtime and is not a
1825
- // per-dispatch option; the WfP dispatch bag takes { cpuMs, subRequests }.
1814
+ // bigger per-fetch budget widens with you.
1815
+ // (`memoryMb` is deleted — memory is fixed by the managed runtime and is
1816
+ // not a per-dispatch option; a dispatch takes { cpuMs, subRequests }.
1826
1817
  // Value.Clean strips it from an old config, so such a config still loads
1827
1818
  // and `vxil plan --explain` marks the key DROPPED.)
1828
1819
  limits: Type.Optional(
@@ -1832,7 +1823,7 @@ export const FunctionsConfigSchema = Type.Object({
1832
1823
  }),
1833
1824
  ),
1834
1825
  enabled: Type.Optional(Type.Boolean()),
1835
- // Level-1 typed I/O (cli-sdk design §4.5): the declared input/output
1826
+ // Level-1 typed I/O (guide ch. 8): the declared input/output
1836
1827
  // contract, persisted by the deploy body so ONLINE `vxil gen` emits the
1837
1828
  // same typed fn client as --offline. Opaque JSON-schema-ish payloads —
1838
1829
  // the CLI's lowerSig lowers them; the platform never interprets them.
@@ -1848,7 +1839,7 @@ export const FunctionsConfigSchema = Type.Object({
1848
1839
  // functions(4, the Type.Record bag → ONE leaf; bindings[]/scriptRef/scopes/
1849
1840
  // secrets/egressAllow/limits/enabled/signature are DATA inside the MAP value
1850
1841
  // and never move the count, like cms.hooks). countLeaves → 4. Cap = 15.
1851
- // (was 7 — F8-54 deleted the inert runtime, defaultLimits.timeoutMs and
1842
+ // (was 7 — 2026-09-11 deleted the inert runtime, defaultLimits.timeoutMs and
1852
1843
  // defaultLimits.memoryMb leaves.)
1853
1844
  export type FunctionsConfig = Static<typeof FunctionsConfigSchema>;
1854
1845
 
@@ -1934,7 +1925,7 @@ export const CopilotConfigSchema = Type.Object({
1934
1925
  { default: {} },
1935
1926
  ),
1936
1927
 
1937
- // (F8-54) the `escalation` bag ({enabled, handler, notifyTemplate}) was
1928
+ // (2026-09-11) the `escalation` bag ({enabled, handler, notifyTemplate}) was
1938
1929
  // DELETED: the human hand-off it declared was never built — copilot-v1 read
1939
1930
  // none of the three leaves, so a tenant who turned it on got silence. The
1940
1931
  // shipped hand-off path is a tenant function on the conversation events.
@@ -1942,7 +1933,7 @@ export const CopilotConfigSchema = Type.Object({
1942
1933
  // ── limits: DELEGATE token/credit accounting to ai-v1 ─────────────────────
1943
1934
  limits: Type.Object({
1944
1935
  consumeCredits: Type.Boolean({ default: false }),
1945
- // (F8-54) `tokensPerUserPerDay` was DELETED here: token accounting is
1936
+ // (2026-09-11) `tokensPerUserPerDay` was DELETED here: token accounting is
1946
1937
  // delegated to ai-v1 (this bag's own doctrine) and only `ai`'s
1947
1938
  // limits.tokensPerUserPerDay is enforced — the copilot twin read nothing.
1948
1939
  }, { default: {} }),
@@ -1958,7 +1949,7 @@ export const CopilotConfigSchema = Type.Object({
1958
1949
  // Leaves: enabled(1), agents(2 — Type.Record MAP, ONE leaf),
1959
1950
  // limits.consumeCredits(3), widget.{enabled,requireAuth,allowedOrigins,theme}
1960
1951
  // (+4=7). countLeaves → 7. Cap = 15.
1961
- // (was 9 — F8-54 deleted the inert escalation bag and limits.tokensPerUserPerDay.)
1952
+ // (was 9 — 2026-09-11 deleted the inert escalation bag and limits.tokensPerUserPerDay.)
1962
1953
 
1963
1954
  export type CopilotConfig = Static<typeof CopilotConfigSchema>;
1964
1955
 
@@ -1986,11 +1977,11 @@ export const FEATURE_SCHEMAS: Record<string, TSchema> = {
1986
1977
 
1987
1978
  export const CONFIG_FLAG_CAP = 15;
1988
1979
 
1989
- /** Counts leaf flags in a TypeBox object schema (architecture §6).
1990
- * Per features/auth.md §4: an OPTIONAL object (e.g. a provider credential
1980
+ /** Counts leaf flags in a TypeBox object schema.
1981
+ * Per guide ch. 6, auth: an OPTIONAL object (e.g. a provider credential
1991
1982
  * block) counts as ONE flag — the tenant's decision is "configure it or
1992
1983
  * not", not each inner ref. */
1993
- /** The auth lifecycle events an `authHook` binding may name (F4-30) — the
1984
+ /** The auth lifecycle events an `authHook` binding may name — the
1994
1985
  * closed union `packages/config` types as AuthHookEvent; the control-plane
1995
1986
  * reconciler maps each to its `auth.<event>` audit-event prefix. */
1996
1987
  export const AUTH_HOOK_EVENTS = ['user.created', 'session.created', 'session.revoked', 'signin.failure'] as const;
@@ -2003,7 +1994,7 @@ export type AuthHookEvent = (typeof AUTH_HOOK_EVENTS)[number];
2003
1994
  * else is refused at config write (E-CMSHOOK, 2026-10-01): an unknown name used
2004
1995
  * to fall back to beforeWrite at delivery, so a typo like 'afterCreate' or
2005
1996
  * 'beforeDelete' silently subscribed the function to creates AND updates.
2006
- * tests/ci/src/fn-binding-contracts.test.ts pins every copy to this list. */
1997
+ * A CI gate pins every copy to this list. */
2007
1998
  export const CMS_HOOK_EVENTS = ['beforeCreate', 'beforeUpdate', 'beforeWrite'] as const;
2008
1999
  export type CmsHookEvent = (typeof CMS_HOOK_EVENTS)[number];
2009
2000
 
@@ -2015,7 +2006,7 @@ export function cmsHookEventError(event: unknown): string | null {
2015
2006
  return `a 'cmsHook' binding's event must be one of ${CMS_HOOK_EVENTS.join(' | ')} (or omitted = beforeWrite), not ${JSON.stringify(event)}`;
2016
2007
  }
2017
2008
 
2018
- /** F4-29 test-recipient entry grammar (shared by the validator and auth-v1's
2009
+ /** The OTP test-recipient entry grammar (shared by the validator and auth-v1's
2019
2010
  * matcher): an exact email, a `*@domain` glob, or a +E.164 phone number. */
2020
2011
  export const TEST_RECIPIENT_EMAIL_RE = /^[^\s@*]+@[^\s@]+\.[^\s@]+$/;
2021
2012
  export const TEST_RECIPIENT_GLOB_RE = /^\*@[^\s@*]+\.[^\s@*]+$/;
@@ -2078,7 +2069,7 @@ export function setKnownMcpTools(names: readonly string[]): void {
2078
2069
  }
2079
2070
 
2080
2071
  // ── mcp custom-tool / prompt caps + the pure public-https check ──────────────
2081
- // (mcp.md §6.5) Caps are deliberately tighter than the 200-tool listing cap:
2072
+ // (guide ch. 10) Caps are deliberately tighter than the 200-tool listing cap:
2082
2073
  // each custom tool is a platform-signed egress target, so the bag stays small.
2083
2074
  export const MCP_MAX_CUSTOM_TOOLS = 32;
2084
2075
  export const MCP_MAX_PROMPTS = 32;
@@ -2136,7 +2127,7 @@ function publicHttpsUrlError(url: string): string | null {
2136
2127
  * validation below and the control-plane deploy clamp. */
2137
2128
  export const DENY_FUNCTION_SCOPES = new Set(['admin', '*', 'features:write', 'functions:write', 'secrets:write']);
2138
2129
 
2139
- // ── declared API state — config-write validators (roadmap §4.11 P0-3) ────────
2130
+ // ── declared API state — config-write validators ────────────────────────────
2140
2131
  /** `{var}` names of a rate-limit key template (rate-limits-v1 core.ts
2141
2132
  * templateVars parity). */
2142
2133
  export function rlTemplateVars(template: string): string[] {
@@ -2228,12 +2219,12 @@ export function validateFeatureConfig(feature: string, raw: unknown): ConfigVali
2228
2219
  return { ok: false, errors: [`schema for '${feature}' exceeds the ${CONFIG_FLAG_CAP}-flag cap`] };
2229
2220
  }
2230
2221
  // Apply defaults to a clone, then STRIP any property the schema does not
2231
- // declare, then check. Value.Clean makes the validator TOTAL (audit #112):
2222
+ // declare, then check. Value.Clean makes the validator TOTAL:
2232
2223
  // the schemas are open Type.Object()s, so without it Value.Check passes on —
2233
2224
  // and putConfig would persist — arbitrary unknown keys. Clean runs AFTER
2234
2225
  // Default so materialized nested defaults survive but stray top-level/nested
2235
2226
  // keys are dropped. This also removes the CLI dry-run idempotency drift
2236
- // (audit #118): plan/push diff the same cleaned manifest the server stores.
2227
+ // too: plan/push diff the same cleaned manifest the server stores.
2237
2228
  const withDefaults = Value.Clean(
2238
2229
  schema,
2239
2230
  Value.Default(schema, Value.Clone(raw)),
@@ -2277,7 +2268,7 @@ export function validateFeatureConfig(feature: string, raw: unknown): ConfigVali
2277
2268
  }
2278
2269
  // Cross-field rule: a REAL payments provider needs its credential block; the
2279
2270
  // 'mock' provider (the deterministic default) stays zero-config so the whole
2280
- // ledger path is testable without real keys (features/payments.md §0/§6).
2271
+ // ledger path is testable without real keys (guide ch. 6, payments).
2281
2272
  if (feature === 'payments') {
2282
2273
  const v = withDefaults as {
2283
2274
  provider?: string; stripe?: unknown; paddle?: unknown; revenuecat?: unknown;
@@ -2299,7 +2290,7 @@ export function validateFeatureConfig(feature: string, raw: unknown): ConfigVali
2299
2290
  }
2300
2291
  // A reserved (vxil-COGS) credit_type must NEVER appear in a ledger grant map:
2301
2292
  // the webhook/subscription reducers would otherwise credit `fn_cpu_ms` to a
2302
- // user, running vxil-billed functions for free (audit F2). Rejected at write
2293
+ // user, running vxil-billed functions for free. Rejected at write
2303
2294
  // time so the tenant gets a clear `vxil push` error, not a silent runtime skip.
2304
2295
  const ledgerErrs: string[] = [];
2305
2296
  const tierKeysForProducts = new Set(Object.keys(v.ledger?.tierMap ?? {}));
@@ -2309,7 +2300,7 @@ export function validateFeatureConfig(feature: string, raw: unknown): ConfigVali
2309
2300
  `/ledger/productMap/${productId}/creditType: '${rule.creditType}' is a reserved vxil-COGS credit type and cannot be granted via config`,
2310
2301
  );
2311
2302
  }
2312
- // (2026-09-25 F35+) an entitlement rule must name a declared tier — the
2303
+ // (2026-09-25) an entitlement rule must name a declared tier — the
2313
2304
  // write-time mirror of the runtime's unknown-tier refusal (a purchase for
2314
2305
  // a tier nobody declared would land the delivery `error`).
2315
2306
  if (rule.tier !== undefined && !tierKeysForProducts.has(rule.tier)) {
@@ -2360,7 +2351,7 @@ export function validateFeatureConfig(feature: string, raw: unknown): ConfigVali
2360
2351
  );
2361
2352
  }
2362
2353
  }
2363
- // Money-path wave F1-3 (D3): an UNMAPPED price silently revoked a paying
2354
+ // An UNMAPPED price silently revoked a paying
2364
2355
  // customer (priceMap miss → tier NULL → refold excluded the row). The
2365
2356
  // reducer now stamps such an event outcome 'error' (reprocessable), and
2366
2357
  // this lint catches the config half at `vxil push` time: every priceMap
@@ -2411,7 +2402,7 @@ export function validateFeatureConfig(feature: string, raw: unknown): ConfigVali
2411
2402
  ];
2412
2403
  if (rmErrors.length) return { ok: false, errors: rmErrors.slice(0, 10) };
2413
2404
  }
2414
- // Cross-field rules: mcp custom tools + prompts (mcp.md §6.5/§11). The URL
2405
+ // Cross-field rules: mcp custom tools + prompts (guide ch. 10). The URL
2415
2406
  // check here is a PURE mirror of @vxil/runtime assertPublicHttpsUrl (this
2416
2407
  // package is typebox-only) — the authoritative runtime guard re-runs in
2417
2408
  // mcp-v1 at call time; this gate rejects obviously-internal targets BEFORE
@@ -2477,7 +2468,7 @@ export function validateFeatureConfig(feature: string, raw: unknown): ConfigVali
2477
2468
  const tplErrs = validateDeclaredAiTemplates(v.templates);
2478
2469
  if (tplErrs.length) return { ok: false, errors: tplErrs.slice(0, 10) };
2479
2470
  }
2480
- // Cross-field rules: declared API state (roadmap §4.11 P0-3). Each list is
2471
+ // Cross-field rules: declared API state. Each list is
2481
2472
  // converged by NAME/URL, so a duplicate key is ambiguous and refused at push;
2482
2473
  // the per-item rules mirror the feature route's own validation so a declared
2483
2474
  // entry can never be one the converge would 422 on.
@@ -2491,7 +2482,7 @@ export function validateFeatureConfig(feature: string, raw: unknown): ConfigVali
2491
2482
  const errs = validateDeclaredWebhookSubscriptions(v.subscriptions, v.maxSubscriptions);
2492
2483
  if (errs.length) return { ok: false, errors: errs.slice(0, 10) };
2493
2484
  }
2494
- // Cross-field rule: auth otp.testRecipients (F4-29) — every entry must be an
2485
+ // Cross-field rule: auth otp.testRecipients — every entry must be an
2495
2486
  // email, an `*@domain` glob, or a +E.164 number; anything else would never
2496
2487
  // match and silently do nothing.
2497
2488
  if (feature === 'auth') {
@@ -2547,7 +2538,7 @@ export function validateFeatureConfig(feature: string, raw: unknown): ConfigVali
2547
2538
  if (b.overlap !== undefined && b.kind !== 'cron') {
2548
2539
  errs.push(`/functions/${name}/bindings/${i}: 'overlap' applies to cron bindings only (not '${b.kind}')`);
2549
2540
  }
2550
- // F33: retry is an opt-in for the platform-delivered event lanes only —
2541
+ // retry is an opt-in for the platform-delivered event lanes only —
2551
2542
  // an http invoke returns its real status to its caller, and a cron
2552
2543
  // tick's retry would overlap the next tick.
2553
2544
  if (b.retry !== undefined && !FN_RETRY_BINDING_KINDS.has(b.kind)) {
@@ -2560,7 +2551,7 @@ export function validateFeatureConfig(feature: string, raw: unknown): ConfigVali
2560
2551
  // at write time, never a silent created+updated subscription.
2561
2552
  const cmsEventErr = b.kind === 'cmsHook' ? cmsHookEventError(b.event) : null;
2562
2553
  if (cmsEventErr) errs.push(`/functions/${name}/bindings/${i}: ${cmsEventErr}`);
2563
- // authHook: a CLOSED event union (F4-30) — reject typos at write time
2554
+ // authHook: a CLOSED event union — reject typos at write time
2564
2555
  // so a binding never silently subscribes to nothing.
2565
2556
  if (b.kind === 'authHook' && b.event !== undefined && !(AUTH_HOOK_EVENTS as readonly string[]).includes(b.event)) {
2566
2557
  errs.push(`/functions/${name}/bindings/${i}: an 'authHook' binding's event must be one of ${AUTH_HOOK_EVENTS.join(' | ')} (or omitted = user.created)`);