@wenathlan/cobaia 2.0.166 → 2.0.167
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/.dockerignore +1 -1
- package/App.tsx +2 -31
- package/Dockerfile +157 -3
- package/biome.jsonc +3 -3
- package/build.gradle.kts +1 -1
- package/bun.lock +386 -330
- package/cobaia.csproj +2 -2
- package/cobaia.gemspec +2 -2
- package/cobaia.java +1 -1
- package/docker-compose.yml +58 -0
- package/docker.config +74 -0
- package/keywords.txt +1 -0
- package/monetization.ts +248 -0
- package/nginx.conf +106 -0
- package/package.json +68 -67
- package/pnpm-workspace.yaml +60 -38
- package/pom.xml +2 -2
- package/routerbase.ts +1 -0
- package/sol/apps/apps.tsx +1 -1
- package/sol/blog/blog.tsx +1 -1
- package/sol/home/home.tsx +1 -1
- package/sol/pricing/pricing.tsx +1 -1
- package/sol/shell/Shell.tsx +1 -1
- package/sol/support/support.tsx +1 -1
- package/sol/wingets/IntroFooter.tsx +1 -1
- package/sol/wingets/IntroPlans.tsx +1 -1
- package/sol/wingets/ShellAdsslot.tsx +1 -1
- package/sol/wingets/index.ts +1 -1
- package/tauri.conf.json +1 -1
- package/{tests → test}/adsconfig.test.ts +2 -2
- package/{tests → test}/checkoutlinks.test.ts +2 -2
- package/{tests → test}/entryflow.test.ts +2 -2
- package/{tests → test}/familyurl.test.ts +1 -1
- package/{tests → test}/posts.test.ts +2 -2
- package/{tests → test}/sandboxmodality.test.ts +1 -1
- package/vitest.config.ts +1 -1
- package/adsconfig.ts +0 -59
- package/checkoutlinks.ts +0 -80
- package/posts.ts +0 -54
- package/roster.ts +0 -29
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.
|
|
7
|
+
<Version>2.0.167</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="
|
|
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.
|
|
3
|
+
spec.version = ENV.fetch("COBAIA_VERSION", "2.0.167")
|
|
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/**/*", "
|
|
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.
|
|
13
|
+
public static final String VERSION = "2.0.167";
|
|
14
14
|
private Cobaia() {
|
|
15
15
|
}
|
|
16
16
|
}
|
|
@@ -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.166}"
|
|
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.166}"
|
|
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/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
|
package/monetization.ts
ADDED
|
@@ -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
|
+
}
|