@odla-ai/chapter 0.34.0 → 0.36.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.
package/README.md CHANGED
@@ -432,8 +432,11 @@ The document carries the source, exact chapter/environment/Stripe-mode target,
432
432
  monotonic revision, ordered and available tiers, immutable Stripe Price ids,
433
433
  the membership-page renderer document, and explicitly delegated application,
434
434
  policy, scheduling, and theme sections. Theme tokens include typography as
435
- ordinary CSS custom properties such as `--ui-font-display` and
436
- `--ui-font-body`.
435
+ allowlisted `--ui-*` CSS custom properties such as `--ui-font-display` and
436
+ `--ui-font-sans`. Chapter rejects non-`--ui-*` names, non-finite values,
437
+ oversized token sets, control characters, style-tag breakout, declaration
438
+ breakout, comments, imports, expressions, and URL-bearing values. Application
439
+ aliases such as `--teal` or `--deepocean` remain host-owned CSS.
437
440
 
438
441
  The packaged follower receiver validates the signed request timestamp, sender,
439
442
  target, environment, Stripe mode, schema, and SHA-256 digest. It rejects a stale
@@ -455,6 +458,84 @@ count before asking odla-db to order revision history. A freshly provisioned
455
458
  follower therefore acknowledges revision 1 immediately instead of depending on
456
459
  an ordered query over an empty result set.
457
460
 
461
+ #### Render active tokens before first paint
462
+
463
+ Put Chapter's marker after the local bootstrap theme and before any application
464
+ boot script in the HTML head:
465
+
466
+ ```html
467
+ <link rel="stylesheet" href="/bootstrap-theme.css">
468
+ <!--odla:signup-control-theme-->
469
+ <script type="module" src="/app.js"></script>
470
+ ```
471
+
472
+ A host route can transform the static asset with the follower-local active row:
473
+
474
+ ```ts
475
+ import { chapterWorker, withActiveSignupControlTheme } from "@odla-ai/chapter/worker";
476
+ import { chapter } from "./chapter.config.mjs";
477
+
478
+ export default chapterWorker({
479
+ chapter,
480
+ routes: [async (request, url, env, context) => {
481
+ if (request.method !== "GET" || url.pathname !== "/apply") return null;
482
+ const asset = await env.ASSETS.fetch(new URL("/index.html", url));
483
+ return withActiveSignupControlTheme(asset, {
484
+ db: context.makeDb(env),
485
+ chapterId: chapter.id,
486
+ environment: env.ODLA_ENV,
487
+ // The site's existing deterministic fallback when no revision is active.
488
+ bootstrapTokens: { "--ui-accent": "#096ea8" },
489
+ });
490
+ }],
491
+ });
492
+ ```
493
+
494
+ `withActiveSignupControlTheme` reads only the follower database, revalidates the
495
+ stored document and its canonical digest, and injects one bounded style block
496
+ with matching revision/digest metadata. It never calls the controlling hub. A
497
+ missing active row emits the documented `bootstrap` state; a failed local read
498
+ or corrupt active row fails closed instead of looking like absence. The response
499
+ defaults to `Cache-Control: no-store`, carries digest/revision headers, and gets
500
+ an ETag over the complete transformed HTML. This prevents a pre-activation page
501
+ from surviving activation. A host-owned cache may instead call
502
+ `renderActiveSignupControlHtml()` and include
503
+ `result.theme.metadata.cacheKey` in its complete page cache key.
504
+
505
+ For an SSR or static-HTML transform, use the same helper on the generated string:
506
+
507
+ ```ts
508
+ import { renderActiveSignupControlHtml } from "@odla-ai/chapter/worker";
509
+
510
+ const result = await renderActiveSignupControlHtml(generatedHtml, {
511
+ db, chapterId: chapter.id, environment,
512
+ bootstrapTokens: { "--ui-accent": "#096ea8" },
513
+ });
514
+ await writeHtml(result.html); // cache with result.theme.metadata.cacheKey
515
+ ```
516
+
517
+ After `GET /api/join-config`, hydrate the existing style element rather than
518
+ writing a second theme system. Matching revision/digest metadata produces no DOM
519
+ write and therefore no visual token change:
520
+
521
+ ```ts
522
+ import {
523
+ SIGNUP_CONTROL_THEME_STYLE_ID,
524
+ hydrateSignupControlTheme,
525
+ } from "@odla-ai/chapter";
526
+
527
+ const style = document.getElementById(SIGNUP_CONTROL_THEME_STYLE_ID);
528
+ if (style instanceof HTMLStyleElement) {
529
+ hydrateSignupControlTheme(style, joinConfig.signupControl ?? null, {
530
+ bootstrapTokens: { "--ui-accent": "#096ea8" },
531
+ });
532
+ }
533
+ ```
534
+
535
+ The host still decides which routes use managed theming and owns any legacy
536
+ token aliases. Keep the marker after bootstrap declarations so the active
537
+ `--ui-*` values win in the initial parsed document.
538
+
458
539
  The hub owns a durable outbox: create/verify immutable Stripe Prices first,
459
540
  persist the desired revision, retry signed delivery, and mark it active only
460
541
  after the exact revision/digest receipt. Version history is for audit and