Skip to content
Closed
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
54 changes: 54 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ or miss one that does. A test fails the build when it drifts.
| `moshcode pwd` <br>`where` | system | show the current directory and git context |
| `moshcode engines` | engines | list engines and installation status |
| `moshcode tools` | tools | list workflow tools and installation status |
| `moshcode trade` | tools | look up markets and trade through Alpaca |
| `moshcode commands` | script | list built-in moshscript commands |
| `moshcode completion` | extend | print a shell completion script |
| `moshcode run` | script | run a moshscript |
Expand Down Expand Up @@ -154,6 +155,59 @@ native setup and authentication commands. CoinPay currently requires Node.js
In the TUI, use `/tools`, `/ugig [args…]`, or `/coinpay [args…]`. The native CLI
owns the terminal until it exits, then MoshCode returns to the pit.

### Alpaca trading

Alpaca is a workflow tool, not a coding engine. Install its official Go CLI,
use `alpaca` for exact native passthrough, or use `trade` for the shorter market
and order vocabulary:

```sh
moshcode install alpaca # go install github.com/alpacahq/cli/cmd/alpaca@latest
moshcode trade login # Alpaca profile login; paper trading is the default
moshcode trade ticker AAPL # asset get --symbol-or-asset-id AAPL
moshcode trade quote AAPL # latest quote
moshcode trade analysis AAPL # quote/trade/bar snapshot for analysis
moshcode trade watch # list watchlists
moshcode trade positions # list open positions
moshcode trade orders # list open orders
```

`buy` and `sell` are safe previews unless `--submit` is explicit. Other Alpaca
order flags pass through, including limit prices and its separate live-trading
opt-in:

```sh
moshcode trade buy AAPL 1 # adds --type market --dry-run
moshcode trade buy AAPL 1 --type limit --limit-price 185
moshcode trade buy AAPL --notional 100 # preview a $100 market buy
moshcode trade buy AAPL 1 --submit # places the paper order
moshcode trade raw data news --symbol AAPL # any native Alpaca command
moshcode alpaca order submit --help # exact native passthrough
```

The same facade is `/trade …` in the pit and `trade(…)` in moshscript.
Alpaca's CLI has no confirmation prompts; `--submit` intentionally removes
MoshCode's preview guard. Live trading additionally requires Alpaca's `--live`
opt-in or corresponding environment setting.

### Social posting from the pit

The pit can hand a prepared post to Bluesky or Nostr without storing either
account's credentials in MoshCode:

```text
/socials
/post bsky "shipped it 🤘"
/post nostr "shipped it 🤘"
```

Bluesky opens its official compose intent. Nostr opens the MoshCode composer,
connects to a NIP-07 browser signer (or a NIP-46 bunker through
[`window.nostr.js`](https://github.com/fiatjaf/window.nostr.js)), signs a kind-1
event, and publishes it to the displayed relays. Both flows leave the final
confirmation in the browser. If the pit is remote or headless, `/post` prints
the composer URL instead.

## Browser terminal (`moshcode console`)

A real terminal in the browser — arrow keys, history, full-screen TUIs — because
Expand Down
62 changes: 62 additions & 0 deletions apps/pwa/src/lib/moshpit-name.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,68 @@ export function parseMoshpitName(input) {
return { label: normalizedLabel, tld: normalizedTld };
}

/**
* A label as the records table may hold it: an ordinary label, or the wildcard
* form `*.<label>`.
*
* `*` is legal as the whole leftmost label of a record name and nowhere else.
* `*.chovy` is "every name under chovy", which is a thing DNS has always been
* able to say; `ch*vy` or `*.*` is nothing, and a parser that guessed at it
* would publish a record no resolver will ever match.
*
* Registration does not use this — moshpit_names stays strictly one label, and
* normalizeLabel is still the only door into it. This is for the paths that
* read and write DNS records, where a wildcard is a record name, not a name.
*/
export function normalizeRecordLabel(input) {
const raw = String(input ?? "").trim().toLowerCase();
if (!raw.startsWith("*.")) return normalizeLabel(raw);
const parent = normalizeLabel(raw.slice(2));
return parent ? `*.${parent}` : null;
}

/**
* A name as the record and resolve paths may see it: everything
* parseMoshpitName accepts, plus one more label on the left.
*
* `foo.chovy.hacker` comes back as tld "hacker", label "foo.chovy" — the
* records table keys on (tld, label), so a third-level name needs no new
* shape, only a second dot in the label half. The wildcard spelling
* `*.chovy.hacker` is the same with `*` as the leftmost label, and it is the
* only place `*` may stand.
*
* Returned alongside: `sub`, the leftmost label as written (null for a
* two-label name), and `parent`, the registered name the wildcard hangs off —
* `foo.chovy.hacker` is answered by the records of `*.chovy`, never by
* registering `foo.chovy`, because registration stays two labels.
*
* parseMoshpitName is deliberately untouched: registration, pricing and the
* bulk-claim list all keep the strict two-label rule, and only the paths that
* answer DNS questions take this one.
*/
export function parseMoshpitQueryName(input) {
const raw = String(input ?? "").trim().toLowerCase().replace(/^\.+/, "").replace(/\.+$/, "");
if (!raw) return null;
const parts = raw.split(".");
if (parts.length === 2) {
const name = parseMoshpitName(raw);
return name ? { ...name, sub: null, parent: name.label, wildcard: false } : null;
}
if (parts.length !== 3) return null;
const [sub, parentLabel, ending] = parts;
const parent = normalizeLabel(parentLabel);
const tld = normalizeTld(ending);
if (!parent || !tld) return null;
if (sub !== "*" && !normalizeLabel(sub)) return null;

// The same IPv4-literal guard parseMoshpitName applies, on the registered
// half: `foo.1.420` reads as an address with something in front of it.
if (/^\d+$/.test(parent) && /^\d+$/.test(tld)) return null;

const left = sub === "*" ? "*" : normalizeLabel(sub);
return { label: `${left}.${parent}`, tld, sub: left, parent, wildcard: sub === "*" };
}

/**
* Labels worth offering under an ending that has nothing under it yet.
*
Expand Down
110 changes: 98 additions & 12 deletions apps/pwa/src/moshpit.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,10 @@ import {
MAX_BULK_TLDS,
MAX_CHILD_PRICE_USD,
normalizeLabel,
normalizeRecordLabel,
normalizeTld,
parseMoshpitName,
parseMoshpitQueryName,
parseTldList,
tldRejection,
} from "./lib/moshpit-name.mjs";
Expand All @@ -32,8 +34,8 @@ import {
} from "./lib/moshpit-records.mjs";

export {
RESERVED_TLDS, RESOLVE_MODES, MAX_BULK_TLDS, BULK_CHUNK, BULK_TIME_BUDGET_MS, shortCount, DEFAULT_TLD_PRICE_USD, MAX_CHILD_PRICE_USD, CHILD_PRICE_USD, ENDING_PRICE_USD, normalizeLabel, normalizeTld, parseMoshpitName,
parseTldList, tldRejection, normalizeMode, resolutionPreference, STARTER_LABELS, suggestedLabels,
RESERVED_TLDS, RESOLVE_MODES, MAX_BULK_TLDS, BULK_CHUNK, BULK_TIME_BUDGET_MS, shortCount, DEFAULT_TLD_PRICE_USD, MAX_CHILD_PRICE_USD, CHILD_PRICE_USD, ENDING_PRICE_USD, normalizeLabel, normalizeRecordLabel, normalizeTld, parseMoshpitName,
parseMoshpitQueryName, parseTldList, tldRejection, normalizeMode, resolutionPreference, STARTER_LABELS, suggestedLabels,
} from "./lib/moshpit-name.mjs";

export {
Expand Down Expand Up @@ -380,7 +382,11 @@ export async function releaseName({ tld: tldInput, label: labelInput, userId })
await run(`DELETE FROM moshpit_name_pins WHERE tld = ? AND label = ?`, [owned.tld, owned.label]);
// Records go with the name for the same reason, and it matters more: an
// inherited MX would route the next holder's mail to the last one's server.
await run(`DELETE FROM moshpit_records WHERE tld = ? AND label = ?`, [owned.tld, owned.label]);
// The wildcard hangs off the name rather than existing beside it, so
// `*.chovy`'s records go too — an inherited one would answer for every
// subdomain of a name whose owner just changed.
await run(`DELETE FROM moshpit_records WHERE tld = ? AND (label = ? OR label = ?)`,
[owned.tld, owned.label, `*.${owned.label}`]);
await run(`DELETE FROM moshpit_names WHERE tld = ? AND label = ?`, [owned.tld, owned.label]);
await logAction(owned.tld, userId, `unname:${owned.label}`);
return { ok: true };
Expand All @@ -396,6 +402,23 @@ async function ownedName(tldInput, labelInput, userId) {
return { ok: true, tld, label };
}

/**
* Ownership of the name a record goes on — including the wildcard form.
*
* Records for `*.chovy` are authorized by owning `chovy`: the wildcard answers
* for everything under the name, so it belongs to whoever the name belongs to.
* The wildcard label itself is what comes back, because that is the key the
* records table stores — the parent only decided whether you may write there.
*/
async function ownedRecordName(tldInput, labelInput, userId) {
const tld = normalizeTld(tldInput);
const label = normalizeRecordLabel(labelInput);
if (!tld || !label) return { ok: false, error: "not a valid name" };
const owned = await ownedName(tld, label.startsWith("*.") ? label.slice(2) : label, userId);
if (!owned.ok) return owned;
return { ok: true, tld, label };
}

/* ---- selling names under a TLD ---- */

/**
Expand Down Expand Up @@ -611,13 +634,20 @@ export async function listNamePurchases(userId, limit = 50) {
* under it keeps its own identity on the other side.
*/
export async function resolveMoshpitName(input) {
const parsed = parseMoshpitName(input);
const parsed = parseMoshpitQueryName(input);
if (!parsed) return null;
const { label, tld } = parsed;
const name = `${label}.${tld}`;

const owner = await getTld(tld);
if (!owner) return { name, resolved: name, aliased: false, registered: false, name_registered: false, target: null };
if (!owner) {
// A third-level name under an ending nobody holds is not a name at all:
// null, so the API's callers say "not a Moshpit name" rather than parking
// something the pit has no authority over. Two-label names keep the old
// answer — the extension's precedence rule reads `registered` off it.
if (parsed.sub) return null;
return { name, resolved: name, aliased: false, registered: false, name_registered: false, target: null };
}

// Where the name ends up: itself, unless the TLD points elsewhere and this
// name is not held back from that alias. Exemption is checked at read time,
Expand All @@ -626,6 +656,28 @@ export async function resolveMoshpitName(input) {
const resolvedTld = aliased ? owner.alias_of : tld;
const resolved = `${label}.${resolvedTld}`;

// A third-level name has no moshpit_names row — registration is strictly two
// labels. What it can have is records: its own exact set, or the wildcard
// set of its parent. With neither it does not exist at all, and null is how
// a resolver hears NXDOMAIN rather than "parked" — a parked answer would
// send every typo under a real name to the for-sale page.
if (parsed.sub) {
const found = await recordsWithWildcard(resolvedTld, label);
if (!found.records.length) return null;
return {
name,
resolved,
aliased,
registered: true,
// The name "exists" in the only way a third-level name can: something
// answers for it. `target` is derived from those records the same way
// addRecord derives it for a registered name, so a wildcard AAAA is the
// address answer exactly as an exact AAAA would be.
name_registered: true,
target: effectiveTarget(null, found.records),
};
}

// The name is looked up on the TLD it actually resolves to -- that is the one
// whose operator mints names there, so it is the only place the answer can
// legitimately come from.
Expand Down Expand Up @@ -777,12 +829,40 @@ const RECORD_ORDER = `CASE type WHEN 'AAAA' THEN 0 WHEN 'CNAME' THEN 1 WHEN 'MX'

export async function listRecords(tldInput, labelInput) {
const tld = normalizeTld(tldInput);
const label = normalizeLabel(labelInput);
if (!tld || !label) return [];
// Read by the label exactly as stored — an ordinary label, the wildcard
// `*.chovy`, or a third-level `foo.chovy` on the exact side of a wildcard
// lookup. Writes are the strict half (normalizeRecordLabel, in addRecord);
// a read that re-validated the key could never see a third-level row at all.
const label = String(labelInput ?? "").trim().toLowerCase();
if (!tld || !label || label.length > 127) return [];
return all(`SELECT ${RECORD_COLS} FROM moshpit_records WHERE tld = ? AND label = ?
ORDER BY ${RECORD_ORDER}`, [tld, label]);
}

/**
* The set a query for (tld, label) answers with, wildcard and all.
*
* Lookup order is DNS's own: the exact name first, then the wildcard of the
* parent. `foo.chovy` under `hacker` is answered by records stored as
* (hacker, "foo.chovy") when it has any — there normally are none, because a
* third-level name cannot be registered and only the wildcard form can be
* published — and otherwise by (hacker, "*.chovy"), answered AS the asked
* name, which is what a wildcard means everywhere else in DNS too.
*
* A two-label name and the wildcard name itself are exact lookups: `*.chovy`
* does not answer for `chovy`, and nothing answers for `*.chovy` but its own
* records.
*/
export async function recordsWithWildcard(tld, label) {
if (!String(label).includes(".") || String(label).startsWith("*.")) {
return { label, records: await listRecords(tld, label) };
}
const exact = await listRecords(tld, label);
if (exact.length) return { label, records: exact };
const wildcard = `*.${String(label).slice(String(label).indexOf(".") + 1)}`;
return { label: wildcard, records: await listRecords(tld, wildcard) };
}

/**
* The records a resolver should answer with for `scrambled.eggs`.
*
Expand All @@ -798,7 +878,7 @@ export async function recordsForName(input) {
const resolution = await resolveMoshpitName(input);
if (!resolution || !resolution.registered) return null;

const parsed = parseMoshpitName(resolution.resolved);
const parsed = parseMoshpitQueryName(resolution.resolved);
if (!parsed) return null;

return {
Expand All @@ -808,7 +888,7 @@ export async function recordsForName(input) {
label: parsed.label,
name_registered: resolution.name_registered,
target: resolution.target,
records: await listRecords(parsed.tld, parsed.label),
records: (await recordsWithWildcard(parsed.tld, parsed.label)).records,
};
}

Expand All @@ -825,7 +905,7 @@ export async function recordsForName(input) {
* what they wanted.
*/
export async function addRecord({ tld: tldInput, label: labelInput, type, value, ttl, priority, userId }) {
const owned = await ownedName(tldInput, labelInput, userId);
const owned = await ownedRecordName(tldInput, labelInput, userId);
if (!owned.ok) return owned;

const name = `${owned.label}.${owned.tld}`;
Expand Down Expand Up @@ -860,7 +940,13 @@ export async function addRecord({ tld: tldInput, label: labelInput, type, value,
// name with a perfectly good AAAA record would still resolve to the parking
// page. Never the other way: an owner who typed a target is not overruled by
// a record they added afterwards.
if (!(await getName(owned.tld, owned.label))?.target) {
//
// A wildcard has no moshpit_names row to mirror into — `*.chovy` is not a
// registered name, and writing the parent's target would answer for the
// parent itself, which the wildcard deliberately does not do. Its address
// answer is derived from the records at resolve time instead, by
// resolveMoshpitName, through the same effectiveTarget rule.
if (!owned.label.startsWith("*.") && !(await getName(owned.tld, owned.label))?.target) {
const target = effectiveTarget(null, await listRecords(owned.tld, owned.label));
if (target) await run(`UPDATE moshpit_names SET target = ? WHERE tld = ? AND label = ?`, [target, owned.tld, owned.label]);
}
Expand All @@ -877,7 +963,7 @@ export async function addRecord({ tld: tldInput, label: labelInput, type, value,
* that already knows exactly which record it means.
*/
export async function removeRecord({ tld: tldInput, label: labelInput, type, value, userId }) {
const owned = await ownedName(tldInput, labelInput, userId);
const owned = await ownedRecordName(tldInput, labelInput, userId);
if (!owned.ok) return owned;

const wanted = normalizeRecordType(type);
Expand Down
Loading