diff --git a/apps/pwa/src/lib/moshpit-name.mjs b/apps/pwa/src/lib/moshpit-name.mjs
index f7be06c..19d1952 100644
--- a/apps/pwa/src/lib/moshpit-name.mjs
+++ b/apps/pwa/src/lib/moshpit-name.mjs
@@ -226,20 +226,25 @@ export function shortCount(n) {
}
/**
- * The most a child name should cost per year.
+ * The most a child name should cost. Once, for good.
*
* This is the `me.whatever` price — what a buyer pays to mint a name under an
* ending someone else holds. It is not the price of `.whatever` itself, which
- * is a separate thing the registry does not charge for yet.
+ * is a separate thing.
*
* $2 flat. PRD 0005 R3 wrote this as $1.99; the extra cent buys nothing but a
* price tag that looks like a supermarket shelf, and every number a person has
* to reason about here — a default, a cap, a per-line override — reads better
* round. The PRD number is superseded by this one.
*
- * The ceiling is on the annual registration/renewal price only. A one-time
- * Buy Now resale transfers ownership rather than starting a term, and §10.2.4
- * puts no ceiling on that.
+ * Not an annual price, and never was in practice: `moshpit_names` has never had
+ * an expiry column and nothing has ever renewed a name. The PRD called it a
+ * yearly fee, the schema sold it outright, and this comment used to describe
+ * the PRD. It now describes what the code does, which is the thing buyers were
+ * actually getting.
+ *
+ * A one-time Buy Now resale transfers ownership rather than starting anything,
+ * and §10.2.4 puts no ceiling on that.
*/
export const MAX_CHILD_PRICE_USD = 2;
@@ -247,7 +252,7 @@ export const MAX_CHILD_PRICE_USD = 2;
export const CHILD_PRICE_USD = MAX_CHILD_PRICE_USD;
/**
- * What a direct ending costs per year: `.whatever` itself.
+ * What a direct ending costs, once: `.whatever` itself.
*
* Nothing charges this yet — `registerTld` inserts a row and claiming is free.
* It lives here anyway so the two prices sit together and the number is settled
@@ -257,6 +262,12 @@ export const CHILD_PRICE_USD = MAX_CHILD_PRICE_USD;
* $5 flat, for the same reason the child price is $2: PRD 0005 §10.1 wrote
* these as $4.99 and $1.99, and the trailing cents buy nothing but a price tag
* shaped like a supermarket shelf.
+ *
+ * Paid once and held for good. §5's one-year term with renewals is withdrawn
+ * (migration 016). A name that lapses is a name somebody else can catch, and
+ * the whole reason to be here is to stop settling for the hyphenated version of
+ * the name you wanted — an annual invoice with a drop date attached is the
+ * thing people are leaving, not something to sell them again.
*/
export const ENDING_PRICE_USD = 5;
diff --git a/apps/pwa/src/migrations/016_moshpit_lifetime.sql b/apps/pwa/src/migrations/016_moshpit_lifetime.sql
new file mode 100644
index 0000000..790e156
--- /dev/null
+++ b/apps/pwa/src/migrations/016_moshpit_lifetime.sql
@@ -0,0 +1,37 @@
+-- Endings are sold once and held for good. The term is withdrawn.
+--
+-- Migration 010 gave endings a one-year term with renewals, per PRD 0005 §5.
+-- That is reversed here: $5 buys `.eggs` outright and $2 buys a name under one,
+-- both paid once. The prices have not changed -- what changed is that they are
+-- not charged again.
+--
+-- The reason is what the namespace is for rather than generosity. A name that
+-- lapses is a name somebody else can catch, and the pitch is that you can
+-- finally hold the clean name instead of the hyphenated one you settled for. An
+-- annual invoice with a drop date on it is the thing people are trying to get
+-- away from; selling it back to them undoes the pitch.
+--
+-- Worth recording that the term never actually shipped: nothing in the app
+-- could open an ending checkout, so `quoteTld` and `quoteRenewal` were
+-- unreachable and only the webhook settler was wired up. No ending was ever
+-- charged a renewal, and no row in the wild has an expiry that this drop takes
+-- away from somebody. That is why this is a plain drop rather than a
+-- grandfathering policy -- §21.8 asks for one before putting endings INTO a
+-- lifecycle, and taking them back out of one nobody was in needs no such thing.
+--
+-- `moshpit_tld_purchases` keeps its `kind` and `years` columns, deliberately.
+-- They are a financial record of what was sold at the time, and a ledger is not
+-- something to rewrite once the product changes. New rows are written
+-- 'register' and 1 for good; a 'renew' row that settles late is honoured rather
+-- than refused, because the buyer is owed what they were promised.
+--
+-- (Nothing may follow the last statement here but whitespace: migrate.mjs
+-- splits on semicolons and hands each piece to libSQL, and a trailing
+-- comment-only piece comes back as the opaque `SQLITE_OK: not an error`.)
+
+-- The index goes first: SQLite refuses to drop a column an index reads.
+DROP INDEX IF EXISTS idx_moshpit_tlds_expires;
+
+ALTER TABLE moshpit_tlds DROP COLUMN expires_at;
+
+ALTER TABLE moshpit_tlds DROP COLUMN term_started_at;
diff --git a/apps/pwa/src/moshpit.mjs b/apps/pwa/src/moshpit.mjs
index e347b0f..727f0d1 100644
--- a/apps/pwa/src/moshpit.mjs
+++ b/apps/pwa/src/moshpit.mjs
@@ -1403,13 +1403,27 @@ export function summarizeBulkClaim(result, limit = MAX_BULK_TLDS) {
return parts.length ? parts.join(". ") + "." : "nothing to claim — paste one ending per line.";
}
-/* ---- buying and renewing an ending ---- */
+/* ---- buying an ending ---- */
-/** A term is a year. Ten is the ceiling PRD 0005 R6 puts on one checkout. */
-export const TERM_MS = 365 * 24 * 60 * 60 * 1000;
-export const MAX_TERM_YEARS = 10;
-
-const TLD_COLS_FULL = `tld, user_id, owner_email, alias_of, price_usd, term_started_at, expires_at, created_at`;
+/**
+ * Endings are sold once and held for good.
+ *
+ * They used to carry a one-year term with renewals, per PRD 0005 §5. That is
+ * withdrawn: $5 buys `.eggs` outright, $2 buys a name under one, and neither
+ * ever comes up for renewal. The prices are unchanged -- what changed is that
+ * they are paid once.
+ *
+ * The reason is not generosity, it is what the namespace is for. A name that
+ * lapses is a name somebody else can catch, and the whole pitch here is that
+ * you can finally have the clean name instead of the hyphenated one you settled
+ * for. An annual invoice with a drop date attached is the thing people are
+ * trying to get away from, and selling it back to them undoes the pitch.
+ *
+ * The term columns are gone (migration 016). The purchase ledger keeps its
+ * `kind` and `years` columns, because those record what was actually sold at
+ * the time and a financial record is not something to rewrite after the fact.
+ */
+const TLD_COLS_FULL = `tld, user_id, owner_email, alias_of, price_usd, created_at`;
export async function getTldWithTerm(tld) {
return get(`SELECT ${TLD_COLS_FULL} FROM moshpit_tlds WHERE tld = ?`, [tld]);
@@ -1423,18 +1437,13 @@ export async function getTldWithTerm(tld) {
* reserved, already held, and "you already own it" are three different answers
* and a single "unavailable" would be none of them.
*/
-export async function quoteTld({ tld: tldInput, buyerId, years = 1, now = Date.now() }) {
+export async function quoteTld({ tld: tldInput, buyerId, now = Date.now() }) {
const tld = normalizeTld(tldInput);
if (!tld) return { ok: false, error: "not a valid TLD — letters, digits and dashes only, no dots" };
const why = tldRejection(tld);
if (why) return { ok: false, error: why };
- const term = Number(years);
- if (!Number.isInteger(term) || term < 1 || term > MAX_TERM_YEARS) {
- return { ok: false, error: `a term is 1 to ${MAX_TERM_YEARS} years` };
- }
-
const owner = await getTldWithTerm(tld);
if (owner) {
if (owner.user_id === buyerId) return { ok: false, error: `.${tld} is already yours`, taken: true };
@@ -1450,46 +1459,40 @@ export async function quoteTld({ tld: tldInput, buyerId, years = 1, now = Date.n
);
if (held) return { ok: false, error: `.${tld} is in someone's checkout right now — try again shortly`, taken: true };
- return { ok: true, tld, years: term, priceUsd: Math.round(ENDING_PRICE_USD * term * 100) / 100 };
+ // Not multiplied by anything. There is one price and one purchase, and a
+ // quote that still carried a term would be an offer the checkout cannot make.
+ return { ok: true, tld, priceUsd: ENDING_PRICE_USD };
}
-/** What it costs to keep one you hold. */
-export async function quoteRenewal({ tld: tldInput, userId, years = 1 }) {
- const tld = normalizeTld(tldInput);
- if (!tld) return { ok: false, error: "not a valid TLD" };
-
- const term = Number(years);
- if (!Number.isInteger(term) || term < 1 || term > MAX_TERM_YEARS) {
- return { ok: false, error: `a term is 1 to ${MAX_TERM_YEARS} years` };
- }
-
- const owner = await getTldWithTerm(tld);
- if (!owner) return { ok: false, error: `.${tld} is not registered` };
- if (owner.user_id !== userId) return { ok: false, error: `you do not own .${tld}` };
-
- return { ok: true, tld, years: term, priceUsd: Math.round(ENDING_PRICE_USD * term * 100) / 100, expiresAt: owner.expires_at };
-}
-
-export async function openTldPurchase({ paymentId, tld, userId, amountUsd, years = 1, kind = "register", now = Date.now() }) {
+/**
+ * Open a checkout for an ending.
+ *
+ * `kind` and `years` are written as the constants they now always are rather
+ * than dropped from the INSERT: the columns are the ledger's, and a row that
+ * left them NULL would be indistinguishable from one written before they
+ * existed. Every ending sold from here on is one registration, held for good.
+ */
+export async function openTldPurchase({ paymentId, tld, userId, amountUsd, now = Date.now() }) {
await run(
`INSERT INTO moshpit_tld_purchases (id, tld, user_id, amount_usd, kind, status, years, created_at, reserved_until)
- VALUES (?,?,?,?,?, 'pending', ?,?,?)`,
- [paymentId, tld, userId, amountUsd, kind, years, now, now + RESERVATION_MS],
+ VALUES (?,?,?,?, 'register', 'pending', 1, ?,?)`,
+ [paymentId, tld, userId, amountUsd, now, now + RESERVATION_MS],
);
}
/**
- * Hand over a paid-for ending, or extend one. Idempotent on the payment id.
+ * Hand over a paid-for ending. Idempotent on the payment id.
*
* The claim is a conditional UPDATE for the same reason every other settlement
* here uses one: CoinPay retries a webhook it never got an ack for, so two
* deliveries can be in flight at once and both read 'pending' before either
* write lands.
*
- * A renewal never shortens a term. It extends from whichever is later — the
- * current expiry or now — so renewing early adds to what is left rather than
- * throwing it away, and renewing late does not backdate the new term into the
- * past. PRD 0005 R7.
+ * A 'renew' row can no longer be created, but one may still arrive here: a
+ * checkout opened before endings went lifetime can settle after. It is honoured
+ * as what the buyer was actually promised -- they hold the ending, and it now
+ * holds for good -- rather than refused for naming a kind this code no longer
+ * sells. Refusing it would take money for nothing.
*/
export async function settleTldPurchase(paymentId, now = Date.now()) {
const p = await get(`SELECT * FROM moshpit_tld_purchases WHERE id = ? AND status = 'pending'`, [paymentId]);
@@ -1499,8 +1502,6 @@ export async function settleTldPurchase(paymentId, now = Date.now()) {
`UPDATE moshpit_tld_purchases SET status = 'settling' WHERE id = ? AND status = 'pending'`, [paymentId]);
if (!claimed.rowsAffected) return { ok: false, error: "already settled" };
- const span = TERM_MS * (p.years || 1);
-
if (p.kind === "renew") {
const owner = await getTldWithTerm(p.tld);
if (!owner || owner.user_id !== p.user_id) {
@@ -1508,19 +1509,17 @@ export async function settleTldPurchase(paymentId, now = Date.now()) {
console.error(`[moshpit] .${p.tld} left ${p.user_id} before renewal ${paymentId} settled — refund due`);
return { ok: false, error: "ending changed hands before the renewal settled", refundDue: true };
}
- const from = Math.max(owner.expires_at || 0, now);
- await run(`UPDATE moshpit_tlds SET expires_at = ? WHERE tld = ? AND user_id = ?`,
- [from + span, p.tld, p.user_id]);
+ // Nothing to extend any more. The ending is already theirs for good.
await run(`UPDATE moshpit_tld_purchases SET status = 'cleared' WHERE id = ?`, [paymentId]);
- await logAction(p.tld, p.user_id, `renew:${p.years}y`);
- return { ok: true, tld: p.tld, userId: p.user_id, expiresAt: from + span, renewed: true };
+ await logAction(p.tld, p.user_id, `renew:lifetime`);
+ return { ok: true, tld: p.tld, userId: p.user_id, renewed: true, lifetime: true };
}
try {
await run(
- `INSERT INTO moshpit_tlds (tld, user_id, owner_email, owner_key, created_at, term_started_at, expires_at)
- VALUES (?,?,?,?,?,?,?)`,
- [p.tld, p.user_id, null, null, now, now, now + span],
+ `INSERT INTO moshpit_tlds (tld, user_id, owner_email, owner_key, created_at)
+ VALUES (?,?,?,?,?)`,
+ [p.tld, p.user_id, null, null, now],
);
} catch {
// Claimed by someone else between checkout and confirmation. Real money
@@ -1532,7 +1531,7 @@ export async function settleTldPurchase(paymentId, now = Date.now()) {
await run(`UPDATE moshpit_tld_purchases SET status = 'cleared' WHERE id = ?`, [paymentId]);
await logAction(p.tld, p.user_id, `bought:.${p.tld}`);
- return { ok: true, tld: p.tld, userId: p.user_id, expiresAt: now + span };
+ return { ok: true, tld: p.tld, userId: p.user_id, lifetime: true };
}
export async function listTldPurchases(userId, limit = 50) {
@@ -1544,14 +1543,16 @@ export async function listTldPurchases(userId, limit = 50) {
}
/**
- * Is this ending inside its term?
+ * Kept, and it always answers no.
*
- * A NULL expiry is not expired. Every ending claimed before terms existed has
- * one, and treating "no term recorded" as "term ended" would expire a few
- * hundred namespaces that nobody agreed to put on a clock.
+ * Endings do not expire any more. This stays as a named answer rather than
+ * being deleted because "does this ending still belong to its holder" is a
+ * question callers are entitled to keep asking -- the CLI, the DNS bridge and
+ * the resolvers all reasonably might -- and the honest reply is now a permanent
+ * no rather than a missing export that fails at import time.
*/
-export function isExpired(tld, now = Date.now()) {
- return Boolean(tld?.expires_at) && tld.expires_at <= now;
+export function isExpired() {
+ return false;
}
/* ---- short links: /f/ ---- */
diff --git a/apps/pwa/src/routes/moshpit.mjs b/apps/pwa/src/routes/moshpit.mjs
index 600c98c..094bf38 100644
--- a/apps/pwa/src/routes/moshpit.mjs
+++ b/apps/pwa/src/routes/moshpit.mjs
@@ -64,7 +64,9 @@ import {
countSearchTlds,
countTldsNotOwnedBy,
createLink,
+ CHILD_PRICE_USD,
DEFAULT_TLD_PRICE_USD,
+ ENDING_PRICE_USD,
deleteContent,
deleteLink,
getContent,
@@ -1868,6 +1870,12 @@ const PIT_CSS = `
.pit-forsale{border-color:color-mix(in srgb,var(--acid) 35%,var(--line))}
.pit-tab .count{font-size:.68rem;color:var(--faint);margin-left:6px}
.pit-tab.on .count{color:var(--acid)}
+/* The one claim worth making above the fold, so it reads as a fact about the
+ namespace rather than as a banner. Bordered on one side only — a full box
+ here would sit next to the error and success boxes and be mistaken for one. */
+.pit-forever{border-left:2px solid var(--acid);padding:2px 0 2px 14px;margin:18px 0 0;max-width:62ch}
+.pit-forever b{color:var(--acid);font-weight:600}
+.pit-forever .mono{color:var(--acid)}
.pit-msg{border-radius:8px;padding:10px 14px;margin:14px 0;font-family:var(--mono);font-size:.84rem}
.pit-msg.err{border:1px solid var(--danger);color:var(--danger)}
.pit-msg.ok{border:1px solid var(--acid);color:var(--acid)}
@@ -2457,6 +2465,12 @@ moshpitRouter.get("/pit", async (req, res) => {
foo.agentic resolve to foo.agent — while any name
you exempt stays exactly where it is.
+ Bought once. Yours for good.
+ $${ENDING_PRICE_USD} an ending, $${CHILD_PRICE_USD} a name —
+ paid one time, not every year. Nothing here renews, nothing lapses, and no name you hold can drop
+ because an invoice went to an address you stopped reading.
+
Endings somebody else holds. Where the operator has set a price you can buy a name under it — foo.whatever without owning .whatever. Paid in crypto - through CoinPay; the name lands the moment the payment confirms. + through CoinPay; the name lands the moment the payment confirms, and it is yours from then on — + there is no renewal and no expiry date.
${theirsHtml} ${pager({ diff --git a/apps/pwa/test/moshpit-terms.test.mjs b/apps/pwa/test/moshpit-terms.test.mjs index b6f45dd..708b849 100644 --- a/apps/pwa/test/moshpit-terms.test.mjs +++ b/apps/pwa/test/moshpit-terms.test.mjs @@ -32,18 +32,24 @@ test("ending terms", { skip: installed ? false : "pwa dependencies not installed const uniq = () => `e${randomBytes(4).toString("hex")}`; const pay = () => `pay-${randomBytes(6).toString("hex")}`; - await t.test("an unclaimed ending quotes at the ending price", async () => { + await t.test("an unclaimed ending quotes at the ending price, once", async () => { const q = await m.quoteTld({ tld: uniq(), buyerId: ALICE }); assert.equal(q.ok, true); assert.equal(q.priceUsd, 5, "PRD 0005 §10.1, rounded to whole dollars"); - assert.equal(q.years, 1); + assert.equal(q.years, undefined, "there is no term to quote"); }); - await t.test("multiple years multiply, up to the cap", async () => { - assert.equal((await m.quoteTld({ tld: uniq(), buyerId: ALICE, years: 3 })).priceUsd, 15); - assert.equal((await m.quoteTld({ tld: uniq(), buyerId: ALICE, years: 11 })).ok, false); - assert.equal((await m.quoteTld({ tld: uniq(), buyerId: ALICE, years: 0 })).ok, false); - assert.equal((await m.quoteTld({ tld: uniq(), buyerId: ALICE, years: 1.5 })).ok, false); + await t.test("there is no term to buy more of", async () => { + // A quantity used to multiply the price. Passing one now is not an error + // that needs naming, it is a field nothing reads -- what matters is that it + // cannot quietly produce a different price. + assert.equal((await m.quoteTld({ tld: uniq(), buyerId: ALICE, years: 3 })).priceUsd, 5); + assert.equal((await m.quoteTld({ tld: uniq(), buyerId: ALICE, years: 11 })).priceUsd, 5); + // And renewing is gone entirely rather than left as a no-op somebody could + // still wire a checkout to. + assert.equal(typeof m.quoteRenewal, "undefined"); + assert.equal(typeof m.MAX_TERM_YEARS, "undefined"); + assert.equal(typeof m.TERM_MS, "undefined"); }); await t.test("every refusal names itself", async () => { @@ -72,7 +78,7 @@ test("ending terms", { skip: installed ? false : "pwa dependencies not installed assert.equal((await m.quoteTld({ tld, buyerId: BOB })).ok, true); }); - await t.test("settling hands it over with a term", async () => { + await t.test("settling hands it over for good", async () => { const tld = uniq(); const id = pay(); const now = Date.now(); @@ -80,11 +86,26 @@ test("ending terms", { skip: installed ? false : "pwa dependencies not installed const result = await m.settleTldPurchase(id, now); assert.equal(result.ok, true); + assert.equal(result.lifetime, true); const row = await m.getTldWithTerm(tld); assert.equal(row.user_id, ALICE); - assert.equal(row.term_started_at, now); - assert.equal(row.expires_at, now + m.TERM_MS, "one year"); + // The columns are gone, not merely unset: a NULL expiry somebody could + // later populate is an annual term waiting to be switched back on. + assert.equal("expires_at" in row, false); + assert.equal("term_started_at" in row, false); + }); + + await t.test("the ledger still records what was sold", async () => { + const tld = uniq(); + const id = pay(); + await m.openTldPurchase({ paymentId: id, tld, userId: ALICE, amountUsd: 5 }); + await m.settleTldPurchase(id); + + const row = (await m.listTldPurchases(ALICE, 50)).find((r) => r.id === id); + assert.equal(row.kind, "register"); + assert.equal(row.years, 1); + assert.equal(row.status, "cleared"); }); await t.test("a redelivered webhook does not settle twice", async () => { @@ -113,56 +134,50 @@ test("ending terms", { skip: installed ? false : "pwa dependencies not installed assert.ok(row, "the purchase is still on Alice's record"); }); - await t.test("renewing extends, and never shortens", async () => { + await t.test("a renewal opened before the change is honoured, not refunded", async () => { + // Nothing can create one of these any more, but a checkout opened before + // endings went lifetime may still settle afterwards. The buyer is owed what + // they were promised -- they keep the ending, and it now keeps itself. const tld = uniq(); - const now = Date.now(); - const first = pay(); - await m.openTldPurchase({ paymentId: first, tld, userId: ALICE, amountUsd: 5, now }); - await m.settleTldPurchase(first, now); - - // Renewing early adds to what is left rather than throwing it away. - const second = pay(); - await m.openTldPurchase({ paymentId: second, tld, userId: ALICE, amountUsd: 5, kind: "renew", now }); - await m.settleTldPurchase(second, now + 1000); + await m.registerTld({ tld, userId: ALICE }); + const id = pay(); + await run( + `INSERT INTO moshpit_tld_purchases (id,tld,user_id,amount_usd,kind,status,years,created_at,reserved_until) + VALUES (?,?,?,?, 'renew', 'pending', 1, ?, ?)`, + [id, tld, ALICE, 5, Date.now(), Date.now() + 60_000], + ); - assert.equal((await m.getTldWithTerm(tld)).expires_at, now + m.TERM_MS * 2); + const result = await m.settleTldPurchase(id); + assert.equal(result.ok, true, result.error); + assert.equal(result.renewed, true); + assert.equal(result.lifetime, true); + assert.equal((await m.getTldWithTerm(tld)).user_id, ALICE); + assert.equal((await m.listTldPurchases(ALICE, 50)).find((r) => r.id === id).status, "cleared"); }); - await t.test("renewing a lapsed term runs from now, not from the past", async () => { + await t.test("a renewal for an ending that changed hands is still a refund", async () => { const tld = uniq(); - const past = Date.now() - m.TERM_MS * 2; + await m.registerTld({ tld, userId: BOB }); + const id = pay(); await run( - `INSERT INTO moshpit_tlds (tld,user_id,created_at,term_started_at,expires_at) VALUES (?,?,?,?,?)`, - [tld, ALICE, past, past, past + m.TERM_MS], + `INSERT INTO moshpit_tld_purchases (id,tld,user_id,amount_usd,kind,status,years,created_at,reserved_until) + VALUES (?,?,?,?, 'renew', 'pending', 1, ?, ?)`, + [id, tld, ALICE, 5, Date.now(), Date.now() + 60_000], ); - const id = pay(); - const now = Date.now(); - await m.openTldPurchase({ paymentId: id, tld, userId: ALICE, amountUsd: 5, kind: "renew", now }); - await m.settleTldPurchase(id, now); - - assert.equal((await m.getTldWithTerm(tld)).expires_at, now + m.TERM_MS, "not backdated"); - }); - - await t.test("only the holder may renew", async () => { - const tld = uniq(); - await m.registerTld({ tld, userId: ALICE }); - assert.match((await m.quoteRenewal({ tld, userId: BOB })).error, /do not own/); - assert.equal((await m.quoteRenewal({ tld, userId: ALICE })).ok, true); + const result = await m.settleTldPurchase(id); + assert.equal(result.ok, false); + assert.equal(result.refundDue, true); }); - await t.test("an ending with no term recorded is not expired", async () => { - // Every ending claimed before terms existed has a NULL expiry. Treating - // that as expired would put a few hundred namespaces on a clock nobody - // agreed to. + await t.test("nothing expires any more", async () => { const tld = uniq(); await m.registerTld({ tld, userId: ALICE }); - const row = await m.getTldWithTerm(tld); - - assert.equal(row.expires_at, null); - assert.equal(m.isExpired(row), false); - assert.equal(m.isExpired({ expires_at: Date.now() - 1 }), true); - assert.equal(m.isExpired({ expires_at: Date.now() + 1000 }), false); + assert.equal(m.isExpired(await m.getTldWithTerm(tld)), false); + // Including for a row that still carries a date from somewhere. The answer + // is a permanent no, not a comparison against a column that no longer runs. + assert.equal(m.isExpired({ expires_at: Date.now() - 1 }), false); + assert.equal(m.isExpired(), false); }); });