Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

- `src/` – portable declarative and imperative workflow SDKs, host-resource admission, diagrams, direct Vercel AI Gateway generation, local vectorization, semantic operations, MCP, and canvas integration.
- `apps/desktop/` – the canonical `atet` CLI, complete local media host, durable scheduler, HTML and Three.js overlays, native capture helpers, frontend, and desktop shell.
- `apps/web/` – the dependency-free static `atet.sh` site, with no API, account, or credential surface.
- `apps/web/` – the static `atet.sh` site, with a local browser bundle and a production-only anonymous pageview boundary but no API, account, or credential surface.
- `packages/scene/` – the shared local scene-analysis contract.
- `src/code/` – portable declarative graph authoring, the closed public capability projection, compilation, planning, and execution contracts.
- `schema/` – version-one diagram JSON Schema.
Expand Down
5 changes: 5 additions & 0 deletions NOTICE.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,3 +42,8 @@ Optional model-backed generation uses
sent to Vercel AI Gateway and the model provider selected by the caller, under
those services' terms. Atet does not operate an intermediary generation
service.

The production `atet.sh` browser bundle includes
[`posthog-js`](https://github.com/PostHog/posthog-js), distributed under its
combined Apache License 2.0 and MIT terms. The fingerprinted bundle carries the
upstream license text generated from the exact locked package.
3 changes: 3 additions & 0 deletions apps/web/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Configure this public client token only in Vercel's Production environment.
NEXT_PUBLIC_POSTHOG_KEY=
NEXT_PUBLIC_POSTHOG_HOST=https://us.i.posthog.com
7 changes: 4 additions & 3 deletions apps/web/AGENTS.md
Original file line number Diff line number Diff line change
@@ -1,17 +1,18 @@
# Contents

- `src/` contains the static `atet.sh` homepage and documentation, visual system, appearance control, crawler files, favicons, and social preview.
- `scripts/build.ts` renders fingerprinted local assets into `dist/` from an explicit allowlist without dependencies or network access.
- `scripts/build.ts` renders fingerprinted local assets into `dist/` from an explicit allowlist and bundles the pinned PostHog browser client only for a configured Production build.
- `site.test.ts`, `package.json`, and `vercel.json` define the content, identity, accessibility, performance, legacy-host, and deployment contracts.

# Guidelines

- Keep the site static and useful without JavaScript. JavaScript may improve appearance controls, but it must never call a network API, load remote code, or receive credentials.
- Keep the site static and useful without JavaScript. Browser code may load only fingerprinted local assets. The sole runtime request is the reviewed PostHog pageview boundary below.
- Keep `apps/web` independently installable from its Vercel Root Directory: pin every dependency exactly in this package and commit its local `bun.lock`. Verify an isolated `bun install --frozen-lockfile --ignore-scripts`; do not depend on the parent workspace catalog or lockfile.
- Describe the released SDK, local host, and desktop capture shell as one Atet system. Do not introduce a hosted account, billing, authentication, or generation service.
- Keep generation credentials in local SDK or CLI processes. The browser must never accept, store, forward, or render an AI Gateway credential.
- Preserve the four public output families: images, diagrams, animated loops, and video. Treat audio and captions as composable project inputs rather than another project model.
- Use Atet, Ra's solar barque, respectfully as an abstract metaphor for passage and transformation. Do not imitate sacred figures, hieroglyphs, or archaeological objects.
- Keep every page semantic, keyboard-operable, readable at 200% zoom, and free of analytics, remote fonts, client frameworks, and runtime network requests. Keep the homepage concise; put durable usage detail in `/docs`.
- Keep every page semantic, keyboard-operable, readable at 200% zoom, and free of remote fonts and client frameworks. Analytics may emit one anonymous cookieless `$pageview` from `https://atet.sh/` to `https://us.i.posthog.com`, tagged with `site_id=atet` and `analytics_schema_version=1`. Keep persons, persistence, autocapture, replay, flags, surveys, heatmaps, pageleave, web vitals, referrer, URL, query, hash, page text, content, and custom events disabled. Do not initialize analytics on Preview, staging, alternate hosts, or `404.html`.
- Organize `/docs` by user intent: guided learning, goal-oriented how-to, factual reference, and conceptual explanation. Do not mix those modes into one undifferentiated command catalog.
- Use the canonical Hraness footer lockup. Include every durable public route in crawler discovery.
- Preserve permanent production and preview redirects for every reviewed predecessor host without redirecting canonical Atet hosts.
Expand Down
17 changes: 11 additions & 6 deletions apps/web/README.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
# Atet web

`atet.sh` is the dependency-free public site and documentation for Atet. It
`atet.sh` is the static public site and documentation for Atet. It
presents the SDK, Bun CLI, local runtime, and desktop capture shell without
adding a server, account surface, API route, analytics, remote font, or browser credential path.
adding a server, account surface, API route, remote font, or browser credential path.
Generation runs from the local Atet SDK or CLI with the operator's Vercel AI
Gateway access.

Expand All @@ -11,7 +11,12 @@ bun run check
```

The build fingerprints the local stylesheet and appearance script, then copies
an explicit allowlist from `src/` into `dist/`. Configure the Vercel project
with this directory as its Root Directory. The checked `vercel.json` performs
no dependency install, serves only built files under a strict CSP, and sends
reviewed predecessor hosts to their matching Atet production or preview host.
an explicit allowlist from `src/` into `dist/`. A configured Vercel Production
build also bundles the pinned PostHog client as a fingerprinted local asset.
That client sends one anonymous cookieless pageview from `https://atet.sh/` and
does not run on Preview, alternate hosts, or the not-found page. Configure the
Vercel project with this directory as its Root Directory. The checked
`vercel.json` installs from this directory's frozen Bun lockfile without relying
on the parent workspace catalog, serves only built files under a strict CSP,
and sends reviewed predecessor hosts to their matching Atet production or
preview host.
37 changes: 37 additions & 0 deletions apps/web/bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 4 additions & 1 deletion apps/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,12 @@
"name": "@hraness/atet-web",
"version": "0.0.0",
"private": true,
"description": "The dependency-free static public site for Atet.",
"description": "The static public site for Atet.",
"type": "module",
"packageManager": "bun@1.3.14",
"dependencies": {
"posthog-js": "1.413.2"
},
"scripts": {
"build": "bun run ./scripts/build.ts",
"check": "bun run test && bun run build",
Expand Down
95 changes: 87 additions & 8 deletions apps/web/scripts/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@ import { fileURLToPath } from "node:url"

const appDirectory = dirname(dirname(fileURLToPath(import.meta.url)))
const sourceDirectory = join(appDirectory, "src")
const outputDirectory = join(appDirectory, "dist")
const defaultOutputDirectory = join(appDirectory, "dist")
const posthogIngestOrigin = "https://us.i.posthog.com"
const posthogPackageDirectory = dirname(fileURLToPath(import.meta.resolve("posthog-js/package.json")))

const copiedFiles = [
"apple-touch-icon.png",
Expand Down Expand Up @@ -37,10 +39,75 @@ function renderDocument(template: string, assets: Readonly<Record<string, string
return rendered
}

export async function buildWebsite(): Promise<Readonly<{
type BuildEnvironment = Readonly<Record<string, string | undefined>>

type BuildOptions = Readonly<{
environment?: BuildEnvironment
outputDirectory?: string
}>

function productionAnalyticsConfig(environment: BuildEnvironment): Readonly<{
host: string
key: string
}> | null {
const key = environment.NEXT_PUBLIC_POSTHOG_KEY?.trim()
if (environment.VERCEL_ENV !== "production" || key === undefined || key === "") {
return null
}
if (!/^phc_[A-Za-z0-9_-]+$/u.test(key)) {
throw new Error("NEXT_PUBLIC_POSTHOG_KEY must be a PostHog project token")
}

const host = environment.NEXT_PUBLIC_POSTHOG_HOST?.trim() || posthogIngestOrigin
if (host !== posthogIngestOrigin) {
throw new Error(`NEXT_PUBLIC_POSTHOG_HOST must equal ${posthogIngestOrigin}`)
}
return { host, key }
}

async function bundleAnalytics(config: Readonly<{ host: string; key: string }>): Promise<Uint8Array> {
const [license, manifestSource] = await Promise.all([
readFile(join(posthogPackageDirectory, "LICENSE"), "utf8"),
readFile(join(posthogPackageDirectory, "package.json"), "utf8"),
])
const manifest = JSON.parse(manifestSource) as unknown
if (
typeof manifest !== "object"
|| manifest === null
|| !("version" in manifest)
|| typeof manifest.version !== "string"
|| license.includes("*/")
) {
throw new Error("The installed PostHog package has an invalid license boundary")
}
const result = await Bun.build({
banner: `/*! posthog-js ${manifest.version}\n${license.trim()}\n*/`,
define: {
__ATET_POSTHOG_HOST__: JSON.stringify(config.host),
__ATET_POSTHOG_KEY__: JSON.stringify(config.key),
},
entrypoints: [join(sourceDirectory, "analytics.ts")],
env: "disable",
format: "esm",
minify: true,
sourcemap: "none",
target: "browser",
})
if (!result.success || result.outputs.length !== 1) {
const details = result.logs.map(log => log.message).join("\n")
throw new Error(`Could not bundle the analytics client${details === "" ? "" : `: ${details}`}`)
}
return new Uint8Array(await result.outputs[0].arrayBuffer())
}

export async function buildWebsite(options: BuildOptions = {}): Promise<Readonly<{
analyticsPath: string | null
stylesPath: string
themePath: string
}>> {
const environment = options.environment ?? process.env
const outputDirectory = options.outputDirectory ?? defaultOutputDirectory
const analyticsConfig = productionAnalyticsConfig(environment)
const [indexTemplate, notFoundTemplate, styles, theme] = await Promise.all([
readFile(join(sourceDirectory, "index.html"), "utf8"),
readFile(join(sourceDirectory, "404.html"), "utf8"),
Expand All @@ -50,19 +117,30 @@ export async function buildWebsite(): Promise<Readonly<{

const stylesPath = assetPath("styles.css", styles)
const themePath = assetPath("theme.js", theme)
const assets = {
const commonAssets = {
"{{CSS_ASSET}}": stylesPath,
"{{THEME_ASSET}}": themePath,
} as const
const analytics = analyticsConfig === null ? null : await bundleAnalytics(analyticsConfig)
const analyticsPath = analytics === null ? null : assetPath("analytics.js", analytics)
const indexAssets = {
...commonAssets,
"{{ANALYTICS_SCRIPT}}": analyticsPath === null
? ""
: `<script src="${analyticsPath}" type="module"></script>`,
} as const

await rm(outputDirectory, { force: true, recursive: true })
await mkdir(join(outputDirectory, "assets"), { recursive: true })

await Promise.all([
writeFile(join(outputDirectory, "index.html"), renderDocument(indexTemplate, assets)),
writeFile(join(outputDirectory, "404.html"), renderDocument(notFoundTemplate, assets)),
writeFile(join(outputDirectory, "index.html"), renderDocument(indexTemplate, indexAssets)),
writeFile(join(outputDirectory, "404.html"), renderDocument(notFoundTemplate, commonAssets)),
writeFile(join(outputDirectory, stylesPath.slice(1)), styles),
writeFile(join(outputDirectory, themePath.slice(1)), theme),
...(analyticsPath === null || analytics === null
? []
: [writeFile(join(outputDirectory, analyticsPath.slice(1)), analytics)]),
])

for (const file of copiedFiles) {
Expand All @@ -77,10 +155,11 @@ export async function buildWebsite(): Promise<Readonly<{
})
}

return { stylesPath, themePath }
return { analyticsPath, stylesPath, themePath }
}

if (import.meta.main) {
await buildWebsite()
console.log(`Built ${copiedFiles.length + 4} static files in ${outputDirectory}`)
const result = await buildWebsite()
const generatedFiles = copiedFiles.length + 4 + (result.analyticsPath === null ? 0 : 1)
console.log(`Built ${generatedFiles} static files in ${defaultOutputDirectory}`)
}
Loading
Loading