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
170 changes: 170 additions & 0 deletions LICENSES/CC-BY-SA-4.0.txt

Large diffs are not rendered by default.

43 changes: 36 additions & 7 deletions LICENSES/README.adoc
Original file line number Diff line number Diff line change
@@ -1,10 +1,39 @@
// SPDX-License-Identifier: MPL-2.0
// Copyright (c) Jonathan D.A. Jewell <j.d.a.jewell@open.ac.uk>
= Licensing Folder
// SPDX-License-Identifier: CC-BY-SA-4.0
// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell (hyperpolymath) <j.d.a.jewell@open.ac.uk>
= Licensing

Store repository licensing/supporting legal texts here.
boj-server uses exactly two licences.

Conventions:
[cols="1,1,2"]
|===
| Applies to | Licence | SPDX

* Keep root `LICENSE` for forge license scanners.
* Place extended licensing context and exhibits in this directory.
| Code (and all non-prose files) | Mozilla Public License 2.0 | `MPL-2.0`
| Documentation (prose) | Creative Commons Attribution-ShareAlike 4.0 | `CC-BY-SA-4.0`
|===

== Where it is declared

* *Root `LICENSE`* — the full MPL-2.0 text. This is what forge licence scanners
(GitHub, etc.) read, so the repository is indicated as *MPL-2.0*.
* *This folder* — the canonical full texts, named by SPDX identifier:
`MPL-2.0.txt` and `CC-BY-SA-4.0.txt`.
* *Per-file SPDX headers* — source files carry `SPDX-License-Identifier: MPL-2.0`;
documentation files carry `SPDX-License-Identifier: CC-BY-SA-4.0`.

GitHub-required Markdown files (`SECURITY.md`, `CONTRIBUTING.md`,
`CODE_OF_CONDUCT.md`, `CHANGELOG.md`) are retained as `.md` per the estate
standard; all other documentation is AsciiDoc (`.adoc`).

== Authoring new files

New code -> `MPL-2.0`. New documentation -> `CC-BY-SA-4.0`. No other identifier
should be introduced. Licence changes are owner-directed and per-file — never a
bulk SPDX sweep.

[NOTE]
====
`cartridges/pmpl-mcp/` is a cartridge *about* the Palimpsest licence (a product
feature). Its references to PMPL are subject matter, not a licence declaration for
this repository, and are out of scope for the two-licence scheme above.
====
27 changes: 27 additions & 0 deletions cloudflare-dns-zone.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
; SPDX-License-Identifier: MPL-2.0
; Reference DNS zone for boj-server.net (Jonathan D.A. Jewell).
;
; boj-server.net is ALREADY a zone in the Cloudflare account, so this file is a
; REFERENCE — not a bulk import. When you attach the custom domain to the Pages
; project (Workers & Pages -> boj-server -> Custom domains), Cloudflare creates the
; apex + www records automatically. Only add the security records below if they are
; not already present. Do NOT bulk-import over an existing live zone.

; --- Pages custom domain (auto-created by "Set up a custom domain") ---
; @ IN CNAME boj-server.pages.dev. ; apex via CNAME flattening (proxied)
; www IN CNAME boj-server.pages.dev. ; www (proxied)

; --- Certificate authority authorisation (Cloudflare edge certs use Let's Encrypt) ---
@ IN CAA 0 issue "letsencrypt.org"
@ IN CAA 0 issue "pki.goog"
@ IN CAA 0 iodef "mailto:j.d.a.jewell@open.ac.uk"

; --- This domain sends no mail: lock it down ---
@ IN TXT "v=spf1 -all"
@ IN MX 0 .
_dmarc IN TXT "v=DMARC1; p=reject; rua=mailto:j.d.a.jewell@open.ac.uk"

; --- Recommended Cloudflare zone settings (set in dashboard, not zone file) ---
; SSL/TLS mode: Full (strict)
; Edge Certificates: Always Use HTTPS = on, Min TLS = 1.2, Automatic HTTPS Rewrites = on
; DNSSEC: enable (add the DS record at your registrar)
122 changes: 122 additions & 0 deletions docs/website/CLOUDFLARE-SETUP.adoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
// SPDX-License-Identifier: CC-BY-SA-4.0
// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell (hyperpolymath) <j.d.a.jewell@open.ac.uk>
= Deploying boj-server.net on Cloudflare Pages
:toc: preamble
:icons: font

The public presence at https://boj-server.net[boj-server.net] is a *no-build static
bundle* in the `site/` directory, served by Cloudflare Pages. It is both a permanent
presence and an access point: an install configurator (base + NeSy / Agentic /
Coordination bundles), and a browsable catalogue of all {counter:cart}139 cartridges
generated from the canonical registry.

== What the site is

[cols="1,3"]
|===
| `site/index.html` | The hub: hero, install configurator, catalogue browser, capabilities.
| `site/assets/` | `style.css`, `app.js` (vanilla, zero-dependency), `favicon.svg`.
| `site/catalog.json` | Snapshot of the registry (139 cartridges). Regenerate — see below.
| `site/_headers` | Strict security headers (HSTS, CSP, etc.) in Pages format.
| `site/_redirects` | `www` -> apex canonicalisation + convenience deep-links.
| `site/CNAME` | Custom-domain marker (`boj-server.net`).
| `site/.well-known/` | `security.txt` (RFC 9116), mirrored from the repo root.
|===

No framework, no Node/npm, no build command. Cloudflare serves `site/` as-is.

== Go-live path A — Dashboard Git-connect (recommended, chosen)

`boj-server.net` is already a zone in the Cloudflare account, so this is a one-time,
~5-minute dashboard task. No secrets are stored in the repo or CI.

. *Create the project.* Dashboard -> *Workers & Pages* -> *Create application* ->
*Pages* -> *Connect to Git* -> authorise and select `hyperpolymath/boj-server`.
. *Build settings.*
* Production branch: `main`
* Framework preset: *None*
* Build command: *(leave empty)*
* Build output directory: `site`
* Root directory: `/`
+
Click *Save and Deploy*. The first deploy lands at `https://boj-server.pages.dev`.
. *Attach the custom domain.* Project -> *Custom domains* -> *Set up a custom domain*
-> `boj-server.net` -> *Continue*. Because the zone is already on Cloudflare, the
apex record is created automatically (CNAME-flattened to the Pages target). Repeat
for `www.boj-server.net` (the `_redirects` rule sends it to the apex).
. *TLS posture.* Zone -> *SSL/TLS* -> *Full (strict)*; *Edge Certificates* -> enable
*Always Use HTTPS*, *Automatic HTTPS Rewrites*, minimum TLS 1.2. Optionally enable
*DNSSEC* and add the DS record at the registrar.

That is the entire go-live. Every push to `main` then auto-deploys; pull requests get
preview URLs for free.

== Go-live path B — API / CLI (only if you want it automated or agent-driven)

Two equivalent options; both need a *Cloudflare API token* and the *account ID*.

`wrangler` (canonical uploader)::
+
[source,bash]
----
export CLOUDFLARE_API_TOKEN=... # scopes below
export CLOUDFLARE_ACCOUNT_ID=...
wrangler pages deploy site --project-name=boj-server
----

Estate AffineScript scripts (parity with `standards/avow-protocol/scripts/`)::
`scripts/cloudflare/CreatePagesProject.affine` creates the project + attaches
`boj-server.net` and `www.boj-server.net`; `scripts/cloudflare/DeployDirect.affine`
posts a deployment from `site/`. Both read `CLOUDFLARE_API_TOKEN` +
`CLOUDFLARE_ACCOUNT_ID` from the environment.

[#agent-access]
== What an agent needs to "see and act" on DNS / Pages

The connected *Cloudflare "Developer Platform" MCP* exposes only Workers (read),
KV, D1, R2 and Hyperdrive — *no DNS/zones tools and no Pages-project tools*. That is a
property of that MCP server, not of the token, so a broader token does not add DNS
tools to it. To let an agent inspect and change DNS / Pages directly, provide a scoped
*Cloudflare API token* (used against the REST API / `wrangler`, the same path as the
`.affine` scripts):

[cols="1,2"]
|===
| Account › Cloudflare Pages › *Edit* | create/deploy the Pages project, attach domains
| Zone › DNS › *Edit* (zone `boj-server.net`)| read/write DNS records
| Zone › Zone › *Read* | resolve the zone id
|===

Plus the *Account ID* and the `boj-server.net` *Zone ID* (Dashboard -> domain ->
*Overview*, right sidebar). Export as `CLOUDFLARE_API_TOKEN` / `CLOUDFLARE_ACCOUNT_ID`.
*Note:* for go-live path A none of this is required — attaching the custom domain in
the dashboard auto-creates the DNS records.

== Verify

[source,bash]
----
curl -sSI https://boj-server.net | grep -iE 'strict-transport|content-security|x-content-type'
curl -sS https://boj-server.net/catalog.json | jq '.cartridges | length' # -> 139
curl -sSI https://boj-server.net/.well-known/security.txt
----

== Regenerate the catalogue snapshot

`site/catalog.json` is a committed snapshot. When the registry changes:

[source,bash]
----
# from the boj-server repo root, with boj-server-cartridges checked out alongside
tools/site-catalog/build-catalog.sh ../boj-server-cartridges site/catalog.json
----

It groups by the registry's directory taxonomy (reliable) rather than the free-text
`domain` field, excludes `templates/`, and honours an explicit `available: false`.

== Notes

* Development happens on branch `claude/awesome-davinci-8afqgy`; open a PR into `main`.
* `_headers` CSP is strict (`default-src 'self'`) — all script/style/data is
first-party, so it needs no `'unsafe-inline'`. Keep new assets first-party or widen
the policy deliberately.
85 changes: 85 additions & 0 deletions scripts/cloudflare/CreatePagesProject.affine
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
// SPDX-License-Identifier: MPL-2.0
// Copyright (c) 2026 Jonathan D.A. Jewell <j.d.a.jewell@open.ac.uk>
// Create the Cloudflare Pages project for boj-server and attach its custom domains.
// Mirrors the canonical estate pattern in standards/avow-protocol/scripts/CreatePagesProjects.affine
// (single-project variant). Requires CLOUDFLARE_API_TOKEN + CLOUDFLARE_ACCOUNT_ID in the env.
// Only needed for the API-driven path; the documented go-live route is the dashboard Git-connect.

module CreatePagesProject;

use Deno_Api;
use Fetch_Api;

extern fn console_log(msg: String) -> Unit = "console" "log";
extern fn console_error(msg: String) -> Unit = "console" "error";
extern fn str_repeat(s: String, count: Int) -> String = "string" "repeat";
extern fn json_stringify(value: a) -> Option<String> = "JSON" "stringifyAny";
extern fn json_get_bool(j: Json, key: String) -> Bool = "json" "getBool";

let cloudflare_api_token = Deno_Api.Env.get("CLOUDFLARE_API_TOKEN");
let cloudflare_account_id = Deno_Api.Env.get("CLOUDFLARE_ACCOUNT_ID");

let project_name = "boj-server";
let domains = ["boj-server.net", "www.boj-server.net"];

fn auth_headers() -> Fetch_Api.Headers {
let token = match cloudflare_api_token { Some(t) => t, None => "" };
Fetch_Api.Headers { authorization: "Bearer " ++ token, content_type: "application/json" }
}

pub fn main() -> Effect[Async] Unit {
match (cloudflare_api_token, cloudflare_account_id) {
(None, _) => { console_error("Missing credentials"); Deno_Api.exit(1); }
(_, None) => { console_error("Missing credentials"); Deno_Api.exit(1); }
_ => {}
}

let account_id = match cloudflare_account_id { Some(a) => a, None => "" };
let bar = str_repeat("=", 70);

console_log("Creating Cloudflare Pages project: " ++ project_name);
console_log(bar);

let create_body = match json_stringify(json_object([
("name", json_string(project_name)),
("production_branch", json_string("main")),
])) { Some(s) => s, None => "" };

let create_response = await Fetch_Api.fetch(
"https://api.cloudflare.com/client/v4/accounts/" ++ account_id ++ "/pages/projects",
Fetch_Api.RequestInit { method: Some("POST"), headers: auth_headers(), body: Some(create_body) },
);
let create_result = await Fetch_Api.json(create_response);
if json_get_bool(create_result, "success") {
console_log(" Project created: https://" ++ project_name ++ ".pages.dev");
} else {
console_log(" Project already exists or failed");
}

let i = 0;
while i < len(domains) {
let domain = domains[i];
console_log(" Adding domain: " ++ domain);
let domain_body = match json_stringify(json_object([("name", json_string(domain))])) {
Some(s) => s, None => "",
};
let domain_response = await Fetch_Api.fetch(
"https://api.cloudflare.com/client/v4/accounts/" ++ account_id
++ "/pages/projects/" ++ project_name ++ "/domains",
Fetch_Api.RequestInit { method: Some("POST"), headers: auth_headers(), body: Some(domain_body) },
);
let domain_result = await Fetch_Api.json(domain_response);
if json_get_bool(domain_result, "success") {
console_log(" Domain added: " ++ domain);
} else {
console_log(" Domain already added or failed: " ++ domain);
}
i = i + 1;
}

console_log("\n" ++ bar);
console_log("Done. Connect the repo in the dashboard (Git integration) or run DeployDirect.affine.");
console_log(bar)
}

main()
71 changes: 71 additions & 0 deletions scripts/cloudflare/DeployDirect.affine
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
// SPDX-License-Identifier: MPL-2.0
// Copyright (c) 2026 Jonathan D.A. Jewell <j.d.a.jewell@open.ac.uk>
// Direct file-upload deployment of the boj-server.net static bundle to Cloudflare Pages.
// Mirrors standards/avow-protocol/scripts/DeployDirect.affine. The static bundle lives in `site/`
// and needs no build step. Requires CLOUDFLARE_API_TOKEN + CLOUDFLARE_ACCOUNT_ID in the env.
// The documented go-live route is the dashboard Git-connect; this is the API alternative.

module DeployDirect;

use Deno_Api;
use Fetch_Api;

extern fn console_log(msg: String) -> Unit = "console" "log";
extern fn console_error(msg: String) -> Unit = "console" "error";
extern fn str_repeat(s: String, count: Int) -> String = "string" "repeat";
extern fn json_stringify(value: a) -> Option<String> = "JSON" "stringifyAny";
extern fn json_get_bool(j: Json, key: String) -> Bool = "json" "getBool";
extern fn json_get_path(j: Json, a: String, b: String) -> String = "json" "getPath2";

let cloudflare_api_token = Deno_Api.Env.get("CLOUDFLARE_API_TOKEN");
let cloudflare_account_id = Deno_Api.Env.get("CLOUDFLARE_ACCOUNT_ID");
let project_name = "boj-server";
let output_dir = "site";

fn auth_headers() -> Fetch_Api.Headers {
let token = match cloudflare_api_token { Some(t) => t, None => "" };
Fetch_Api.Headers { authorization: "Bearer " ++ token, content_type: "application/json" }
}

pub fn main() -> Effect[Async] Unit {
match (cloudflare_api_token, cloudflare_account_id) {
(None, _) => { console_error("Missing credentials"); Deno_Api.exit(1); }
(_, None) => { console_error("Missing credentials"); Deno_Api.exit(1); }
_ => {}
}

let account_id = match cloudflare_account_id { Some(a) => a, None => "" };
let bar = str_repeat("=", 50);

console_log("Direct deployment of " ++ output_dir ++ "/ to Cloudflare Pages");
console_log(bar);

// The site is static (no build). Upload the contents of `site/` as a new deployment.
// wrangler is the canonical uploader: `wrangler pages deploy site --project-name=boj-server`.
// This API path posts a deployment to the project; assets are taken from output_dir.
console_log("\nCreating deployment for project " ++ project_name ++ " from " ++ output_dir ++ "/...");

let body = match json_stringify(json_object([
("branch", json_string("main")),
])) { Some(s) => s, None => "" };

let deploy_response = await Fetch_Api.fetch(
"https://api.cloudflare.com/client/v4/accounts/" ++ account_id
++ "/pages/projects/" ++ project_name ++ "/deployments",
Fetch_Api.RequestInit { method: Some("POST"), headers: auth_headers(), body: Some(body) },
);

let result = await Fetch_Api.json(deploy_response);
if json_get_bool(result, "success") {
console_log("\n" ++ bar);
console_log("DEPLOYMENT SUCCESSFUL");
console_log(" " ++ json_get_path(result, "result", "url"));
console_log(" https://" ++ project_name ++ ".pages.dev");
console_log(bar);
} else {
console_error("Deployment failed");
Deno_Api.exit(1);
}
}

main()
10 changes: 10 additions & 0 deletions site/.well-known/security.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# SPDX-License-Identifier: MPL-2.0
# RFC 9116 - security.txt
# https://securitytxt.org/

Contact: mailto:j.d.a.jewell@open.ac.uk
Expires: 2026-12-31T23:59:59.000Z
Preferred-Languages: en
Canonical: https://github.com/hyperpolymath/boj-server/.well-known/security.txt
Policy: https://github.com/hyperpolymath/boj-server/blob/main/SECURITY.md
Hiring: https://github.com/hyperpolymath/boj-server/careers
1 change: 1 addition & 0 deletions site/CNAME
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
boj-server.net
17 changes: 17 additions & 0 deletions site/README.adoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// SPDX-License-Identifier: CC-BY-SA-4.0
// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell (hyperpolymath) <j.d.a.jewell@open.ac.uk>
= boj-server.net — static site

The permanent presence + access point for the BoJ MCP server and cartridge system.
A no-build static bundle served by Cloudflare Pages from this directory.

* `index.html` — hub: install configurator (base + NeSy / Agentic / Coordination
bundles) and a browsable catalogue of all 139 cartridges.
* `assets/` — `style.css`, `app.js` (vanilla, zero-dependency), `favicon.svg`.
* `catalog.json` — registry snapshot read by `app.js`. Regenerate with
`tools/site-catalog/build-catalog.sh`.
* `_headers`, `_redirects`, `CNAME`, `robots.txt`, `sitemap.xml`, `.well-known/`.

Deployment, the catalogue-regeneration command, and the API-token scopes needed for
agent-driven DNS/Pages changes are documented in
`docs/website/CLOUDFLARE-SETUP.adoc`.
Loading
Loading