@wenathlan/cobaia 2.0.166 → 2.0.168

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.
Files changed (53) hide show
  1. package/.dockerignore +1 -1
  2. package/App.tsx +2 -31
  3. package/Cargo.toml +17 -0
  4. package/Dockerfile +162 -3
  5. package/MANIFEST.in +7 -0
  6. package/biome.jsonc +3 -3
  7. package/build.gradle.kts +1 -1
  8. package/bun.lock +396 -343
  9. package/cobaia.csproj +2 -2
  10. package/cobaia.gemspec +2 -2
  11. package/cobaia.java +1 -1
  12. package/cobaia.rs +8 -0
  13. package/db.ts +12 -17
  14. package/docker-compose.yml +58 -0
  15. package/docker.config +74 -0
  16. package/homebrew.rb +25 -0
  17. package/keywords.txt +1 -0
  18. package/monetization.ts +248 -0
  19. package/nginx.conf +106 -0
  20. package/package.json +74 -76
  21. package/pnpm-workspace.yaml +59 -38
  22. package/pom.xml +2 -2
  23. package/pyproject.toml +23 -0
  24. package/routerbase.ts +1 -0
  25. package/sandboxmodality.ts +26 -40
  26. package/sol/Sol.tsx +1 -1
  27. package/sol/apps/apps.tsx +2 -2
  28. package/sol/blog/blog.tsx +2 -2
  29. package/sol/home/home.tsx +2 -2
  30. package/sol/intro/intro.tsx +1 -1
  31. package/sol/onboarding/onboarding.tsx +1 -1
  32. package/sol/pricing/pricing.tsx +1 -1
  33. package/sol/shell/Shell.tsx +2 -2
  34. package/sol/support/support.tsx +1 -1
  35. package/sol/wingets/IntroFooter.tsx +1 -1
  36. package/sol/wingets/IntroPlans.tsx +1 -1
  37. package/sol/wingets/ShellAdsslot.tsx +1 -1
  38. package/tauri.conf.json +1 -1
  39. package/{tests → test}/adsconfig.test.ts +2 -2
  40. package/{tests → test}/checkoutlinks.test.ts +2 -2
  41. package/{tests → test}/entryflow.test.ts +2 -2
  42. package/{tests → test}/familyurl.test.ts +1 -1
  43. package/{tests → test}/posts.test.ts +2 -2
  44. package/{tests → test}/sandboxmodality.test.ts +1 -1
  45. package/tsconfig.json +1 -0
  46. package/vite.config.ts +14 -0
  47. package/vitest.config.ts +9 -1
  48. package/wingets.ts +21 -0
  49. package/adsconfig.ts +0 -59
  50. package/checkoutlinks.ts +0 -80
  51. package/posts.ts +0 -54
  52. package/roster.ts +0 -29
  53. package/sol/wingets/index.ts +0 -20
package/cobaia.csproj CHANGED
@@ -4,7 +4,7 @@
4
4
  <AssemblyName>cobaia</AssemblyName>
5
5
  <RootNamespace>cobaia</RootNamespace>
6
6
  <PackageId>cobaia</PackageId>
7
- <Version>2.0.166</Version>
7
+ <Version>2.0.168</Version>
8
8
  <Authors>wenathlan</Authors>
9
9
  <Description>Distribution package containing the cobaia documentation set and the tests directory. cobaia is the monetization surface — the plans, the support rails and the honest advertisement slots.</Description>
10
10
  <PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
@@ -17,6 +17,6 @@
17
17
  <ItemGroup>
18
18
  <None Include="README.md" Pack="true" PackagePath="/" />
19
19
  <None Include="docs/**/*" Pack="true" PackagePath="contentFiles/any/any/docs/" />
20
- <None Include="tests/**/*" Pack="true" PackagePath="contentFiles/any/any/tests/" />
20
+ <None Include="test/**/*" Pack="true" PackagePath="contentFiles/any/any/test/" />
21
21
  </ItemGroup>
22
22
  </Project>
package/cobaia.gemspec CHANGED
@@ -1,6 +1,6 @@
1
1
  Gem::Specification.new do |spec|
2
2
  spec.name = "cobaia"
3
- spec.version = ENV.fetch("COBAIA_VERSION", "2.0.166")
3
+ spec.version = ENV.fetch("COBAIA_VERSION", "2.0.168")
4
4
  spec.authors = ["wenathlan"]
5
5
  spec.email = ["support@users.noreply.github.com"]
6
6
  spec.summary = "The cobaia documentation and tests envelope (the monetization surface — the plans, the support rails and the honest advertisement slots)."
@@ -8,7 +8,7 @@ Gem::Specification.new do |spec|
8
8
  spec.homepage = "https://github.com/wenathlan/devthink"
9
9
  spec.license = "GPL-3.0-only"
10
10
  spec.required_ruby_version = ">= 3.1"
11
- spec.files = Dir["README.md", "docs/**/*", "tests/**/*"]
11
+ spec.files = Dir["README.md", "docs/**/*", "test/**/*"]
12
12
  spec.metadata["source_code_uri"] = "https://github.com/wenathlan/devthink"
13
13
  spec.metadata["homepage_uri"] = spec.homepage
14
14
  end
package/cobaia.java CHANGED
@@ -10,7 +10,7 @@ package com.wenathlan.cobaia;
10
10
  */
11
11
  final class Cobaia {
12
12
  /** The version of the cobaia envelope (the six-distribution completeness wave). */
13
- public static final String VERSION = "2.0.166";
13
+ public static final String VERSION = "2.0.168";
14
14
  private Cobaia() {
15
15
  }
16
16
  }
package/cobaia.rs ADDED
@@ -0,0 +1,8 @@
1
+ //! The cobaia version envelope carrier.
2
+ //!
3
+ //! The carrier answers the lowercase file name doctrine: the envelope
4
+ //! carries the version constant the metadata lockstep stamps, the cargo
5
+ //! lane packages it, the registry lanes never compile it locally.
6
+
7
+ /// The version of the cobaia envelope (the family tri-channel wave).
8
+ pub const VERSION: &str = "2.0.167";
package/db.ts CHANGED
@@ -8,6 +8,7 @@
8
8
  * tables over HTTPS (GET/POST /api/v1/onboarding, GET /api/v1/ui/content);
9
9
  * no server route lives here.
10
10
  */
11
+ import { createOnboardingStore } from "@wenathlan/vault/onboardingstore";
11
12
  import { openStorageLane } from "@wenathlan/vault/storage";
12
13
  import { CONTENTKINDS, type contentrow, seedAdsLabel, seedAdsEmpty, seedAdsSlots, seedPlans, seedPosts, seedRoster, seedSupportLinks } from "./content";
13
14
  import { LABEL_MANIFEST, LABEL_REVISION } from "./labels";
@@ -70,6 +71,12 @@ export type OnboardingStateRow = {
70
71
  /** the stage flags the entry flow consults, as booleans. */
71
72
  export type OnboardingFlags = { introseen: boolean; onboardingseen: boolean };
72
73
 
74
+ /** the onboarding store of the site: the vault factory over the site's own
75
+ * storage lane (the seen-flags table rides the app schema, the read and the
76
+ * seen-wins upsert ride the shared mechanism — never duplicated inside the
77
+ * app; the storage lane law of the family, the labelstore precedent). */
78
+ const onboardingstore = createOnboardingStore(lane);
79
+
73
80
  /**
74
81
  * Reads the onboarding stage flags of one visitor (parameterized; undefined
75
82
  * before the first write). The browser accessor serves the same flags over
@@ -79,17 +86,13 @@ export type OnboardingFlags = { introseen: boolean; onboardingseen: boolean };
79
86
  * @returns the flags, or undefined when the visitor never wrote.
80
87
  */
81
88
  export function getOnboardingState(visitor: string): OnboardingFlags | undefined {
82
- const row = queryOne<Pick<OnboardingStateRow, "introseen" | "onboardingseen">>(
83
- "SELECT introseen, onboardingseen FROM onboarding_states WHERE visitor = ?",
84
- [visitor],
85
- );
86
- if (!row) return undefined;
87
- return { introseen: row.introseen === 1, onboardingseen: row.onboardingseen === 1 };
89
+ return onboardingstore.getOnboardingState(visitor);
88
90
  }
89
91
 
90
92
  /**
91
- * Marks one onboarding stage seen for a visitor — idempotent seen-wins
92
- * upsert (a stage seen anywhere stays seen), prepared and parameterized,
93
+ * Marks one onboarding stage seen for a visitor — the idempotent seen-wins
94
+ * upsert of the vault store (a stage seen anywhere stays seen; the later
95
+ * stage implies the earlier — the flow order), prepared and parameterized,
93
96
  * never concatenated SQL.
94
97
  *
95
98
  * @param visitor the visitor key of the row.
@@ -97,15 +100,7 @@ export function getOnboardingState(visitor: string): OnboardingFlags | undefined
97
100
  * @returns the flags after the write.
98
101
  */
99
102
  export function setOnboardingStage(visitor: string, stage: "intro" | "onboarding"): OnboardingFlags {
100
- execute(
101
- `INSERT INTO onboarding_states (visitor, introseen, onboardingseen, updatedAt)
102
- VALUES (?, ?, ?, datetime('now'))
103
- ON CONFLICT(visitor) DO UPDATE SET
104
- introseen = MAX(introseen, excluded.introseen),
105
- onboardingseen = MAX(onboardingseen, excluded.onboardingseen),
106
- updatedAt = datetime('now')`,
107
- [visitor, stage === "intro" ? 1 : 0, stage === "onboarding" ? 1 : 0],
108
- );
103
+ onboardingstore.setOnboardingStage(visitor, stage);
109
104
  return getOnboardingState(visitor) ?? { introseen: false, onboardingseen: false };
110
105
  }
111
106
 
@@ -0,0 +1,58 @@
1
+ # cobaia docker-compose — the two-service deploy of the monetization face: the
2
+ # static theme served by the cobaia-face nginx door beside the /api/* proxy lane
3
+ # that carries every platform call to the cobaia engine. until this file the
4
+ # app rode the registry image alone: no compose seated the pair, and every
5
+ # /api call of the face answered the away rehearsal. this file wires the
6
+ # pair the vault compose law standardizes:
7
+ #
8
+ # cobaia-engine the cobaia site container the publish lane builds from the staged
9
+ # family context (the app nested under app/ with the vault sibling
10
+ # the copy law carries): the built theme behind the unprivileged
11
+ # nginx, EXPOSE 8080, the wget listen probe the image carries
12
+ # cobaia-face the nginx surface: the family conf (nginx.conf) mounted over
13
+ # the default conf — the resolver law, the security header law,
14
+ # the gzip and cache law, the /api/* proxy lane to the engine,
15
+ # the spa rewrite as try_files, host 8083 published on 8080.
16
+ #
17
+ # the engine image is the one the registry lane publishes (ghcr.io/wenathlan/cobaia:<version>
18
+ # — the publish matrix); the publish matrix builds the same image from the staged family context (the app nested under app/ with the vault sibling the copy law carries):
19
+ #
20
+ # rm -rf familybuildctx && mkdir -p familybuildctx/app
21
+ # cp -a cobaia/. familybuildctx/app/
22
+ # cp -a vault/. familybuildctx/vault/ && rm -rf familybuildctx/vault/node_modules
23
+ # docker build -f cobaia/Dockerfile familybuildctx
24
+ #
25
+ # the /api lane carries the platform calls same-origin: the face proxies
26
+ # them to the cobaia container, and the data lanes the browser contract
27
+ # demands ride the configured gateway base the build stamps (the https law
28
+ # the modules carry).
29
+
30
+ services:
31
+ cobaia-engine:
32
+ image: "ghcr.io/wenathlan/cobaia:${COBAIA_VERSION:-2.0.167}"
33
+ # the engine never publishes a port by default: the face proxies it on
34
+ # the private compose network (the vault precedent). uncomment to serve
35
+ # the engine lane directly behind your own door:
36
+ # ports:
37
+ # - "3213:8080"
38
+ # the healthcheck rides the image (wget -qO- http://127.0.0.1:8080/ —
39
+ # the listen probe the site container carries): the face gates on it below.
40
+ init: true
41
+ restart: unless-stopped
42
+
43
+ cobaia-face:
44
+ image: "ghcr.io/wenathlan/cobaia:${COBAIA_VERSION:-2.0.167}"
45
+ volumes:
46
+ # the family conf over the default site conf: the resolver law, the
47
+ # security headers, the gzip and cache law, the /api proxy lane.
48
+ - ./nginx.conf:/etc/nginx/conf.d/default.conf:ro
49
+ depends_on:
50
+ # the face starts only when the engine health lane answered — the
51
+ # proxy upstream is live before the first /api/* call arrives.
52
+ cobaia-engine:
53
+ condition: service_healthy
54
+ ports:
55
+ # the public door: host 8083 → the unprivileged nginx on 8080.
56
+ # remap freely ("8084:8080") when another service owns the host.
57
+ - "8083:8080"
58
+ restart: unless-stopped
package/docker.config ADDED
@@ -0,0 +1,74 @@
1
+ {
2
+ "meta": {
3
+ "name": "docker.config",
4
+ "title": "cobaia - the container configuration of the monetization site (the compose pair, the nginx face, the site container)",
5
+ "version": "2.0.167",
6
+ "date": "2026-11-06",
7
+ "language": "en",
8
+ "sources": [
9
+ "app:Dockerfile [the cobaia site container — solbuild vite stage + the nginx-unprivileged face, EXPOSE 8080, the wget listen healthcheck]",
10
+ "app:docker-compose.yml [the pair: cobaia-engine + cobaia-face, host 8083:8080, the engine direct lane 3213:8080 unpublished by default]",
11
+ "app:nginx.conf [the vault pattern: resolver 127.0.0.11 valid=10s ipv6=off, location ^~ /api/, the _headers security law, the faun block, the root-emit seal]",
12
+ "app:vite.config.ts [the root-emit outDir law — the hashed bundles land at the application root]",
13
+ "app:package.json [the version stamp the compose args and this manifest carry]",
14
+ "vault:nginx.conf [the reference pattern the family standardizes — the resolver law, the proxy lane, the header law]",
15
+ "saddle:docker.config [the meta format this manifest follows — name, title, version, date, language, sources, mergeReport]",
16
+ "app:content.ts [the platform content lane the /api proxy carries — the https base law]"
17
+ ],
18
+ "mergeReport": {
19
+ "rootFiles": [
20
+ "Dockerfile",
21
+ "docker-compose.yml",
22
+ "nginx.conf",
23
+ "docker.config"
24
+ ],
25
+ "composeServices": [
26
+ "cobaia-engine",
27
+ "cobaia-face"
28
+ ],
29
+ "portMap": {
30
+ "face": "8083:8080",
31
+ "engineDirect": "3213:8080 (unpublished by default — the face proxies the private compose network)"
32
+ },
33
+ "healthLane": "the image healthcheck (wget -qO- http://127.0.0.1:8080/) — the listen probe",
34
+ "conflictResolution": "the vault nginx.conf pattern owns the conf law and the saddle docker.config meta format owns this manifest — one owner per law, the family consumes (the engineowners doctrine)",
35
+ "pinPatch": "the family base nginxinc/nginx-unprivileged:1.29-alpine@sha256:0c79d56aee561a1d81c63f00eee5fb5fe29279560cdc55e91425133104c7fbe6 rides the pattern faces; the static faces ride the registry site image the publish lane stamps"
36
+ }
37
+ },
38
+ "engine": {
39
+ "service": "cobaia-engine",
40
+ "surface": "the cobaia site container the publish lane builds (the staged family context the Dockerfile reads); the image healthcheck probes the listen port (wget http://127.0.0.1:8080/)",
41
+ "port": "8080",
42
+ "healthcheck": "wget -qO- http://127.0.0.1:8080/ (rides the image)",
43
+ "stateHome": "stateless — the site container carries no store",
44
+ "entry": "the site container (nginx serving the built theme)"
45
+ },
46
+ "face": {
47
+ "service": "cobaia-face",
48
+ "image": "ghcr.io/wenathlan/cobaia:2.0.167",
49
+ "surface": "the nginx face: the family conf (nginx.conf) mounted over the default site conf of the same cobaia image — resolver 127.0.0.11, the _headers security law, gzip, the hashed-asset cache, the ^~ /api proxy lane, the faun and root-emit seals, the try_files spa rewrite",
50
+ "listen": 8080,
51
+ "seals": [
52
+ "the faun db block (403)",
53
+ "the root-emit source seal (403)",
54
+ "the dotfile seal (403)",
55
+ "the tree seal (node_modules, tests, docs, sol — 403)"
56
+ ],
57
+ "spaRewrite": "try_files $uri $uri/ /index.html"
58
+ },
59
+ "apiLane": {
60
+ "route": "/api/*",
61
+ "upstream": "http://cobaia-engine:8080",
62
+ "resolver": "127.0.0.11 valid=10s ipv6=off (the docker embedded dns — request-time resolution, the vault law)",
63
+ "body": "client_max_body_size 0 — the engine quota gate is the only ceiling",
64
+ "timeouts": "read 300s / send 300s",
65
+ "carries": "the monetization console rides the platform content lanes (GET /api/v1/ui/content) through the configured https base the build stamps; the /api lane carries the same-origin calls to the cobaia container"
66
+ },
67
+ "ports": {
68
+ "faceHost": 8083,
69
+ "faceInternal": 8080,
70
+ "engineInternal": 8080,
71
+ "engineDirectCommented": 3213,
72
+ "collisionLaw": "every family face owns one distinct host door (80 for vault, 8081-8090 for the ten siblings) — no two composes ever claim the same host port"
73
+ }
74
+ }
package/homebrew.rb ADDED
@@ -0,0 +1,25 @@
1
+ # wenathlan-cobaia — the homebrew formula of the family (the homebrew channel).
2
+ # The lane stamps the version and sha256 at push time (the sums derive from
3
+ # the release tarball assets the release lane builds, never hardcoded).
4
+ class WenathlanCobaia < Formula
5
+ desc "The monetization surface — the plans, the support rails and the honest advertisement slots"
6
+ homepage "https://github.com/wenathlan/devthink"
7
+ url "https://github.com/wenathlan/devthink/releases/download/v2.0.167/wenathlan-cobaia-2.0.167.tgz"
8
+ version "2.0.167"
9
+ # sha256 stamped by the lane from the release asset (never hardcoded)
10
+ sha256 "0000000000000000000000000000000000000000000000000000000000000000"
11
+
12
+ depends_on "node"
13
+
14
+ def install
15
+ libexec.install Dir["*"]
16
+ (bin/"wenathlan-cobaia").write <<~SH
17
+ #!/bin/bash
18
+ exec "#{libexec}/cli.js" "$@"
19
+ SH
20
+ end
21
+
22
+ test do
23
+ assert_match "2.0.167", shell_output("#{bin}/wenathlan-cobaia --version")
24
+ end
25
+ end
package/keywords.txt ADDED
@@ -0,0 +1 @@
1
+ cobaia, cobaia app, cobaia devthink, cobaia monetization, devthink os, wenathlan, monetization surface, monetization app, mrr plans, subscription plans, honest ads, ad slots, ad inventory, ad placements, checkout links, github sponsors, ko-fi, support links, donation links, open source funding, creator monetization, posts feed, content publishing, public content, roster page, team roster, pricing page, plan comparison, free plan, pro plan, business plan, revenue dashboard, monetization dashboard, static monetization site, vite monetization app, react monetization app, open source monetization, developer tools monetization, monetization strategy, passive income, funding goals, sponsor tiers, transparent revenue, wenathlan cobaia, devthink family, monetization app 2026
@@ -0,0 +1,248 @@
1
+ // # monetization.ts — the one keyword file of the monetization domain: the
2
+ // consolidation law of the root (PR3 of the uploaded rules — one keyword
3
+ // file per domain, "unifique num único módulo as lógicas variantes +
4
+ // correlacionadas que hoje encontram-se espalhadas") merged the four
5
+ // monetization doors of the cobaia site into this seat — the honest ad slot
6
+ // registry (the former adsconfig.ts body below), the typed billing accessor
7
+ // (checkoutlinks.ts), the seed blog accessor (posts.ts) and the family
8
+ // directory accessor (roster.ts) — every export preserved verbatim, zero
9
+ // logic changes, zero behavior changes. The consolidation notes: the doors
10
+ // shared no duplicated helper (each rode its own thin accessors over the
11
+ // content lane), so the one import the four sources repeated — the
12
+ // content.ts door of the uistore chain — rides once at the top; and no two
13
+ // sources double-declared a const or a decl under one name, so every export
14
+ // keeps the exact name the importers always read (the wingets index
15
+ // re-exports the roster pair by name so its surface stays identical). The
16
+ // four source files are deleted; every importer reads this one seat.
17
+
18
+ import {
19
+ type blogpost,
20
+ type checkoutplan,
21
+ contenttable,
22
+ contentvalue,
23
+ type familymember,
24
+ seedPlans,
25
+ seedPosts,
26
+ seedRoster,
27
+ seedSupportLinks,
28
+ type supportlinks as supportlinkstable,
29
+ } from "./content.ts";
30
+
31
+ export type { blogpost, checkoutplan, familymember, supportlinkstable as supportlinks };
32
+
33
+ /* ════════════════════════════════════════════════════════════════════ */
34
+ /* SECTION: adsconfig.ts — the honest ad slot registry (the ad seat). */
35
+ /* ════════════════════════════════════════════════════════════════════ */
36
+
37
+ // # adsconfig.ts — the honest ad slot registry of the family site, pure data
38
+ // at the app root. The family site carries ONE reserved ad container per
39
+ // content page (home, the app directory, the blog index), always at the end
40
+ // of the content, always labeled "advertisement", always a fixed aspect
41
+ // ratio so the page never shifts when a real unit arrives.
42
+ //
43
+ // THE SCRIPT RULE: no ad network script lives in this repository. The
44
+ // AdSense (or equivalent) loader is injected AFTER the network approves the
45
+ // site, through the deploy environment only. Until then the slot ids stay
46
+ // empty and the container renders the honest empty note. No network call,
47
+ // no click encouragement, no automated traffic ever touches an ad surface —
48
+ // invalid traffic is the one thing that kills both the ad account and the
49
+ // domain reputation, so the family does not do it.
50
+
51
+ /** one reserved ad slot: the config id and the reserved shape. */
52
+ export type adsslot = {
53
+ /** the ad network slot id — empty until the owner configures it after approval */
54
+ slotid: string;
55
+ /** the display format the reserved box mimics */
56
+ format: string;
57
+ /** the css aspect-ratio value the container reserves (never shifts layout) */
58
+ ratio: string;
59
+ };
60
+
61
+ /** the fixed label every ad container renders, visible and honest. */
62
+ export const ADS_LABEL = "advertisement";
63
+
64
+ /** the honest note the container renders while no slot id is configured. */
65
+ export const ADS_EMPTY = "ad slot not configured yet";
66
+
67
+ /** the reserved slots per content page, keyed by the page folder name. */
68
+ export const ADSSLOTS: Readonly<Record<string, adsslot>> = {
69
+ home: { slotid: "", format: "display", ratio: "728 / 90" },
70
+ apps: { slotid: "", format: "in-feed", ratio: "3 / 1" },
71
+ blog: { slotid: "", format: "in-article", ratio: "3 / 1" },
72
+ };
73
+
74
+ /**
75
+ * Reads the reserved slot of one content page.
76
+ *
77
+ * @param page the page folder name (home, apps, blog).
78
+ * @returns the slot, or undefined when the page carries no slot.
79
+ */
80
+ export function adsslotfor(page: string): adsslot | undefined {
81
+ return ADSSLOTS[page];
82
+ }
83
+
84
+ /**
85
+ * Answers whether a slot is configured with a real network id. An empty
86
+ * (or whitespace) slot id means the container stays an honest placeholder.
87
+ *
88
+ * @param slot the reserved slot entry.
89
+ * @returns true only when the slot id carries a value.
90
+ */
91
+ export function adsslotlive(slot: adsslot | undefined): boolean {
92
+ return slot !== undefined && slot.slotid.trim().length > 0;
93
+ }
94
+
95
+ /* ════════════════════════════════════════════════════════════════════ */
96
+ /* SECTION: checkoutlinks.ts — the typed billing accessor (the plans */
97
+ /* seat). */
98
+ /* ════════════════════════════════════════════════════════════════════ */
99
+
100
+ // # checkoutlinks.ts — the typed accessor of the billing configuration of
101
+ // the family site: the thin door the pricing page, the support page and
102
+ // the intro plans zone import, answering the plan table and the support
103
+ // channels of the content lane (content.ts — the manifest; db.ts — the
104
+ // content_rows table; the platform gateway — GET /api/v1/ui/content). The
105
+ // data never lives here anymore: checkoutlinks() and supportlinks() answer
106
+ // the live rows the platform served, the manifest seed otherwise (the
107
+ // uistore chain). The payment links themselves are configuration, never
108
+ // code: the owner pins the checkout hrefs (Stripe Payment Links or Paddle
109
+ // overlays) into the store once billing goes live. Until then every href
110
+ // ships empty and the interface renders the honest empty state — "checkout
111
+ // not configured yet" — instead of pretending.
112
+ //
113
+ // The words never ride here: every plan sentence (name, billing period,
114
+ // blurb, features) answers from the label store (labels.ts — the manifest
115
+ // the seed carries into the site database), so the pricing cards and the
116
+ // intro plans zone read their prose through the typed accessor and this
117
+ // lane keeps only the billing values — ids, prices and links.
118
+ //
119
+ // No network call ever happens here: the links are plain hrefs the visitor
120
+ // chooses to follow, no key is committed (not even a placeholder that looks
121
+ // real) and no script is loaded. The prices are placeholders (US$0 / 5 / 19
122
+ // per month) until the billing platform decision lands.
123
+
124
+ /**
125
+ * The typed accessor of the pricing table: the live db rows when the
126
+ * platform answered, the manifest seed otherwise — in display order (the
127
+ * prose answers from the label store; only the billing values ride this
128
+ * lane).
129
+ *
130
+ * @returns the plans of the /pricing page.
131
+ */
132
+ export function checkoutlinks(): readonly checkoutplan[] {
133
+ return contenttable("plans", seedPlans);
134
+ }
135
+
136
+ /**
137
+ * The typed accessor of the support channels: the live db row when the
138
+ * platform answered, the manifest seed otherwise — the same rule as the
139
+ * plans: hrefs stay empty until the owner configures them.
140
+ *
141
+ * @returns the support channels of the /support page.
142
+ */
143
+ export function supportlinks(): Readonly<supportlinkstable> {
144
+ return contentvalue("supportlinks", seedSupportLinks);
145
+ }
146
+
147
+ /**
148
+ * Answers whether a configured link is live. An empty (or whitespace) href
149
+ * means the feature is not configured yet and the interface must say so.
150
+ *
151
+ * @param href the configured link href.
152
+ * @returns true only when the href carries a real target.
153
+ */
154
+ export function isconfigured(href: string): boolean {
155
+ return href.trim().length > 0;
156
+ }
157
+
158
+ /**
159
+ * Finds one plan of the table by its stable id.
160
+ *
161
+ * @param id the plan id (free, pro, studio).
162
+ * @returns the plan, or undefined when no plan carries the id.
163
+ */
164
+ export function planbyid(id: string): checkoutplan | undefined {
165
+ return checkoutlinks().find((plan) => plan.id === id);
166
+ }
167
+
168
+ /* ════════════════════════════════════════════════════════════════════ */
169
+ /* SECTION: posts.ts — the seed blog accessor (the blog seat). */
170
+ /* ════════════════════════════════════════════════════════════════════ */
171
+
172
+ // # posts.ts — the typed accessor of the seed blog: the thin door the blog
173
+ // page and the tests import, answering the posts of the content lane
174
+ // (content.ts — the manifest; db.ts — the content_rows table; the platform
175
+ // gateway — GET /api/v1/ui/content). The data never lives here anymore:
176
+ // posts() answers the live rows the platform served, the manifest seed
177
+ // otherwise (the uistore chain), the index sort and the honest word count
178
+ // stay exactly where they rode, and the blog index (/blog) renders the
179
+ // sorted index and the full bodies through this door — real, written
180
+ // content about how the family is built, so the site is useful on its own
181
+ // merits (the ad networks approve content, not containers). New posts
182
+ // append to the manifest; the index sorts by date, so the manifest order
183
+ // never matters.
184
+
185
+ /**
186
+ * The typed accessor of the blog: the live db rows when the platform
187
+ * answered, the manifest seed otherwise.
188
+ *
189
+ * @returns the posts of the blog.
190
+ */
191
+ export function posts(): readonly blogpost[] {
192
+ return contenttable("posts", seedPosts);
193
+ }
194
+
195
+ /**
196
+ * Builds the blog index: the posts sorted by date, newest first. The ISO
197
+ * date sort is lexicographic and stable, so the index order never depends
198
+ * on the row order the store answers.
199
+ *
200
+ * @returns the posts of the blog, newest first.
201
+ */
202
+ export function postindex(): readonly blogpost[] {
203
+ return [...posts()].sort((a, b) => {
204
+ if (a.date < b.date) return 1;
205
+ if (a.date > b.date) return -1;
206
+ return 0;
207
+ });
208
+ }
209
+
210
+ /**
211
+ * Counts the words of one post's body — the honest length measure the tests
212
+ * and the page footer can both read.
213
+ *
214
+ * @param post the post to measure.
215
+ * @returns the total word count across the body paragraphs.
216
+ */
217
+ export function postwords(post: blogpost): number {
218
+ return post.body.join(" ").split(/\s+/).filter(Boolean).length;
219
+ }
220
+
221
+ /* ════════════════════════════════════════════════════════════════════ */
222
+ /* SECTION: roster.ts — the family directory accessor (the directory */
223
+ /* seat). */
224
+ /* ════════════════════════════════════════════════════════════════════ */
225
+
226
+ // # roster.ts — the typed accessor of the family directory data: the thin
227
+ // door the /apps page, the shell footer, the home preview and the intro
228
+ // footer import, answering the roster of the content lane (content.ts —
229
+ // the manifest; db.ts — the content_rows table; the platform gateway — GET
230
+ // /api/v1/ui/content). The data never lives here anymore: roster() answers
231
+ // the live rows the platform served, the manifest seed otherwise (the
232
+ // uistore chain). Every deploy unit beside the cobaia, with the one accent
233
+ // each carries (the spec §12 table, mirrored from the argan contract; the
234
+ // devthink OS rides the family sun) and the honest role each app declares
235
+ // in its own devthink.toml — condensed to one line, nothing embellished:
236
+ // the page and the tests read the same table through this door, so the
237
+ // directory can never drift from the family.
238
+
239
+ /**
240
+ * The typed accessor of the directory: the live db rows when the platform
241
+ * answered, the manifest seed otherwise — rail order, the OS first, then
242
+ * the applications.
243
+ *
244
+ * @returns the directory roster.
245
+ */
246
+ export function roster(): readonly familymember[] {
247
+ return contenttable("roster", seedRoster);
248
+ }
package/nginx.conf ADDED
@@ -0,0 +1,106 @@
1
+ # cobaia face — the monetization face: the nginx surface of the cobaia container pair.
2
+ # the static theme of the cobaia container pair: the ads and plans console
3
+ # the vite build assembles into the site container, served beside the /api/*
4
+ # proxy lane that carries every platform call to the cobaia container.
5
+ # until this conf the app rode the registry image alone: no compose seated
6
+ # the pair, and every /api call of the theme answered the away rehearsal.
7
+ #
8
+ # the resolver is the docker embedded dns (127.0.0.11) — the vault face law
9
+ # the family standardizes: the engine hostname resolves at request time, so
10
+ # this container starts even while the engine recovers and keeps answering
11
+ # after an engine recreation changes its address (an upstream block would
12
+ # pin the address at config load and die with the first engine restart).
13
+
14
+ resolver 127.0.0.11 valid=10s ipv6=off;
15
+
16
+ server {
17
+ listen 8080;
18
+ server_name _;
19
+
20
+ # the staged theme surface (the shell, the hashed bundles, the fonts,
21
+ # the icons and the crawler files the build stages).
22
+ root /usr/share/nginx/html;
23
+ index index.html;
24
+
25
+ # the security header set the netlify _headers file declares — one
26
+ # header law across every deploy surface of the family.
27
+ add_header X-Content-Type-Options nosniff always;
28
+ add_header X-Frame-Options DENY always;
29
+ add_header Referrer-Policy strict-origin-when-cross-origin always;
30
+
31
+ # the transport law: the text faces ride compressed, the proxied
32
+ # answers included (gzip_proxied any — the engine lanes compress the
33
+ # same way).
34
+ gzip on;
35
+ gzip_vary on;
36
+ gzip_proxied any;
37
+ gzip_comp_level 6;
38
+ gzip_min_length 1024;
39
+ gzip_types text/plain text/css text/javascript application/javascript application/json application/xml image/svg+xml text/webmanifest;
40
+
41
+ # the api lane — every /api/* call rides the engine upstream (the
42
+ # compose `cobaia-engine` service, port 8080): the ^~ law keeps the lane
43
+ # ahead of every regex block below (a hashed asset pattern never steals
44
+ # an api path), the platform headers pass verbatim, the raw bodies ride
45
+ # un-sized (client_max_body_size 0 — the engine's own quota gate is the
46
+ # only ceiling a call answers), and the generous read window covers the
47
+ # slow platform lanes.
48
+ location ^~ /api/ {
49
+ set $cobaia_engine http://cobaia-engine:8080;
50
+ proxy_pass $cobaia_engine;
51
+ proxy_http_version 1.1;
52
+ proxy_set_header Host $host;
53
+ proxy_set_header X-Real-IP $remote_addr;
54
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
55
+ proxy_set_header X-Forwarded-Proto $scheme;
56
+ client_max_body_size 0;
57
+ proxy_read_timeout 300s;
58
+ proxy_send_timeout 300s;
59
+ }
60
+
61
+ # the faun block of the face lane (the vault law): a database face
62
+ # inside the staged tree answers 403 — the raw store never ships over
63
+ # the face, the data flows through /api only.
64
+ location ~* \.(db|sqlite|sqlite3)(-wal|-shm|-journal)?$ {
65
+ return 403;
66
+ }
67
+
68
+ # the root-emit seal: the staged tree carries the sources beside the
69
+ # built faces (the family root-emit law), so the faces the platform
70
+ # lanes never publish answer the refusal here — the engine sources,
71
+ # the manifests and the lockfiles never leave the tree.
72
+ location ~* \.(ts|tsx|mts|cts|sql|prisma|lock|gemspec|csproj|java|go|kts|jsonc|json|toml|xml)(-wal|-shm|-journal)?$ {
73
+ return 403;
74
+ }
75
+
76
+ # the dotfile seal: the environment faces (.env, the editor seats)
77
+ # never answer over the face.
78
+ location ~* /\. {
79
+ return 403;
80
+ }
81
+
82
+ # the tree seal: the source homes the build never assembles refuse the
83
+ # same way (the modules, the suites, the docs, the sol sources).
84
+ location ~* ^/(node_modules|tests|docs|sol)(/|$) {
85
+ return 403;
86
+ }
87
+
88
+ # the hashed-asset cache law: the bundles the build stamps carry the
89
+ # content hash in the name, so the long lease is honest — the security
90
+ # header set repeats inside the lease the way the nginx inheritance law
91
+ # demands (an add_header at the location level would drop the server set).
92
+ location ~* \.(js|css|svg|woff2?|png|jpg|jpeg|webp|avif|ico|webmanifest)$ {
93
+ expires 30d;
94
+ add_header Cache-Control "public, immutable";
95
+ add_header X-Content-Type-Options nosniff always;
96
+ add_header X-Frame-Options DENY always;
97
+ add_header Referrer-Policy strict-origin-when-cross-origin always;
98
+ }
99
+
100
+ # the SPA rewrite the vercel/netlify configs declare: every deep link
101
+ # serves the shell, the router resolves it (the former site container
102
+ # had no fallback — a deep link answered 404).
103
+ location / {
104
+ try_files $uri $uri/ /index.html;
105
+ }
106
+ }