@wenathlan/cobaia 2.0.165 → 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 +162 -4
- package/biome.jsonc +3 -3
- package/build.gradle.kts +4 -4
- package/bun.lock +1820 -487
- package/cobaia.csproj +2 -2
- package/cobaia.gemspec +2 -2
- package/cobaia.java +2 -2
- package/db.ts +87 -3
- package/docker-compose.yml +58 -0
- package/docker.config +74 -0
- package/docs/cobaia.md +31 -0
- package/docs/distributionlanes.md +1 -1
- package/keywords.txt +1 -0
- package/labels.ts +23 -1
- package/monetization.ts +248 -0
- package/nginx.conf +106 -0
- package/onboardingstate.ts +211 -0
- package/package.json +430 -83
- package/pnpm-workspace.yaml +60 -38
- package/pom.xml +4 -4
- package/routerbase.ts +1 -0
- package/schema.prisma +23 -0
- package/sol/Sol.tsx +20 -15
- package/sol/apps/apps.tsx +1 -1
- package/sol/blog/blog.tsx +1 -1
- package/sol/home/home.tsx +14 -4
- package/sol/intro/intro.tsx +13 -5
- package/sol/onboarding/onboarding.tsx +146 -0
- package/sol/pricing/pricing.tsx +1 -1
- package/sol/shell/Shell.tsx +1 -1
- package/sol/sol.css +153 -0
- package/sol/support/support.tsx +1 -1
- package/sol/wingets/HomeEntrygate.tsx +64 -0
- 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 +3 -1
- package/tauri.conf.json +1 -1
- package/{tests → test}/adsconfig.test.ts +2 -2
- package/{tests → test}/checkoutlinks.test.ts +2 -2
- package/test/entryflow.test.ts +124 -0
- 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/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
|
+
}
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* onboardingstate.ts — the ONE writer of the visitor's seen flags, the state
|
|
3
|
+
* of the entry-flow contract (intro → onboarding → the site). The state is
|
|
4
|
+
* { introseen, onboardingseen } and it is catalogued, not just browser: the
|
|
5
|
+
* accessor consults the platform database over GET /api/v1/onboarding and
|
|
6
|
+
* writes it over POST /api/v1/onboarding, both HTTPS only — the base url
|
|
7
|
+
* arrives through VITE_ONBOARDING_URL and every candidate host is validated
|
|
8
|
+
* first (localhost, loopback, private, CGNAT, link-local and reserved ranges
|
|
9
|
+
* are refused, mapped IPv6 is normalized, so the flags never ride a dev or
|
|
10
|
+
* internal endpoint). The localStorage key cobaia.onboarding.seen is purely
|
|
11
|
+
* the offline cache of the database (the DB is the source of truth; the
|
|
12
|
+
* cache avoids replaying the flow while offline) and module memory covers
|
|
13
|
+
* the client-side re-navigation between the gates. Reads reconcile with a
|
|
14
|
+
* seen-wins merge — a stage seen anywhere stays seen, the flow is never
|
|
15
|
+
* forced twice. The page anchors never touch the flags directly: the intro
|
|
16
|
+
* marks introseen, the onboarding marks onboardingseen, the home entry gate
|
|
17
|
+
* only reads (the family flow contract the sibling apps already ride —
|
|
18
|
+
* cobaia joins the standardized intro → onboarding → entrada law).
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
/** the visitor state the entry flow consults (the two gates of the site). */
|
|
22
|
+
export type VisitorOnboarding = {
|
|
23
|
+
/** the presentation intro was seen — the splash never replays */
|
|
24
|
+
introseen: boolean;
|
|
25
|
+
/** the first-run onboarding was seen (finish or skip) — never forced again */
|
|
26
|
+
onboardingseen: boolean;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
/** the stages one call can mark seen. */
|
|
30
|
+
export type OnboardingStage = "intro" | "onboarding";
|
|
31
|
+
|
|
32
|
+
/** the localStorage cache key (the DB mirror, never the source of truth). */
|
|
33
|
+
const CACHE_KEY = "cobaia.onboarding.seen";
|
|
34
|
+
|
|
35
|
+
/** the anonymous visitor of this surface (the platform gateway may key its own). */
|
|
36
|
+
const VISITOR = "web";
|
|
37
|
+
|
|
38
|
+
/** the configured platform base (HTTPS self-hosted gateway); empty answers cache-only. */
|
|
39
|
+
const BASE =
|
|
40
|
+
(typeof import.meta !== "undefined" ? (import.meta.env?.VITE_ONBOARDING_URL as string | undefined) : undefined)
|
|
41
|
+
?.trim()
|
|
42
|
+
.replace(/\/+$/, "") ?? "";
|
|
43
|
+
|
|
44
|
+
/** the platform route the accessor rides (the gateway serves /api/v1/*). */
|
|
45
|
+
const API_PATH = "/api/v1/onboarding";
|
|
46
|
+
|
|
47
|
+
/** the abort window of the platform read (ms) — the flow never blocks on a slow gateway. */
|
|
48
|
+
const READ_TIMEOUT_MS = 2500;
|
|
49
|
+
|
|
50
|
+
/** module memory: the flags of the session, consulted before any i/o. */
|
|
51
|
+
let memory: VisitorOnboarding | null = null;
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Validates a platform base url: HTTPS only, and the host must be public —
|
|
55
|
+
* localhost, loopback, private, link-local, CGNAT, unique-local and
|
|
56
|
+
* reserved documentation ranges are refused; IPv4-mapped IPv6 literals are
|
|
57
|
+
* normalized to their IPv4 side and any other IPv6 literal is refused (the
|
|
58
|
+
* gateway rides a name).
|
|
59
|
+
*
|
|
60
|
+
* @param base the configured base url candidate.
|
|
61
|
+
* @returns true when the base may be consulted.
|
|
62
|
+
*/
|
|
63
|
+
export function hostallowed(base: string): boolean {
|
|
64
|
+
try {
|
|
65
|
+
const url = new URL(base);
|
|
66
|
+
if (url.protocol !== "https:") return false;
|
|
67
|
+
const host = url.hostname
|
|
68
|
+
.toLowerCase()
|
|
69
|
+
.replace(/\.+$/, "")
|
|
70
|
+
.replace(/^\[|\]$/g, "");
|
|
71
|
+
if (host.length === 0) return false;
|
|
72
|
+
if (host === "localhost" || host === "0.0.0.0" || host === "::1") return false;
|
|
73
|
+
if (host.endsWith(".localhost") || host.endsWith(".local") || host.endsWith(".internal")) return false;
|
|
74
|
+
// the ipv4-mapped ipv6 form (the url parser normalizes the decimals away)
|
|
75
|
+
if (host.startsWith("::ffff:")) {
|
|
76
|
+
const mapped = host.slice("::ffff:".length).match(/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/);
|
|
77
|
+
return mapped !== null && publicipv4(Number(mapped[1]), Number(mapped[2]));
|
|
78
|
+
}
|
|
79
|
+
const v4 = host.match(/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/);
|
|
80
|
+
if (v4) return publicipv4(Number(v4[1]), Number(v4[2]));
|
|
81
|
+
// every other ipv6 literal: the gateway rides a name, not an address
|
|
82
|
+
if (host.includes(":")) return false;
|
|
83
|
+
return true;
|
|
84
|
+
} catch {
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** the public side of an ipv4 address (the first two octets decide). */
|
|
90
|
+
function publicipv4(a: number, b: number): boolean {
|
|
91
|
+
if (a === 0 || a === 10 || a === 127) return false;
|
|
92
|
+
if (a === 172 && b >= 16 && b <= 31) return false;
|
|
93
|
+
if (a === 192 && b === 168) return false;
|
|
94
|
+
if (a === 169 && b === 254) return false;
|
|
95
|
+
if (a === 100 && b >= 64 && b <= 127) return false;
|
|
96
|
+
if (a === 192 && b === 0) return false;
|
|
97
|
+
if (a === 198 && (b === 51 || b === 52)) return false;
|
|
98
|
+
if (a === 203 && b === 0) return false;
|
|
99
|
+
return true;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Answers the flags after one stage was marked seen (pure: the writer and
|
|
104
|
+
* the tests share this merge — seen-wins, a stage never unsees).
|
|
105
|
+
*
|
|
106
|
+
* @param state the flags before the write.
|
|
107
|
+
* @param stage the stage being marked seen.
|
|
108
|
+
* @returns the flags after the write.
|
|
109
|
+
*/
|
|
110
|
+
export function mergeStage(state: VisitorOnboarding, stage: OnboardingStage): VisitorOnboarding {
|
|
111
|
+
return stage === "intro" ? { ...state, introseen: true } : { ...state, onboardingseen: true };
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** Reads the localStorage cache; any corruption answers null (the cache is best effort). */
|
|
115
|
+
function readCache(): VisitorOnboarding | null {
|
|
116
|
+
if (typeof window === "undefined") return null;
|
|
117
|
+
try {
|
|
118
|
+
const raw = window.localStorage.getItem(CACHE_KEY);
|
|
119
|
+
if (!raw) return null;
|
|
120
|
+
const parsed = JSON.parse(raw) as Partial<VisitorOnboarding>;
|
|
121
|
+
return { introseen: parsed.introseen === true, onboardingseen: parsed.onboardingseen === true };
|
|
122
|
+
} catch {
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** Mirrors the flags into the localStorage cache (silently skipped when storage is sealed). */
|
|
128
|
+
function writeCache(state: VisitorOnboarding): void {
|
|
129
|
+
if (typeof window === "undefined") return;
|
|
130
|
+
try {
|
|
131
|
+
window.localStorage.setItem(CACHE_KEY, JSON.stringify(state));
|
|
132
|
+
} catch {
|
|
133
|
+
// the cache is best effort: the DB and the memory stay authoritative
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** Reads the flags off the platform database; unreachable or unconfigured answers null. */
|
|
138
|
+
async function readRemote(): Promise<VisitorOnboarding | null> {
|
|
139
|
+
if (!BASE || !hostallowed(BASE) || typeof fetch === "undefined") return null;
|
|
140
|
+
const controller = new AbortController();
|
|
141
|
+
const timer = setTimeout(() => controller.abort(), READ_TIMEOUT_MS);
|
|
142
|
+
try {
|
|
143
|
+
const response = await fetch(`${BASE}${API_PATH}?visitor=${encodeURIComponent(VISITOR)}`, {
|
|
144
|
+
headers: { accept: "application/json" },
|
|
145
|
+
signal: controller.signal,
|
|
146
|
+
});
|
|
147
|
+
if (!response.ok) return null;
|
|
148
|
+
const answer = (await response.json()) as Partial<VisitorOnboarding>;
|
|
149
|
+
return { introseen: answer.introseen === true, onboardingseen: answer.onboardingseen === true };
|
|
150
|
+
} catch {
|
|
151
|
+
return null;
|
|
152
|
+
} finally {
|
|
153
|
+
clearTimeout(timer);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/** Writes the flags to the platform database over POST; failures leave the cache holding them. */
|
|
158
|
+
async function writeRemote(state: VisitorOnboarding): Promise<void> {
|
|
159
|
+
if (!BASE || !hostallowed(BASE) || typeof fetch === "undefined") return;
|
|
160
|
+
try {
|
|
161
|
+
await fetch(`${BASE}${API_PATH}`, {
|
|
162
|
+
method: "POST",
|
|
163
|
+
headers: { "content-type": "application/json" },
|
|
164
|
+
body: JSON.stringify({ visitor: VISITOR, ...state }),
|
|
165
|
+
});
|
|
166
|
+
} catch {
|
|
167
|
+
// the cache mirrors the flags until the platform answers again
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Reads the visitor state: module memory first, then the platform database
|
|
173
|
+
* over GET /api/v1/onboarding, then the localStorage cache — reconciled with
|
|
174
|
+
* the seen-wins merge so a stage seen anywhere stays seen. The session
|
|
175
|
+
* answer is memoized; re-navigation never refetches.
|
|
176
|
+
*
|
|
177
|
+
* @returns the visitor onboarding flags.
|
|
178
|
+
*/
|
|
179
|
+
export async function readOnboardingState(): Promise<VisitorOnboarding> {
|
|
180
|
+
if (memory) return memory;
|
|
181
|
+
const cache = readCache();
|
|
182
|
+
const remote = await readRemote();
|
|
183
|
+
const state: VisitorOnboarding = {
|
|
184
|
+
introseen: (cache?.introseen ?? false) || (remote?.introseen ?? false),
|
|
185
|
+
onboardingseen: (cache?.onboardingseen ?? false) || (remote?.onboardingseen ?? false),
|
|
186
|
+
};
|
|
187
|
+
memory = state;
|
|
188
|
+
if (remote) writeCache(state);
|
|
189
|
+
return state;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Marks one stage seen and persists it everywhere: module memory, the
|
|
194
|
+
* localStorage cache and — when a platform base is configured — the
|
|
195
|
+
* database over POST /api/v1/onboarding. The ONLY writer of the flags.
|
|
196
|
+
*
|
|
197
|
+
* @param stage the stage the visitor just saw (or deliberately skipped).
|
|
198
|
+
* @returns the flags after the write.
|
|
199
|
+
*/
|
|
200
|
+
export async function markOnboardingStage(stage: OnboardingStage): Promise<VisitorOnboarding> {
|
|
201
|
+
const previous = memory ?? (await readOnboardingState());
|
|
202
|
+
memory = mergeStage(previous, stage);
|
|
203
|
+
writeCache(memory);
|
|
204
|
+
await writeRemote(memory);
|
|
205
|
+
return memory;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/** Test seam: clears the module memory (the cache and the platform are untouched). */
|
|
209
|
+
export function resetOnboardingMemory(): void {
|
|
210
|
+
memory = null;
|
|
211
|
+
}
|