mirror of
https://github.com/profullstack/logicsrc.git
synced 2026-10-02 12:54:03 +00:00
Merge remote-tracking branch 'origin/master' into openprofile-broadcast-guest
# Conflicts: # apps/logicsrc-web/src/app/llms.txt/route.ts
This commit is contained in:
commit
a36fe5da52
8 changed files with 882 additions and 0 deletions
|
|
@ -32,6 +32,8 @@ export function GET(): Response {
|
|||
- [OpenDisk](${SITE_URL}/opendisk): One file a machine serves about the disk it will rent: free GiB, price per GiB-month, location, policy, proof cadence and hub standing, discovered at /.well-known/opendisk.json. What a peer-to-peer storage market is made of; reference marketplace d1sks.com.
|
||||
- [OpenCoupon](${SITE_URL}/opencoupon): One file a merchant serves about what is on offer right now, at /.well-known/opencoupon.json: every code, sale and shipping threshold with kind, value, scope, dates, status and regions, expired codes kept so directories learn they died. A coupon site reads the merchant instead of a forum thread.
|
||||
- [OpenRecipe.md](${SITE_URL}/openrecipe): One Markdown file that is a recipe: summary block (Serves, Prep, Cook, Cuisine, Diet, Author, Source), ingredients and steps as written, notes, nutrition; served next to the page or linked with rel="openrecipe"; schema.org/Recipe JSON-LD is derived from it, never the reverse.
|
||||
- [OpenAffiliate](${SITE_URL}/openaffiliate): One file a merchant serves about the commission it pays, at /.well-known/openaffiliate.json: programs with what pays (sale, subscription, signup, lead, install), percent or amount, attribution window, hold days and payout methods; four calls let a person or an agent join with an OpenProfile.md, link with ?oa=code, read its own ledger and get paid to its own address. No network in the money; reference implementation crawlproof.com/affiliate.
|
||||
- [OpenThreat](${SITE_URL}/openthreat): One file a security tool serves about what it found in the open, at /.well-known/openthreat.json: findings in public repositories, attacks on the reporter's own infrastructure, indicators and advisories, with severity, rule, subject and status. Private subjects are never in it, secrets are never located while open, announcing is on by default with a one-switch opt-out. First reporter threatcrush.com/discovery, first directory nichedb.dev/c/threats.
|
||||
- [OpenBroadcast](${SITE_URL}/openbroadcast): The Broadcast section of an OpenProfile.md: the show a person hosts (podcast, radio, live audio, stream) with kind, format, cadence, audience, topics, slots, whether it pays or charges guests, and who the host is seeking, so a host and a guest are matched from two files rather than two forms.
|
||||
- [OpenGuest](${SITE_URL}/openguest): The Guest section of an OpenProfile.md: that a person will appear on shows, with expertise, credentials, pitch, formats, availability, rate, past appearances and dealbreakers. An expert is a guest with Expertise and Credentials. Matched against OpenBroadcast.
|
||||
- [AgentSwarm](${SITE_URL}/agent-swarm): Provider-neutral agent orchestration, model routing, and cost controls.
|
||||
|
|
|
|||
233
apps/logicsrc-web/src/app/openaffiliate/page.tsx
Normal file
233
apps/logicsrc-web/src/app/openaffiliate/page.tsx
Normal file
|
|
@ -0,0 +1,233 @@
|
|||
import Link from "next/link";
|
||||
import type { ReactNode } from "react";
|
||||
import type { Metadata } from "next";
|
||||
import { SiteShell } from "@/components/site-shell";
|
||||
import { mono, pre, table, td, th } from "../openontology/ui";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "OpenAffiliate · LogicSRC",
|
||||
description:
|
||||
"OpenAffiliate is one file a merchant serves about the commission it pays, at /.well-known/openaffiliate.json, and four calls that let a person or an agent earn it: join with a profile, link with one parameter, read your own ledger, get paid to your own address. No network in the money.",
|
||||
alternates: { canonical: "/openaffiliate" }
|
||||
};
|
||||
|
||||
const DESCRIPTOR = `{
|
||||
"merchant": { "name": "CrawlProof", "web": "https://crawlproof.com", "currency": "USD",
|
||||
"terms": "https://crawlproof.com/affiliate/terms" },
|
||||
"updated": "2026-09-13T06:00:00Z",
|
||||
"programs": [
|
||||
{ "id": "partners", "title": "CrawlProof partner program",
|
||||
"join": "https://crawlproof.com/api/affiliate/v1/join",
|
||||
"ledger": "https://crawlproof.com/api/affiliate/v1/ledger",
|
||||
"approval": "open",
|
||||
"pays": [
|
||||
{ "event": "sale", "kind": "percent", "value": 30 },
|
||||
{ "event": "subscription", "kind": "percent", "value": 30, "months": 12 },
|
||||
{ "event": "signup", "kind": "amount", "value": 0.5 }
|
||||
],
|
||||
"link": { "param": "oa", "deep": true },
|
||||
"window": 30, "attribution": "last", "hold_days": 30,
|
||||
"payout": { "methods": ["usdc/eip155:137"], "min": 10, "schedule": "weekly" },
|
||||
"self": "refused", "status": "active" }
|
||||
]
|
||||
}`;
|
||||
|
||||
const JOIN = `POST https://crawlproof.com/api/affiliate/v1/join
|
||||
{ "profile": "https://anthony.example/.well-known/openprofile.md" }
|
||||
|
||||
201 { "membership": "am_8f3c", "status": "active", "code": "anthony",
|
||||
"link": "https://crawlproof.com/?oa=anthony", "token": "oa_5Kq…",
|
||||
"ledger": "https://crawlproof.com/api/affiliate/v1/ledger" }`;
|
||||
|
||||
const EVENTS: Array<[string, string, string]> = [
|
||||
["sale", "a one-time charge", "percent of amount, or a flat amount"],
|
||||
["subscription", "each charge of a recurring one", "months caps how many renewals pay; absent is every one"],
|
||||
["signup", "an account created", "usually a flat amount"],
|
||||
["lead", "a form submitted", "a flat amount"],
|
||||
["install", "an app installed", "a flat amount"],
|
||||
["other", "the merchant's own words", "kept, listed, not filtered on"]
|
||||
];
|
||||
|
||||
const CALLS: Array<[string, string]> = [
|
||||
["Join", "POST the program's join URL with an OpenProfile.md URL. Back comes a code, a link, a token and the terms as they stood. open answers active at once; review answers pending."],
|
||||
["Link", "The merchant's URL with ?oa=code. No redirect host, no shortener, no pixel. deep: true means it works on any page, so you link to the product you recommend."],
|
||||
["Ledger", "GET the ledger with the token: clicks, every conversion with its status and hold, balances pending, approved and paid, every payout with its tx. A reversal must carry a reason."],
|
||||
["Payout", "The whole approved balance to your own pay address on the schedule, once it passes min. Nothing netted, because there is no network to pay."]
|
||||
];
|
||||
|
||||
const NETWORK: Array<[string, string]> = [
|
||||
["A third of the commission", "Nothing. The merchant pays the affiliate. A directory that charges does so as a stated fee, never as a share of a conversion."],
|
||||
["Apply, wait weeks", "A profile. approval: open answers at once; review says the merchant looks first, and says so in the file."],
|
||||
["Terms in a PDF you signed once", "Terms in a file at a fixed URL, fetched any time, kept with the membership as they stood at the join, changed forward only."],
|
||||
["A reversal with no reason", "A reversal without a reason is not a reversal. The affiliate reports it and a directory says so beside the program."],
|
||||
["Ten dashboards", "One ledger shape per program, readable by anyone with the token, so one page can show all of them."],
|
||||
["Sixty days, minus a fee, in their currency", "hold_days, then the schedule, to your own address, in the asset the file names. The tx is in the ledger."]
|
||||
];
|
||||
|
||||
const ABSENT: Array<[string, string]> = [
|
||||
["No network", "The merchant serves the terms, records the conversions and sends the money. Nobody sits in the middle of a payment."],
|
||||
["No tracking host", "A link is the merchant's URL with a parameter. Only a navigation sets attribution; an image, frame, script or prefetch sets nothing."],
|
||||
["No application form", "An affiliate is a profile. A person or an agent, with an operator who answers for the agent."],
|
||||
["No exclusivity", "A membership binds nobody to one program, and a program may not require it."],
|
||||
["No impression payments", "A view is not an event. The events are things a customer did."]
|
||||
];
|
||||
|
||||
export default function OpenAffiliatePage(): ReactNode {
|
||||
return (
|
||||
<SiteShell active="OpenAffiliate">
|
||||
<div className="band">
|
||||
<div className="section-head">
|
||||
<p className="eyebrow">LogicSRC standards surface</p>
|
||||
<h2>OpenAffiliate</h2>
|
||||
<p>
|
||||
One file a merchant serves about the commission it pays, and four calls that let a
|
||||
person or an agent earn it. No network in the money.
|
||||
</p>
|
||||
</div>
|
||||
<p style={{ color: "#41505d" }}>
|
||||
Affiliate marketing runs through networks, and the networks are the problem. A merchant
|
||||
pays a third of every commission for a pixel and a payout file. An affiliate applies to
|
||||
each program by hand, waits weeks, and ends up with ten dashboards that disagree. Terms
|
||||
live in a PDF signed once. A conversion is reversed with no reason. A payout arrives sixty
|
||||
days later, minus a fee. And none of it is readable by a machine, so an agent that could
|
||||
earn by recommending the right product cannot find out what the commission is. The
|
||||
merchant already knows what it pays and what each sale was worth. OpenAffiliate is that,
|
||||
written down, at <code style={mono}>/.well-known/openaffiliate.json</code>.
|
||||
</p>
|
||||
<p style={{ color: "#5b6b7a" }}>
|
||||
Status: 0.1. The reference implementation is{" "}
|
||||
<a href="https://crawlproof.com/affiliate">crawlproof.com</a>, which runs its own
|
||||
program, joins other merchants' programs from the same dashboard, and pays in USDC on
|
||||
Polygon. The smallest valid file is a merchant with a name and a program with a title
|
||||
and one thing it pays.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="band">
|
||||
<div className="section-head">
|
||||
<h2>The descriptor</h2>
|
||||
<p>
|
||||
A commission fetched from the merchant's own origin is the commission the merchant
|
||||
says it pays, today, in words it cannot say it never agreed to.
|
||||
</p>
|
||||
</div>
|
||||
<pre style={pre}>{DESCRIPTOR}</pre>
|
||||
<p style={{ color: "#41505d" }}>
|
||||
<code style={mono}>pays</code> is one entry per event. <code style={mono}>window</code>{" "}
|
||||
is the attribution window in days and <code style={mono}>attribution</code> whether a
|
||||
later click replaces an earlier one. <code style={mono}>hold_days</code> is the refund
|
||||
window a conversion waits out as pending. <code style={mono}>payout.methods</code> are{" "}
|
||||
<code style={mono}>asset/chain</code> pairs or a named rail. <code style={mono}>self</code>{" "}
|
||||
says whether the affiliate's own purchase pays. Unknown keys are kept.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="band">
|
||||
<div className="section-head">
|
||||
<h2>Four calls</h2>
|
||||
</div>
|
||||
<table style={table}>
|
||||
<tbody>
|
||||
{CALLS.map(([what, how]) => (
|
||||
<tr key={what}>
|
||||
<td style={td}>
|
||||
<strong>{what}</strong>
|
||||
</td>
|
||||
<td style={td}>{how}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
<pre style={pre}>{JOIN}</pre>
|
||||
</div>
|
||||
|
||||
<div className="band">
|
||||
<div className="section-head">
|
||||
<h2>Six events</h2>
|
||||
</div>
|
||||
<table style={table}>
|
||||
<thead>
|
||||
<tr>
|
||||
<th style={th}>event</th>
|
||||
<th style={th}>what happened</th>
|
||||
<th style={th}>how it pays</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{EVENTS.map(([event, what, how]) => (
|
||||
<tr key={event}>
|
||||
<td style={td}>
|
||||
<code style={mono}>{event}</code>
|
||||
</td>
|
||||
<td style={td}>{what}</td>
|
||||
<td style={td}>{how}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div className="band">
|
||||
<div className="section-head">
|
||||
<h2>What a network costs, and what replaces it</h2>
|
||||
</div>
|
||||
<table style={table}>
|
||||
<thead>
|
||||
<tr>
|
||||
<th style={th}>on a network</th>
|
||||
<th style={th}>with OpenAffiliate</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{NETWORK.map(([before, after]) => (
|
||||
<tr key={before}>
|
||||
<td style={td}>{before}</td>
|
||||
<td style={td}>{after}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div className="band">
|
||||
<div className="section-head">
|
||||
<h2>What is deliberately absent</h2>
|
||||
</div>
|
||||
<table style={table}>
|
||||
<tbody>
|
||||
{ABSENT.map(([what, why]) => (
|
||||
<tr key={what}>
|
||||
<td style={td}>
|
||||
<strong>{what}</strong>
|
||||
</td>
|
||||
<td style={td}>{why}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div className="band">
|
||||
<div className="section-head">
|
||||
<h2>Where everything lives</h2>
|
||||
</div>
|
||||
<ul style={{ color: "#41505d", lineHeight: 1.9, paddingLeft: "1.1rem" }}>
|
||||
<li>
|
||||
<Link href="/docs/openaffiliate">Specification</Link>: the descriptor, eleven rules,
|
||||
six events, join, links and attribution, the ledger, webhooks, payouts, discovery,
|
||||
what a directory owes a merchant
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://crawlproof.com/affiliate">crawlproof.com/affiliate</a>: the reference
|
||||
implementation, running its own program and a directory of others
|
||||
</li>
|
||||
<li>
|
||||
<Link href="/openprofile">OpenProfile.md</Link>, the affiliate's identity and pay
|
||||
address; <Link href="/opencoupon">OpenCoupon</Link>, a merchant's promotions in
|
||||
the same shape
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</SiteShell>
|
||||
);
|
||||
}
|
||||
196
apps/logicsrc-web/src/app/openthreat/page.tsx
Normal file
196
apps/logicsrc-web/src/app/openthreat/page.tsx
Normal file
|
|
@ -0,0 +1,196 @@
|
|||
import Link from "next/link";
|
||||
import type { ReactNode } from "react";
|
||||
import type { Metadata } from "next";
|
||||
import { SiteShell } from "@/components/site-shell";
|
||||
import { mono, pre, table, td, th } from "../openontology/ui";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "OpenThreat · LogicSRC",
|
||||
description:
|
||||
"OpenThreat is one file a security tool serves about what it found in the open, at /.well-known/openthreat.json: findings in public repositories, attacks on the reporter's own infrastructure, indicators and advisories, with severity, rule, subject and status. Private subjects are never in it and secrets are never located while open.",
|
||||
alternates: { canonical: "/openthreat" }
|
||||
};
|
||||
|
||||
const DESCRIPTOR = `{
|
||||
"openthreat": "0.1",
|
||||
"reporter": { "name": "ThreatCrush", "web": "https://threatcrush.com", "tool": "threatcrush",
|
||||
"policy": "https://threatcrush.com/discovery#policy" },
|
||||
"updated": "2026-09-13T06:00:00Z",
|
||||
"threats": [
|
||||
{ "id": "3f9a1c2b", "kind": "finding", "title": "SQL assembled by concatenation",
|
||||
"severity": "high", "rule": "js-sql-string-building", "cwe": "CWE-89", "category": "code",
|
||||
"subject": { "name": "northwind/api", "url": "https://github.com/northwind/api", "ref": "main" },
|
||||
"location": { "file": "src/db/users.ts", "line": 42 },
|
||||
"status": "open", "last_seen": "2026-09-13T05:40:00Z" },
|
||||
{ "id": "b71e0d44", "kind": "finding", "title": "Hardcoded credential",
|
||||
"severity": "critical", "rule": "secret-generic-credential", "cwe": "CWE-798", "category": "secret",
|
||||
"subject": { "name": "northwind/api", "url": "https://github.com/northwind/api" },
|
||||
"status": "open" },
|
||||
{ "id": "ssh-91.232.105.3", "kind": "attack", "title": "SSH brute force", "severity": "medium",
|
||||
"source": { "ip": "91.232.105.3", "country": "RU" }, "target": { "port": 22, "service": "ssh" },
|
||||
"indicators": [{ "type": "ip", "value": "91.232.105.3" }],
|
||||
"status": "blocked", "count": 47, "last_seen": "2026-09-13T04:52:00Z" }
|
||||
]
|
||||
}`;
|
||||
|
||||
const KINDS: Array<[string, string]> = [
|
||||
["finding", "Something in a public subject's code or configuration: a rule, a CWE, a location."],
|
||||
["attack", "Traffic observed against the reporter's own infrastructure: source, target, count."],
|
||||
["indicator", "A value worth blocking or watching on its own: ip, cidr, domain, url, hash, ua."],
|
||||
["advisory", "A statement about a vulnerability, with the document in refs."]
|
||||
];
|
||||
|
||||
const HARD: Array<[string, string]> = [
|
||||
["A subject is public or it is not in the file", "A private repository, a customer's server, a paying user's scan: none of it is a threat in the open, it is someone's private security posture. A reporter that scans private things keeps two tables and serves one."],
|
||||
["A secret is never located while it is open", "A secret finding is published with rule, severity, subject and status only. No location, no message, no excerpt. The credential is already exposed; the file must not be the map to it."]
|
||||
];
|
||||
|
||||
const ABSENT: Array<[string, string]> = [
|
||||
["No private subjects", "Stated in the rules and worth stating twice."],
|
||||
["No exploit detail", "message says what was found; consequence what it means. How to use it is nobody's business here."],
|
||||
["No scoring across reporters", "severity is the reporter's. A directory that normalises labels the result as its own."],
|
||||
["No push", "A reporter serves a file. A directory watches updated."]
|
||||
];
|
||||
|
||||
export default function OpenThreatPage(): ReactNode {
|
||||
return (
|
||||
<SiteShell active="OpenThreat">
|
||||
<div className="band">
|
||||
<div className="section-head">
|
||||
<p className="eyebrow">LogicSRC standards surface</p>
|
||||
<h2>OpenThreat</h2>
|
||||
<p>
|
||||
One file a security tool serves about what it found in the open. A directory reads the
|
||||
reporter instead of a vendor feed, and the reporter decides what it discloses.
|
||||
</p>
|
||||
</div>
|
||||
<p style={{ color: "#41505d" }}>
|
||||
Every security tool finds things, and every one keeps what it found behind its own login.
|
||||
A scanner that runs on a thousand public repositories knows which rules fire and where,
|
||||
and says nothing, because saying it would mean a feed, a schema, an API key and a sales
|
||||
call. The threat feeds that exist are products with terms that forbid redistribution.
|
||||
OpenThreat is the small file a tool can serve in an afternoon at{" "}
|
||||
<code style={mono}>/.well-known/openthreat.json</code>, with a rule for what may go in
|
||||
it.
|
||||
</p>
|
||||
<p style={{ color: "#5b6b7a" }}>
|
||||
Status: 0.1. The first reporter is{" "}
|
||||
<a href="https://threatcrush.com/discovery">threatcrush.com/discovery</a>, built from the
|
||||
scans its GitHub App ran on public repositories; the first directory is{" "}
|
||||
<a href="https://nichedb.dev/c/threats">nichedb.dev/c/threats</a>.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="band">
|
||||
<div className="section-head">
|
||||
<h2>The descriptor</h2>
|
||||
<p>
|
||||
Only <code style={mono}>reporter.name</code> and a threat's{" "}
|
||||
<code style={mono}>title</code> are required. Everything at{" "}
|
||||
<code style={mono}>/.well-known/</code> is TLP:CLEAR by definition.
|
||||
</p>
|
||||
</div>
|
||||
<pre style={pre}>{DESCRIPTOR}</pre>
|
||||
<p style={{ color: "#41505d" }}>
|
||||
<code style={mono}>rule</code> is the same string a SARIF ruleId carries;{" "}
|
||||
<code style={mono}>subject</code> is what the threat is about and is public by
|
||||
definition; <code style={mono}>status</code> is open, fixed, mitigated, blocked or
|
||||
withdrawn, and a withdrawn threat stays in the file a while so directories retract it.
|
||||
The second threat above is a secret: no location, no message, by rule.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="band">
|
||||
<div className="section-head">
|
||||
<h2>Four kinds</h2>
|
||||
</div>
|
||||
<table style={table}>
|
||||
<thead>
|
||||
<tr>
|
||||
<th style={th}>kind</th>
|
||||
<th style={th}>what it is</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{KINDS.map(([kind, what]) => (
|
||||
<tr key={kind}>
|
||||
<td style={td}>
|
||||
<code style={mono}>{kind}</code>
|
||||
</td>
|
||||
<td style={td}>{what}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div className="band">
|
||||
<div className="section-head">
|
||||
<h2>Two rules that do not degrade</h2>
|
||||
<p>Every other rule degrades. These two are the reason the file can exist at all.</p>
|
||||
</div>
|
||||
<table style={table}>
|
||||
<tbody>
|
||||
{HARD.map(([what, why]) => (
|
||||
<tr key={what}>
|
||||
<td style={td}>
|
||||
<strong>{what}</strong>
|
||||
</td>
|
||||
<td style={td}>{why}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
<p style={{ color: "#41505d" }}>
|
||||
A subject that was scanned did not ask to be listed. Announcing is on by default, because
|
||||
a finding in a public repository is public already, and opting out is one switch in the
|
||||
tool's own settings. A subject that opts out leaves the file on the next build, and
|
||||
is served once more as <code style={mono}>withdrawn</code> so directories retract it.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="band">
|
||||
<div className="section-head">
|
||||
<h2>What is deliberately absent</h2>
|
||||
</div>
|
||||
<table style={table}>
|
||||
<tbody>
|
||||
{ABSENT.map(([what, why]) => (
|
||||
<tr key={what}>
|
||||
<td style={td}>
|
||||
<strong>{what}</strong>
|
||||
</td>
|
||||
<td style={td}>{why}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div className="band">
|
||||
<div className="section-head">
|
||||
<h2>Where everything lives</h2>
|
||||
</div>
|
||||
<ul style={{ color: "#41505d", lineHeight: 1.9, paddingLeft: "1.1rem" }}>
|
||||
<li>
|
||||
<Link href="/docs/openthreat">Specification</Link>: the descriptor, twelve rules and the
|
||||
two that do not degrade, announcing and opting out, discovery, SARIF, STIX and CSAF
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://threatcrush.com/discovery">threatcrush.com/discovery</a>: the first
|
||||
reporter, and its policy page
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://nichedb.dev/c/threats">nichedb.dev/c/threats</a>: the first directory,
|
||||
with RSS, JSON, API and MCP over the same rows
|
||||
</li>
|
||||
<li>
|
||||
<Link href="/openprofile">OpenProfile.md</Link>, the operator behind a reporter;{" "}
|
||||
<Link href="/openserver">OpenServer</Link> and <Link href="/opencoupon">OpenCoupon</Link>
|
||||
, the same serve-your-own-file shape for other niches
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</SiteShell>
|
||||
);
|
||||
}
|
||||
|
|
@ -28,6 +28,7 @@ const STATIC_ROUTES: Array<{
|
|||
{ path: "/openmcp", changeFrequency: "weekly", priority: 0.9 },
|
||||
{ path: "/openaccess", changeFrequency: "weekly", priority: 0.9 },
|
||||
{ path: "/openserver", changeFrequency: "weekly", priority: 0.9 },
|
||||
{ path: "/openthreat", changeFrequency: "weekly", priority: 0.9 },
|
||||
{ path: "/opencpu", changeFrequency: "weekly", priority: 0.9 },
|
||||
{ path: "/openmemory", changeFrequency: "weekly", priority: 0.9 },
|
||||
{ path: "/opengpu", changeFrequency: "weekly", priority: 0.9 },
|
||||
|
|
@ -36,6 +37,7 @@ const STATIC_ROUTES: Array<{
|
|||
{ path: "/opendisk", changeFrequency: "weekly", priority: 0.9 },
|
||||
{ path: "/opencoupon", changeFrequency: "weekly", priority: 0.9 },
|
||||
{ path: "/openrecipe", changeFrequency: "weekly", priority: 0.9 },
|
||||
{ path: "/openaffiliate", changeFrequency: "weekly", priority: 0.9 },
|
||||
{ path: "/openontology/explore", changeFrequency: "daily", priority: 0.7 },
|
||||
{ path: "/openspec", changeFrequency: "weekly", priority: 0.8 },
|
||||
{ path: "/agent-swarm", changeFrequency: "weekly", priority: 0.8 },
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ const NAV: Array<{ href: string; label: string; external?: boolean }> = [
|
|||
{ href: "/openmcp", label: "OpenMCP" },
|
||||
{ href: "/openaccess", label: "OpenAccess" },
|
||||
{ href: "/openserver", label: "OpenServer" },
|
||||
{ href: "/openthreat", label: "OpenThreat" },
|
||||
{ href: "/opencpu", label: "OpenCPU" },
|
||||
{ href: "/openmemory", label: "OpenMemory" },
|
||||
{ href: "/opengpu", label: "OpenGPU" },
|
||||
|
|
@ -28,6 +29,7 @@ const NAV: Array<{ href: string; label: string; external?: boolean }> = [
|
|||
{ href: "/opendisk", label: "OpenDisk" },
|
||||
{ href: "/opencoupon", label: "OpenCoupon" },
|
||||
{ href: "/openrecipe", label: "OpenRecipe.md" },
|
||||
{ href: "/openaffiliate", label: "OpenAffiliate" },
|
||||
{ href: "/#cli", label: "CLI" },
|
||||
{ href: "/docs", label: "Docs" },
|
||||
{ href: "/blog", label: "Blog" },
|
||||
|
|
|
|||
|
|
@ -23,10 +23,12 @@ export const DOC_SLUGS = [
|
|||
"openmcp",
|
||||
"openaccess",
|
||||
"openserver",
|
||||
"openthreat",
|
||||
"openfile",
|
||||
"opendisk",
|
||||
"opencoupon",
|
||||
"openrecipe",
|
||||
"openaffiliate",
|
||||
"openstream",
|
||||
"opencpu",
|
||||
"openmemory",
|
||||
|
|
|
|||
270
docs/openaffiliate.md
Normal file
270
docs/openaffiliate.md
Normal file
|
|
@ -0,0 +1,270 @@
|
|||
# OpenAffiliate
|
||||
|
||||
OpenAffiliate is one file a merchant serves about the commission it pays, and the four calls that let anyone earn it: join, link, read the ledger, get paid. The merchant publishes its terms in its own words at a fixed URL, an affiliate joins with a profile instead of an application form, every link is a plain URL with one parameter, every conversion is a row the affiliate can read, and the money goes from the merchant to the affiliate with nobody in between. It is maintained by Profullstack, Inc. as part of the LogicSRC open-standards surface.
|
||||
|
||||
Status: **0.1**. A description of a program a merchant already runs, published so any merchant can run one and any person or agent can join it without a network.
|
||||
|
||||
Slug: `openaffiliate`
|
||||
|
||||
## The problem
|
||||
|
||||
Affiliate marketing runs through networks, and the networks are the problem. A merchant pays the network a fee on every commission, often a third of it, for a tracking pixel and a payout file. An affiliate applies to each program by hand, waits weeks for a decision, and ends up with ten dashboards that disagree with each other. The terms live in a PDF the affiliate agreed to once and cannot fetch again. A conversion is reversed with no reason. A payout arrives sixty days later, minus a fee, in a currency the affiliate did not ask for. And none of it is readable by a machine, so an agent that could earn a commission by recommending the right product has no way to find out what the commission is.
|
||||
|
||||
The merchant already knows what it pays, who it pays, and what each sale was worth. Its checkout is the source of truth. What is missing is the file that says the terms, and the four calls that let an affiliate act on them.
|
||||
|
||||
## Terms
|
||||
|
||||
- A **merchant** is anyone who pays a commission on something it sells: a store, a service, a subscription, a marketplace seller. Its **descriptor** is the file it serves.
|
||||
- A **program** is one set of terms: what pays, how much, for how long, and how the money arrives. A merchant may run several.
|
||||
- An **affiliate** is a person or an agent who joins a program and sends customers. Its identity is an [OpenProfile.md](/openprofile) URL.
|
||||
- A **membership** is one affiliate in one program: a code, a link, a token and a ledger.
|
||||
- A **conversion** is one event the program pays for, attributed to one affiliate.
|
||||
- A **directory** is anything that reads descriptors and lists programs across merchants. It never touches the money.
|
||||
- A **reader** is anything that reads a descriptor.
|
||||
|
||||
## The descriptor
|
||||
|
||||
A merchant serves a JSON document at `/.well-known/openaffiliate.json` on its own origin.
|
||||
|
||||
```json
|
||||
{
|
||||
"merchant": {
|
||||
"name": "CrawlProof",
|
||||
"web": "https://crawlproof.com",
|
||||
"operator": "https://crawlproof.com/.well-known/openprofile.md",
|
||||
"currency": "USD",
|
||||
"terms": "https://crawlproof.com/affiliate/terms",
|
||||
"jwks": "https://crawlproof.com/.well-known/openaffiliate-jwks.json"
|
||||
},
|
||||
"updated": "2026-09-13T06:00:00Z",
|
||||
"programs": [
|
||||
{
|
||||
"id": "partners",
|
||||
"title": "CrawlProof partner program",
|
||||
"url": "https://crawlproof.com/affiliate",
|
||||
"join": "https://crawlproof.com/api/affiliate/v1/join",
|
||||
"ledger": "https://crawlproof.com/api/affiliate/v1/ledger",
|
||||
"approval": "open",
|
||||
"pays": [
|
||||
{ "event": "sale", "kind": "percent", "value": 30 },
|
||||
{ "event": "subscription", "kind": "percent", "value": 30, "months": 12 },
|
||||
{ "event": "signup", "kind": "amount", "value": 0.5 }
|
||||
],
|
||||
"link": { "param": "oa", "template": "https://crawlproof.com/?oa={code}", "deep": true },
|
||||
"window": 30,
|
||||
"attribution": "last",
|
||||
"hold_days": 30,
|
||||
"payout": {
|
||||
"methods": ["usdc/eip155:137", "usdc/eip155:8453"],
|
||||
"min": 10,
|
||||
"schedule": "weekly"
|
||||
},
|
||||
"disclosure": "Paid partner link",
|
||||
"self": "refused",
|
||||
"regions": ["US", "CA", "GB", "EU"],
|
||||
"creatives": "https://crawlproof.com/affiliate/creatives.json",
|
||||
"status": "active"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
The smallest valid descriptor is a merchant with a name and a program with a title and something it pays:
|
||||
|
||||
```json
|
||||
{
|
||||
"merchant": { "name": "CrawlProof" },
|
||||
"programs": [{ "title": "Partners", "pays": [{ "event": "sale", "kind": "percent", "value": 30 }] }]
|
||||
}
|
||||
```
|
||||
|
||||
The rules, and every one degrades:
|
||||
|
||||
1. **`merchant.name`, `programs[].title` and `programs[].pays` are the only required keys.** A reader lists what it was given and reports the rest as unstated rather than assumed. A program with no `join` is one the reader can describe but not join; a person joins it at `url`.
|
||||
2. **`merchant`** is who pays. `web` is the store, `currency` the ISO 4217 code every amount in the file is in, `terms` the page the program is under, `operator` the person or organisation answerable as an OpenProfile.md URL, and `jwks` the merchant's public keys for signing webhooks, in JWK Set form. A merchant without `jwks` sends unsigned webhooks, and an affiliate confirms them against the ledger.
|
||||
3. **`updated`** is when anything in the file last changed, ISO 8601. On a program it wins for that program. A reader with `updated` unchanged since its last fetch may skip the rest.
|
||||
4. **`id`** is stable for as long as the program is the same program. It is the dedupe key. Absent, the reader derives one from `title`.
|
||||
5. **`pays`** is a list, one entry per event the program pays on. `event` is `sale`, `subscription`, `signup`, `lead`, `install` or `other`. `kind` is `percent` or `amount`; `value` a percentage of the conversion's `amount` for `percent`, an amount in `currency` for `amount`. On a `subscription` entry, `months` is how many renewals pay, absent meaning every renewal for as long as the customer stays. An event not listed pays nothing. `sale` is a one-time charge; `subscription` is each charge of a recurring one, the first included.
|
||||
6. **`link`** is how a customer arrives. `param` is the query parameter that carries the affiliate's code, default `oa`. `template` is the link the merchant hands out with `{code}` replaced; absent, it is `web` with `param` appended. `deep: true` means the same parameter works on any page of the site, so an affiliate links to the product it is recommending, not the home page. A merchant that accepts a second parameter name for an older program may list it in `aliases`.
|
||||
7. **`window`** is the number of days after a click during which a conversion is attributed to it. **`attribution`** is `last` or `first`: whether a later click by a different affiliate replaces the earlier one. Absent `window` is unstated; a directory shows it as unstated, never as forever.
|
||||
8. **`hold_days`** is how long a recorded conversion stays `pending` before the merchant approves it, which is the merchant's refund window. Absent is unstated. **`payout`** is how money leaves: `methods` is a list of `asset/chain` pairs in CAIP-2 form for on-chain settlement, or a named rail such as `paypal` or `wire`; `min` the balance below which nothing is sent, in `currency`; `schedule` `weekly`, `monthly` or `on_request`.
|
||||
9. **`approval`** is `open` (a join answers with an active membership at once) or `review` (a join answers `pending` and the merchant decides). Absent is `review`. **`self`** is `refused` when a conversion by the affiliate's own account pays nothing, `allowed` when it pays; absent is `refused`.
|
||||
10. **`disclosure`** is the wording the merchant asks the affiliate to show beside a link. **`regions`** are the ISO country codes of customers the program pays for. **`creatives`** is a URL of a JSON list of `{ "url", "kind", "width", "height", "alt" }` the affiliate may use as given. **`status`** is `active`, `paused` or `closed`; a paused program keeps paying on earlier clicks and accepts no joins.
|
||||
11. **Unknown keys are kept.** A merchant says more than this document names, and a reader passes it through under the merchant's own key.
|
||||
|
||||
Serve it as `application/json`. The descriptor is a claim; that it came from the merchant's own origin is the verification, and it is the reason the file exists: a commission fetched from the merchant's `/.well-known/` is the commission the merchant says it pays, today, in words it cannot say it never agreed to.
|
||||
|
||||
## Joining
|
||||
|
||||
An affiliate joins by POSTing to the program's `join` URL:
|
||||
|
||||
```json
|
||||
{
|
||||
"program": "partners",
|
||||
"profile": "https://anthony.example/.well-known/openprofile.md",
|
||||
"pay": "eip155:137:0xCC3b072391AE7A8d10cF00DdC5F61DB2cA5541E5",
|
||||
"webhook": "https://anthony.example/openaffiliate/events",
|
||||
"code": "anthony"
|
||||
}
|
||||
```
|
||||
|
||||
`profile` is the affiliate's identity and the only required key. The merchant fetches it; `Kind` says whether it is a person or an agent, `Pay` supplies the payout address when `pay` is absent, and `Operator` says who answers for an agent. `code` is the affiliate's preferred code, granted if free. `webhook` is where the merchant posts events.
|
||||
|
||||
The merchant answers:
|
||||
|
||||
```json
|
||||
{
|
||||
"membership": "am_8f3c",
|
||||
"program": "partners",
|
||||
"status": "active",
|
||||
"code": "anthony",
|
||||
"link": "https://crawlproof.com/?oa=anthony",
|
||||
"token": "oa_5Kq…",
|
||||
"ledger": "https://crawlproof.com/api/affiliate/v1/ledger",
|
||||
"pays": [{ "event": "sale", "kind": "percent", "value": 30 }]
|
||||
}
|
||||
```
|
||||
|
||||
`token` is the bearer credential for the ledger, shown once. `status` is `active`, `pending` or `refused`; a `review` program answers `pending` and posts `membership.approved` or `membership.refused` to the webhook later. `pays` is the terms as they stood at the join, so the affiliate keeps a copy of what it agreed to. A second join by the same `profile` to the same program answers the existing membership without a new token.
|
||||
|
||||
An affiliate proves it controls `profile` by listing the membership's `link` in the profile's Accounts section, rel=me style. A merchant on `review` may wait for that; one on `open` need not.
|
||||
|
||||
## Links and attribution
|
||||
|
||||
A link is the merchant's URL with `param` set to the affiliate's code. Nothing else: no redirect through a tracking host, no shortener, no pixel. The customer lands on the merchant, the merchant reads the parameter, and the merchant owns the attribution.
|
||||
|
||||
Rules:
|
||||
|
||||
1. **Only a navigation sets attribution.** A parameter that arrives on an image, a frame, a script or a prefetch sets nothing. This is the whole defence against cookie stuffing, and it is the merchant's to enforce.
|
||||
2. **The parameter is stripped after reading** so the customer's copied URL carries no code.
|
||||
3. **Attribution lasts `window` days** from the click that set it, and `attribution` says whether a later click replaces it.
|
||||
4. **A conversion by the affiliate's own account** follows `self`.
|
||||
5. **The merchant may honour `?oa=` on a page it does not control**, such as a coupon code typed at checkout that matches an affiliate's code, and says so in `link.aliases` or `terms`.
|
||||
|
||||
## The ledger
|
||||
|
||||
A membership reads its own ledger with its token:
|
||||
|
||||
```
|
||||
GET {ledger}?since=2026-09-01T00:00:00Z
|
||||
Authorization: Bearer oa_5Kq…
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"membership": "am_8f3c",
|
||||
"program": "partners",
|
||||
"currency": "USD",
|
||||
"clicks": { "total": 412, "window": 38 },
|
||||
"balance": { "pending": 44.7, "approved": 132.0, "paid": 890.1 },
|
||||
"conversions": [
|
||||
{
|
||||
"id": "cv_01J9",
|
||||
"at": "2026-09-12T14:02:11Z",
|
||||
"event": "subscription",
|
||||
"order": "sub_3f9a",
|
||||
"amount": 49.0,
|
||||
"commission": 14.7,
|
||||
"status": "pending",
|
||||
"held_until": "2026-10-12T14:02:11Z",
|
||||
"recurring": { "n": 1, "of": 12 }
|
||||
},
|
||||
{
|
||||
"id": "cv_01J2",
|
||||
"at": "2026-08-30T09:15:00Z",
|
||||
"event": "sale",
|
||||
"amount": 29.0,
|
||||
"commission": 8.7,
|
||||
"status": "reversed",
|
||||
"reason": "refunded 2026-09-04"
|
||||
}
|
||||
],
|
||||
"payouts": [
|
||||
{ "id": "po_77", "at": "2026-09-07T00:00:00Z", "amount": 120.0, "method": "usdc/eip155:137", "tx": "0x9a…" }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
A conversion is `pending` from the moment it is recorded, `approved` when `hold_days` pass without a refund, `reversed` with a `reason` when the merchant takes it back, and `paid` when a payout covers it. `amount` is what the customer paid, net of tax, shipping and any refund; `commission` is what the affiliate earns, in `currency`. `order` is an opaque handle the merchant can look up; it is never the customer. **A reversal without a `reason` is not a reversal**: an affiliate reading one reports the merchant as not conforming, and a directory that hears of it says so beside the program.
|
||||
|
||||
`since` filters on `at` and on the time a row last changed, so an affiliate polling the ledger sees reversals of old rows. `balance.pending` is the sum of pending commissions, `approved` the sum approved and unpaid, `paid` the lifetime total sent.
|
||||
|
||||
## Webhooks
|
||||
|
||||
When a membership gave a `webhook`, the merchant POSTs one JSON object per event to it:
|
||||
|
||||
```json
|
||||
{
|
||||
"event": "conversion.approved",
|
||||
"at": "2026-10-12T14:02:11Z",
|
||||
"merchant": "https://crawlproof.com",
|
||||
"program": "partners",
|
||||
"membership": "am_8f3c",
|
||||
"conversion": { "id": "cv_01J9", "event": "subscription", "amount": 49.0, "commission": 14.7, "status": "approved" }
|
||||
}
|
||||
```
|
||||
|
||||
Events are `membership.approved`, `membership.refused`, `membership.ended`, `conversion.recorded`, `conversion.approved`, `conversion.reversed`, `payout.sent` and `program.changed`. A merchant with `jwks` signs the raw body and sends the signature as `X-OpenAffiliate-Signature: ed25519=<base64url>`; the affiliate verifies against the key set at `merchant.jwks`. Without a signature the webhook is a hint, and the ledger is the truth. A merchant retries a failed delivery for a day and then stops; the ledger still has the row.
|
||||
|
||||
`program.changed` fires when a program's `pays`, `window`, `hold_days` or `payout` change, and carries the new program. Terms change forward only: a conversion keeps the `pays` that stood when its click happened.
|
||||
|
||||
## Payouts
|
||||
|
||||
A payout is the merchant sending the approved balance to the affiliate's `pay` address, on the program's `schedule`, once the balance reaches `min`. On-chain methods settle to the address as given; the ledger's `tx` is the transaction. A named rail settles by that rail's own reference. The merchant sends the whole approved balance or nothing; it does not net a fee, because there is no network to pay one to. A merchant that must withhold tax says so in `terms` and shows the withheld amount as its own row on the payout.
|
||||
|
||||
## Discovery
|
||||
|
||||
A reader finds a descriptor three ways, in this order:
|
||||
|
||||
1. `/.well-known/openaffiliate.json` on the merchant's origin.
|
||||
2. `<link rel="openaffiliate" href="...">` in the HTML of the merchant's home page, or a `Link: <...>; rel="openaffiliate"` header, when the file lives somewhere else.
|
||||
3. A URL handed to the reader directly.
|
||||
|
||||
A descriptor is **verified** when it was fetched from the same origin as `merchant.web`, or from `/.well-known/` on the origin the reader was pointed at. One found by the third route on some other host is a claim about the merchant by whoever hosts it, and a directory marks it so.
|
||||
|
||||
## Directories
|
||||
|
||||
A directory reading descriptors:
|
||||
|
||||
1. **Fetches daily at least**, and on `program.changed` when it holds a membership.
|
||||
2. **Dedupes on the merchant's origin and the program's `id`.** A re-read updates the row; a program that leaves the file is marked closed, not deleted.
|
||||
3. **Shows the terms with the time they were read**, and the merchant's own `terms` link beside them.
|
||||
4. **Keeps the merchant's `join` and `url` unchanged.** A directory that joins on an affiliate's behalf does so with the affiliate's profile, not its own, and holds the token for the affiliate, not against them.
|
||||
5. **Reports absence as absence.** No `window` is unstated, not lifetime. No `hold_days` is unstated, not instant.
|
||||
6. **Ranks the verified above the claimed**, and a program whose merchant reverses without reasons below both.
|
||||
7. **Takes nothing from the commission.** A directory that charges does so as a fee to whoever asked it, stated up front, and never as a share of a conversion.
|
||||
|
||||
The first directory reading OpenAffiliate is the programs list at [crawlproof.com/affiliate/programs](https://crawlproof.com/affiliate/programs), which also joins programs for the people and agents who use it and shows every ledger on one page.
|
||||
|
||||
## What is deliberately absent
|
||||
|
||||
**No network.** The merchant serves the terms, records the conversions and sends the money. A directory lists and may hold a token on an affiliate's behalf. Nobody sits in the middle of a payment.
|
||||
|
||||
**No tracking host.** A link is the merchant's URL with a parameter. There is no redirect to log, no third-party cookie to lose and no pixel to block.
|
||||
|
||||
**No application form.** An affiliate is a profile. A merchant that wants to look first says `review`; one that does not says `open`.
|
||||
|
||||
**No exclusivity.** A membership binds nobody to one program, and a program may not require it.
|
||||
|
||||
**No impression payments.** A view is not an event in `pays`. The events are things a customer did.
|
||||
|
||||
## Serving one
|
||||
|
||||
By hand, from the table that already exists. A merchant with a referral column on its orders has the ledger; the descriptor is the terms written down, the join call is an insert, and the payout is what it already does on a schedule. The reference implementation is [crawlproof.com](https://crawlproof.com/affiliate), which runs its own program at `/.well-known/openaffiliate.json`, joins other merchants' programs from the same dashboard, and pays in USDC on Polygon through CoinPay.
|
||||
|
||||
## Related standards
|
||||
|
||||
- [OpenProfile.md](/openprofile): the affiliate's identity and payout address, and the `operator` behind a merchant.
|
||||
- [OpenCoupon](/opencoupon): a merchant's promotions, the same shape. A coupon that is also an affiliate code lists in both files.
|
||||
- [OpenAccess](/openaccess): a program that pays only for customers holding an entitlement names the product.
|
||||
- [OpenServer](/docs/openserver): the pattern of a table the seller already keeps, served at a fixed URL.
|
||||
|
||||
## Version history
|
||||
|
||||
| Version | Date | Change |
|
||||
|---|---|---|
|
||||
| 0.1 | 2026-09-13 | First publication: the descriptor, six events, join, links and attribution, the ledger, webhooks, payouts, discovery, what a directory owes a merchant. |
|
||||
|
||||
## License
|
||||
|
||||
The specification text is CC BY 4.0. Serve it, copy it, extend it.
|
||||
175
docs/openthreat.md
Normal file
175
docs/openthreat.md
Normal file
|
|
@ -0,0 +1,175 @@
|
|||
# OpenThreat
|
||||
|
||||
OpenThreat is one file a security tool serves about what it found in the open: a finding in a public repository, an attack observed against the reporter's own infrastructure, an indicator worth blocking, an advisory. A directory reads the reporter's own file instead of a vendor's feed, a defender's agent reads it instead of a dozen dashboards, and the reporter stays the author of what it discloses and what it withholds. It is maintained by Profullstack, Inc. as part of the LogicSRC open-standards surface.
|
||||
|
||||
Status: **0.1**. A description of a file [ThreatCrush](https://threatcrush.com/discovery) serves and [nichedb.dev](https://nichedb.dev/c/threats) reads, published so any scanner or sensor can serve one and any directory can read it.
|
||||
|
||||
Slug: `openthreat`
|
||||
|
||||
## The problem
|
||||
|
||||
Every security tool finds things, and every one keeps what it found behind its own login. A scanner that runs on a thousand public repositories knows which rules fire most and where, and says nothing, because saying it would mean a feed, a schema, an API key and a sales call. The threat feeds that do exist are products: a licence per seat, terms that forbid redistribution, and a format each vendor invented. A defender wanting to know what is being found in the open, this week, by tools that are not theirs, cannot ask.
|
||||
|
||||
The tool already has the data, and for the open part of it there is no reason to hold it. What is missing is the one file that puts what a tool found in public where a reader can fetch it, with a rule for what may go in it.
|
||||
|
||||
## Terms
|
||||
|
||||
- A **reporter** is a tool or the operator of one: a scanner, a sensor, a daemon, a research team. Its **descriptor** is the file it serves.
|
||||
- A **threat** is one thing the reporter found: a finding, an attack, an indicator or an advisory.
|
||||
- A **subject** is what the threat is about: a public repository, a package, a host the reporter operates. A subject is never a private one.
|
||||
- A **directory** is anything that reads descriptors across reporters: a threat index, a dashboard, a defender's cache.
|
||||
|
||||
## The descriptor
|
||||
|
||||
A reporter serves a JSON document at `/.well-known/openthreat.json` on its own origin.
|
||||
|
||||
```json
|
||||
{
|
||||
"openthreat": "0.1",
|
||||
"reporter": {
|
||||
"name": "ThreatCrush",
|
||||
"web": "https://threatcrush.com",
|
||||
"operator": "https://profullstack.com/.well-known/openprofile.md",
|
||||
"tool": "threatcrush",
|
||||
"policy": "https://threatcrush.com/discovery#policy"
|
||||
},
|
||||
"updated": "2026-09-13T06:00:00Z",
|
||||
"threats": [
|
||||
{
|
||||
"id": "3f9a1c2b",
|
||||
"kind": "finding",
|
||||
"title": "SQL assembled by concatenation",
|
||||
"severity": "high",
|
||||
"confidence": "evidence",
|
||||
"rule": "js-sql-string-building",
|
||||
"cwe": "CWE-89",
|
||||
"category": "code",
|
||||
"subject": { "name": "northwind/api", "url": "https://github.com/northwind/api", "ref": "main", "commit": "9c1f0e2" },
|
||||
"location": { "file": "src/db/users.ts", "line": 42 },
|
||||
"status": "open",
|
||||
"first_seen": "2026-09-10T02:14:00Z",
|
||||
"last_seen": "2026-09-13T05:40:00Z",
|
||||
"message": "A query string is built from request input.",
|
||||
"consequence": "An attacker who controls the input controls the query.",
|
||||
"tlp": "clear"
|
||||
},
|
||||
{
|
||||
"id": "b71e0d44",
|
||||
"kind": "finding",
|
||||
"title": "Hardcoded credential",
|
||||
"severity": "critical",
|
||||
"rule": "secret-generic-credential",
|
||||
"cwe": "CWE-798",
|
||||
"category": "secret",
|
||||
"subject": { "name": "northwind/api", "url": "https://github.com/northwind/api" },
|
||||
"status": "open",
|
||||
"last_seen": "2026-09-13T05:40:00Z"
|
||||
},
|
||||
{
|
||||
"id": "ssh-91.232.105.3",
|
||||
"kind": "attack",
|
||||
"title": "SSH brute force",
|
||||
"severity": "medium",
|
||||
"rule": "ssh-bruteforce",
|
||||
"source": { "ip": "91.232.105.3", "country": "RU" },
|
||||
"target": { "port": 22, "service": "ssh" },
|
||||
"indicators": [{ "type": "ip", "value": "91.232.105.3" }],
|
||||
"status": "blocked",
|
||||
"first_seen": "2026-09-12T22:01:00Z",
|
||||
"last_seen": "2026-09-13T04:52:00Z",
|
||||
"count": 47
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
The smallest valid descriptor is a reporter with a name and a threat with a title:
|
||||
|
||||
```json
|
||||
{ "reporter": { "name": "ThreatCrush" }, "threats": [{ "title": "SSH brute force" }] }
|
||||
```
|
||||
|
||||
The rules, and every one degrades except the two marked:
|
||||
|
||||
1. **`reporter.name` and `threats[].title` are the only required keys.** A reader lists what it was given and reports the rest as unstated.
|
||||
2. **`reporter`** is who found it. `web` is the reporter's site, `tool` the name of the software, `operator` the person or organisation answerable as an [OpenProfile.md](/openprofile) URL, and `policy` the page that says what the reporter publishes and what it withholds. A reporter with a policy page is one a reader can hold to it.
|
||||
3. **`updated`** on the descriptor is when anything in it last changed. A reader with it unchanged since its last fetch may skip the rest.
|
||||
4. **`id`** is stable for as long as the threat is the same thing. It is the dedupe key. Absent, the reader derives one from `kind`, `subject.name`, `rule` and `title`, and a retitled threat becomes a new one.
|
||||
5. **`kind`** is `finding` (something in a subject's code or configuration), `attack` (traffic observed against the reporter's own infrastructure), `indicator` (a value worth blocking or watching, on its own), or `advisory` (a statement about a vulnerability, with `refs`). Absent means `finding`.
|
||||
6. **`severity`** is `info`, `low`, `medium`, `high` or `critical`. **`confidence`** is the reporter's own word for how sure it is, kept as written. **`rule`** is the reporter's stable rule identifier, the same string a SARIF `ruleId` carries. **`cwe`** is `CWE-` and a number. **`category`** is the reporter's own grouping.
|
||||
7. **`subject`** is what the threat is about, and it is public by definition. `name` is the subject as the world knows it (`owner/repo`, a package name, a hostname the reporter operates), `url` where it lives, `ref` and `commit` what was looked at. **`location`** is `file` and `line` within the subject.
|
||||
8. **`status`** is `open`, `fixed`, `mitigated`, `blocked` or `withdrawn`. `fixed` is a finding no longer present; `blocked` is an attack the reporter stopped; `withdrawn` is a threat the reporter retracted, kept in the file for a while so directories learn it was. Absent means `open`.
|
||||
9. **`first_seen`**, **`last_seen`** are ISO 8601; **`count`** how many times it was observed between them. **`message`** says what was found and **`consequence`** what happens if it is real. **`refs`** are URLs: an advisory, a commit, a write-up.
|
||||
10. **`source`** and **`target`** describe an attack: where it came from (`ip`, `asn`, `country`) and what it hit (`port`, `service`, `path`). **`indicators`** are values a defender can act on, each `type` (`ip`, `cidr`, `domain`, `url`, `hash`, `ua`) and `value`.
|
||||
11. **`tlp`** is the Traffic Light Protocol label. A descriptor at `/.well-known/` is `clear` by definition, and a reader treats an absent `tlp` as `clear`. A reporter with anything else to say does not put it here.
|
||||
12. **Unknown keys are kept.** A reporter says more than this document names, and a reader passes it through under the reporter's own key.
|
||||
|
||||
**The two rules that do not degrade.**
|
||||
|
||||
**A subject is public or it is not in the file.** A finding about a private repository, a customer's server, a paying user's scan, or anyone's infrastructure but the reporter's own is not a threat in the open; it is someone's private security posture, and publishing it is a breach. A reporter serves only what was already visible to anyone who looked: public repositories, public packages, its own hosts. A reporter that scans private things keeps two tables and serves one.
|
||||
|
||||
**A secret is never located while it is open.** A finding whose category is `secret`, or that the reporter marks sensitive, is published with its `rule`, `severity`, `subject` and `status` only: no `location`, no `message`, no excerpt. The credential is already exposed by being in a public repository; the file must not be the map to it. Once `status` is `fixed` the location may follow.
|
||||
|
||||
Serve it as `application/json`. The descriptor is a claim; that it came from the reporter's own origin is the verification.
|
||||
|
||||
## Announcing, and opting out
|
||||
|
||||
A subject that is scanned by a reporter's tool did not ask to be listed. The reporter's policy says how a subject's owner turns publication off, and the reporter honours it: a subject that opts out disappears from the file on the next build, and any threat about it already read by a directory is served once more as `withdrawn` so the directory retracts it too. Announcing is on by default, because a finding in a public repository is public already and a list nobody is on lists nothing; opting out is one switch, in the tool's own settings, and costs nothing.
|
||||
|
||||
## Discovery
|
||||
|
||||
A reader finds a descriptor three ways, in this order:
|
||||
|
||||
1. `/.well-known/openthreat.json` on the reporter's origin.
|
||||
2. `<link rel="openthreat" href="...">` in the HTML of the reporter's home page, or a `Link: <...>; rel="openthreat"` header, when the file lives somewhere else.
|
||||
3. A URL handed to the reader directly.
|
||||
|
||||
A descriptor is **verified** when it was fetched from the same origin as `reporter.web`, or from `/.well-known/` on the origin the reader was pointed at. One found by the third route on some other host is a claim about the reporter by whoever hosts it, and a directory marks it so.
|
||||
|
||||
## Directories
|
||||
|
||||
A directory reading descriptors:
|
||||
|
||||
1. **Fetches hourly at least.** Threats are fixed, blocked and withdrawn on the hour; a file read once is a snapshot.
|
||||
2. **Dedupes on the reporter's origin and the threat's `id`.** A re-read updates the row; it never adds a second. A `withdrawn` threat is retracted, not merely marked.
|
||||
3. **Keeps the reporter's words and attributes the reporter.** Every listed threat says who found it and links the subject's `url`.
|
||||
4. **Never adds what the reporter withheld.** A directory that fetches the subject and finds the secret the reporter declined to locate, and publishes the line, has broken the second rule on the reporter's behalf.
|
||||
5. **Reports absence as absence.** No `status` is open. No `severity` is unstated, not low.
|
||||
|
||||
The first directory reading OpenThreat is the threats collection at [nichedb.dev](https://nichedb.dev/c/threats), which lists every reporter's threats as feeds, with RSS, JSON, an API and MCP over the same rows. The first reporter is [ThreatCrush](https://threatcrush.com/discovery), whose GitHub App scans public repositories and serves what it found.
|
||||
|
||||
## Related formats
|
||||
|
||||
- [SARIF](https://sarifweb.azurewebsites.net/) is what a scanner emits per run: `rule` here is SARIF's `ruleId`, and `location` its physical location. OpenThreat is the standing list across runs, with a subject and a status, and the disclosure rules SARIF does not have.
|
||||
- [STIX 2.1](https://oasis-open.github.io/cti-documentation/) describes threat intelligence exhaustively. An `indicator` here maps onto a STIX Indicator, an `attack` onto a Sighting. OpenThreat is the small file a tool can serve in an afternoon; a directory that speaks STIX can translate.
|
||||
- [CSAF](https://oasis-open.github.io/csaf/) is the vendor advisory format. An `advisory` here carries the CSAF document in `refs`.
|
||||
|
||||
## What is deliberately absent
|
||||
|
||||
**No private subjects.** Stated above and worth stating twice. The file is for what was found in the open.
|
||||
|
||||
**No exploit detail.** `message` says what was found; `consequence` says what it means. How to use it is nobody's business here.
|
||||
|
||||
**No scoring across reporters.** `severity` is the reporter's. A directory that ranks reporters or normalises severities labels the result as its own.
|
||||
|
||||
**No push.** A reporter serves a file. A directory that wants to be told may watch `updated`; a webhook is another document's business.
|
||||
|
||||
## Serving one
|
||||
|
||||
From the table the tool already keeps of what it found, filtered to public subjects, with secrets unlocated and opt-outs removed, at a fixed URL. ThreatCrush builds its file from the scans its GitHub App ran on public repositories, and nothing else.
|
||||
|
||||
## Related standards
|
||||
|
||||
- [OpenProfile.md](/openprofile): the `operator` behind a reporter.
|
||||
- [OpenServer](/docs/openserver), [OpenCoupon](/docs/opencoupon): the same serve-your-own-file shape for a provider's catalog and a merchant's promotions.
|
||||
- [OpenMCP](/openmcp): a directory that also serves its rows over MCP describes that door with an OpenMCP descriptor.
|
||||
|
||||
## Version history
|
||||
|
||||
| Version | Date | Change |
|
||||
|---|---|---|
|
||||
| 0.1 | 2026-09-13 | First publication: the descriptor, four kinds, twelve rules and the two that do not degrade, announcing and opting out, discovery, what a directory owes a reporter. |
|
||||
|
||||
## License
|
||||
|
||||
The specification text is CC BY 4.0. Serve it, copy it, extend it.
|
||||
Loading…
Add table
Add a link
Reference in a new issue