openvibers/openvibe.sources Commits

Seed ovhcloud-domain-pricing: OVHcloud's public domain catalog (950 endings, first year, renewal and transfer in US dollars), a second registrar for OpenVibe.Host's domain comparison; robots and API schema read 2026-10-22, seeded disabled (#86)

OP OpenVibers committed 22 days ago · 51678810e7 · 3 files, +637 −0

Changes

ddiff --git a/seeds/sources.json b/seeds/sources.json
iindex 6c128ba..0999a8f 100644
---- a/seeds/sources.json
++++ b/seeds/sources.json
@@@ -1207,6 +1207,38 @@
"sensitivity": "none",
"default_indexability": "noindex"
},
++ {
++ "key": "ovhcloud-domain-pricing",
++ "name": "OVHcloud domain prices (every TLD its international store sells, in US dollars)",
++ "type": "api",
++ "category": "hosting",
++ "homepage_url": "https://www.ovhcloud.com/en/domains/tld/",
++ "endpoints": [{
++ "url": "https://api.ca.ovhcloud.com/1.0/order/catalog/public/domain?ovhSubsidiary=WE",
++ "format": "json",
++ "items_path": "plans",
++ "item_kind": "tld_price",
++ "max_bytes": 33554432,
++ "timeout_ms": 120000,
++ "fields": { "identity": "planCode", "title": "invoiceName" },
++ "extra": {
++ "currency": "$root.locale.currencyCode",
++ "register": "pricings[mode=create-default,phase=0].price",
++ "renew": "pricings[mode=create-default,phase=1].price",
++ "transfer": "pricings[mode=transfer-default,phase=0].price"
++ }
++ }],
++ "robots_note": "robots.txt checked 2026-10-22 from the production host: https://api.ca.ovhcloud.com/robots.txt answers 404 (nginx), so the API host publishes no robots.txt rules; a 404 is treated as no restrictions (never guessed as a disallow). ca.api.ovh.com/robots.txt redirects there. The fetcher re-checks robots.txt on every run. (www.ovhcloud.com, the store, is a different host and is not fetched.)",
++ "terms_note": "OVHcloud's own API schema (https://api.ca.ovhcloud.com/1.0/order.json, read 2026-10-22) marks GET /order/catalog/public/domain 'noAuthentication': true with apiStatus 'Stable production version' ('Retrieve domain catalog'): the public catalog is meant to be read without an account, as the VPS catalog OpenVibe.Host already reads (ovhcloud-vps) is. The WE subsidiary is OVHcloud's international English store, priced in US dollars with no tax; api.us.ovhcloud.com does not sell domains ('product not found'). The terms page (https://www.ovhcloud.com/en/terms-and-conditions/contracts/) lists the service contracts; none found states a right to, or a ban on, republishing catalog prices, the same finding as ovhcloud-vps. One file a day of about 30 MB (950 endings with every price variant). Prices are the catalog's integers in hundred-millionths of a dollar (920000000 is $9.20): create-default phase 0 is the first year, phase 1 the renewal, transfer-default phase 0 the transfer; premium and aftermarket modes are not read. A first-year price of 0 (an ending sold only with conditions) or the 999999 sentinel means not sold that way, and OpenVibe.Host leaves those out. Show each price as an observation 'checked on <date>' with a link to https://www.ovhcloud.com/en/domains/tld/, never a quote, and imply no endorsement.",
++ "license_note": "Domain prices are OVHcloud's data (https://www.ovhcloud.com/en/domains/tld/). No licence to republish them was found beyond the public catalog being offered without authentication. Attribute OVHcloud and link its domain price page; no endorsement implied.",
++ "min_interval_ms": 60000,
++ "poll_interval_sec": 86400,
++ "max_items": 1200,
++ "enabled": false,
++ "review_required": true,
++ "sensitivity": "none",
++ "default_indexability": "noindex"
++ },
{
"key": "lowendtalk-offers",
"name": "LowEndTalk: the Offers board",
ddiff --git a/test/fixtures/ovhcloud-domain-pricing.json b/test/fixtures/ovhcloud-domain-pricing.json
nnew file mode 100644
iindex 0000000..fb18440
---- /dev/null
++++ b/test/fixtures/ovhcloud-domain-pricing.json
@@@ -0,0 +1,586 @@
++{
++ "catalogId": 9886,
++ "locale": {
++ "currencyCode": "USD",
++ "subsidiary": "WE",
++ "taxRate": 0
++ },
++ "plans": [
++ {
++ "planCode": "family",
++ "invoiceName": ".family",
++ "product": "family-product",
++ "pricings": [
++ {
++ "capacities": [
++ "installation",
++ "renew"
++ ],
++ "commitment": 0,
++ "description": "Creation of a .family domain name - 1 year",
++ "interval": 12,
++ "intervalUnit": "month",
++ "mode": "create-default",
++ "phase": 0,
++ "price": 3069000000,
++ "strategy": "tiered",
++ "type": "rental"
++ },
++ {
++ "capacities": [
++ "renew"
++ ],
++ "commitment": 0,
++ "description": "Renewal of a .family domain name - 1 year",
++ "interval": 12,
++ "intervalUnit": "month",
++ "mode": "create-default",
++ "phase": 1,
++ "price": 3859000000,
++ "strategy": "tiered",
++ "type": "rental"
++ },
++ {
++ "capacities": [
++ "dynamic",
++ "installation",
++ "renew"
++ ],
++ "commitment": 0,
++ "description": "Creation of a .family domain name - 1 year",
++ "interval": 12,
++ "intervalUnit": "month",
++ "mode": "create-premium",
++ "phase": 0,
++ "price": 99999900000000,
++ "strategy": "tiered",
++ "type": "rental"
++ },
++ {
++ "capacities": [
++ "dynamic",
++ "renew"
++ ],
++ "commitment": 0,
++ "description": "Renewal of a .family domain name - 1 year",
++ "interval": 12,
++ "intervalUnit": "month",
++ "mode": "create-premium",
++ "phase": 1,
++ "price": 99999900000000,
++ "strategy": "tiered",
++ "type": "rental"
++ },
++ {
++ "capacities": [
++ "installation",
++ "renew"
++ ],
++ "commitment": 0,
++ "description": "Transfer request of a .family domain name",
++ "interval": 12,
++ "intervalUnit": "month",
++ "mode": "transfer-default",
++ "phase": 0,
++ "price": 3789000000,
++ "strategy": "tiered",
++ "type": "rental"
++ },
++ {
++ "capacities": [
++ "renew"
++ ],
++ "commitment": 0,
++ "description": "Renewal of a .family domain name - 1 year",
++ "interval": 12,
++ "intervalUnit": "month",
++ "mode": "transfer-default",
++ "phase": 1,
++ "price": 3859000000,
++ "strategy": "tiered",
++ "type": "rental"
++ }
++ ]
++ },
++ {
++ "planCode": "io",
++ "invoiceName": ".io",
++ "product": "io-product",
++ "pricings": [
++ {
++ "capacities": [
++ "installation",
++ "renew"
++ ],
++ "commitment": 0,
++ "description": "Creation of a .io domain name - 1 year",
++ "interval": 12,
++ "intervalUnit": "month",
++ "mode": "create-default",
++ "phase": 0,
++ "price": 6299000000,
++ "strategy": "tiered",
++ "type": "rental"
++ },
++ {
++ "capacities": [
++ "renew"
++ ],
++ "commitment": 0,
++ "description": "Renewal of a .io domain name - 1 year",
++ "interval": 12,
++ "intervalUnit": "month",
++ "mode": "create-default",
++ "phase": 1,
++ "price": 6269000000,
++ "strategy": "tiered",
++ "type": "rental"
++ },
++ {
++ "capacities": [
++ "dynamic",
++ "installation",
++ "renew"
++ ],
++ "commitment": 0,
++ "description": "Creation of a .io domain name - 1 year",
++ "interval": 12,
++ "intervalUnit": "month",
++ "mode": "create-premium",
++ "phase": 0,
++ "price": 99999900000000,
++ "strategy": "tiered",
++ "type": "rental"
++ },
++ {
++ "capacities": [
++ "dynamic",
++ "renew"
++ ],
++ "commitment": 0,
++ "description": "Renewal of a .io domain name - 1 year",
++ "interval": 12,
++ "intervalUnit": "month",
++ "mode": "create-premium",
++ "phase": 1,
++ "price": 99999900000000,
++ "strategy": "tiered",
++ "type": "rental"
++ },
++ {
++ "capacities": [
++ "installation",
++ "renew"
++ ],
++ "commitment": 0,
++ "description": "Transfer request of a .io domain name",
++ "interval": 12,
++ "intervalUnit": "month",
++ "mode": "transfer-default",
++ "phase": 0,
++ "price": 5069000000,
++ "strategy": "tiered",
++ "type": "rental"
++ },
++ {
++ "capacities": [
++ "renew"
++ ],
++ "commitment": 0,
++ "description": "Renewal of a .io domain name - 1 year",
++ "interval": 12,
++ "intervalUnit": "month",
++ "mode": "transfer-default",
++ "phase": 1,
++ "price": 6269000000,
++ "strategy": "tiered",
++ "type": "rental"
++ }
++ ]
++ },
++ {
++ "planCode": "shop",
++ "invoiceName": ".shop",
++ "product": "shop-product",
++ "pricings": [
++ {
++ "capacities": [
++ "installation",
++ "renew"
++ ],
++ "commitment": 0,
++ "description": "Creation of a .shop domain name - 1 year",
++ "interval": 12,
++ "intervalUnit": "month",
++ "mode": "create-default",
++ "phase": 0,
++ "price": 319000000,
++ "strategy": "tiered",
++ "type": "rental"
++ },
++ {
++ "capacities": [
++ "renew"
++ ],
++ "commitment": 0,
++ "description": "Renewal of a .shop domain name - 1 year",
++ "interval": 12,
++ "intervalUnit": "month",
++ "mode": "create-default",
++ "phase": 1,
++ "price": 4299000000,
++ "strategy": "tiered",
++ "type": "rental"
++ },
++ {
++ "capacities": [
++ "dynamic",
++ "installation",
++ "renew"
++ ],
++ "commitment": 0,
++ "description": "Creation of a .shop domain name - 1 year",
++ "interval": 12,
++ "intervalUnit": "month",
++ "mode": "create-premium",
++ "phase": 0,
++ "price": 99999900000000,
++ "strategy": "tiered",
++ "type": "rental"
++ },
++ {
++ "capacities": [
++ "dynamic",
++ "renew"
++ ],
++ "commitment": 0,
++ "description": "Renewal of a .shop domain name - 1 year",
++ "interval": 12,
++ "intervalUnit": "month",
++ "mode": "create-premium",
++ "phase": 1,
++ "price": 99999900000000,
++ "strategy": "tiered",
++ "type": "rental"
++ },
++ {
++ "capacities": [
++ "installation",
++ "renew"
++ ],
++ "commitment": 0,
++ "description": "Transfer request of a .shop domain name",
++ "interval": 12,
++ "intervalUnit": "month",
++ "mode": "transfer-default",
++ "phase": 0,
++ "price": 3429000000,
++ "strategy": "tiered",
++ "type": "rental"
++ },
++ {
++ "capacities": [
++ "renew"
++ ],
++ "commitment": 0,
++ "description": "Renewal of a .shop domain name - 1 year",
++ "interval": 12,
++ "intervalUnit": "month",
++ "mode": "transfer-default",
++ "phase": 1,
++ "price": 4299000000,
++ "strategy": "tiered",
++ "type": "rental"
++ }
++ ]
++ },
++ {
++ "planCode": "co.uk",
++ "invoiceName": ".co.uk",
++ "product": "co-uk-product",
++ "pricings": [
++ {
++ "capacities": [
++ "installation",
++ "renew"
++ ],
++ "commitment": 0,
++ "description": "Creation of a .co.uk domain name - 1 year",
++ "interval": 12,
++ "intervalUnit": "month",
++ "mode": "create-default",
++ "phase": 0,
++ "price": 849000000,
++ "strategy": "tiered",
++ "type": "rental"
++ },
++ {
++ "capacities": [
++ "renew"
++ ],
++ "commitment": 0,
++ "description": "Renewal of a .co.uk domain name - 1 year",
++ "interval": 12,
++ "intervalUnit": "month",
++ "mode": "create-default",
++ "phase": 1,
++ "price": 949000000,
++ "strategy": "tiered",
++ "type": "rental"
++ },
++ {
++ "capacities": [
++ "dynamic",
++ "installation",
++ "renew"
++ ],
++ "commitment": 0,
++ "description": "Creation of a .co.uk domain name - 1 year",
++ "interval": 12,
++ "intervalUnit": "month",
++ "mode": "create-premium",
++ "phase": 0,
++ "price": 99999900000000,
++ "strategy": "tiered",
++ "type": "rental"
++ },
++ {
++ "capacities": [
++ "dynamic",
++ "renew"
++ ],
++ "commitment": 0,
++ "description": "Renewal of a .co.uk domain name - 1 year",
++ "interval": 12,
++ "intervalUnit": "month",
++ "mode": "create-premium",
++ "phase": 1,
++ "price": 99999900000000,
++ "strategy": "tiered",
++ "type": "rental"
++ },
++ {
++ "capacities": [
++ "installation",
++ "renew"
++ ],
++ "commitment": 0,
++ "description": "Transfer request of a .co.uk domain name",
++ "interval": 12,
++ "intervalUnit": "month",
++ "mode": "transfer-default",
++ "phase": 0,
++ "price": 0,
++ "strategy": "tiered",
++ "type": "rental"
++ },
++ {
++ "capacities": [
++ "renew"
++ ],
++ "commitment": 0,
++ "description": "Renewal of a .co.uk domain name - 1 year",
++ "interval": 12,
++ "intervalUnit": "month",
++ "mode": "transfer-default",
++ "phase": 1,
++ "price": 949000000,
++ "strategy": "tiered",
++ "type": "rental"
++ }
++ ]
++ },
++ {
++ "planCode": "dev",
++ "invoiceName": ".dev",
++ "product": "dev-product",
++ "pricings": [
++ {
++ "capacities": [
++ "installation",
++ "renew"
++ ],
++ "commitment": 0,
++ "description": "Creation of a .dev domain name - 1 year",
++ "interval": 12,
++ "intervalUnit": "month",
++ "mode": "create-default",
++ "phase": 0,
++ "price": 1319000000,
++ "strategy": "tiered",
++ "type": "rental"
++ },
++ {
++ "capacities": [
++ "renew"
++ ],
++ "commitment": 0,
++ "description": "Renewal of a .dev domain name - 1 year",
++ "interval": 12,
++ "intervalUnit": "month",
++ "mode": "create-default",
++ "phase": 1,
++ "price": 1719000000,
++ "strategy": "tiered",
++ "type": "rental"
++ },
++ {
++ "capacities": [
++ "dynamic",
++ "installation",
++ "renew"
++ ],
++ "commitment": 0,
++ "description": "Creation of a .dev domain name - 1 year",
++ "interval": 12,
++ "intervalUnit": "month",
++ "mode": "create-premium",
++ "phase": 0,
++ "price": 99999900000000,
++ "strategy": "tiered",
++ "type": "rental"
++ },
++ {
++ "capacities": [
++ "dynamic",
++ "renew"
++ ],
++ "commitment": 0,
++ "description": "Renewal of a .dev domain name - 1 year",
++ "interval": 12,
++ "intervalUnit": "month",
++ "mode": "create-premium",
++ "phase": 1,
++ "price": 99999900000000,
++ "strategy": "tiered",
++ "type": "rental"
++ },
++ {
++ "capacities": [
++ "installation",
++ "renew"
++ ],
++ "commitment": 0,
++ "description": "Transfer request of a .dev domain name",
++ "interval": 12,
++ "intervalUnit": "month",
++ "mode": "transfer-default",
++ "phase": 0,
++ "price": 1719000000,
++ "strategy": "tiered",
++ "type": "rental"
++ },
++ {
++ "capacities": [
++ "renew"
++ ],
++ "commitment": 0,
++ "description": "Renewal of a .dev domain name - 1 year",
++ "interval": 12,
++ "intervalUnit": "month",
++ "mode": "transfer-default",
++ "phase": 1,
++ "price": 1719000000,
++ "strategy": "tiered",
++ "type": "rental"
++ }
++ ]
++ },
++ {
++ "planCode": "com",
++ "invoiceName": ".com",
++ "product": "com-product",
++ "pricings": [
++ {
++ "capacities": [
++ "installation",
++ "renew"
++ ],
++ "commitment": 0,
++ "description": "Creation of a .com domain name - 1 year",
++ "interval": 12,
++ "intervalUnit": "month",
++ "mode": "create-default",
++ "phase": 0,
++ "price": 920000000,
++ "strategy": "tiered",
++ "type": "rental"
++ },
++ {
++ "capacities": [
++ "renew"
++ ],
++ "commitment": 0,
++ "description": "Renewal of a .com domain name - 1 year",
++ "interval": 12,
++ "intervalUnit": "month",
++ "mode": "create-default",
++ "phase": 1,
++ "price": 1649000000,
++ "strategy": "tiered",
++ "type": "rental"
++ },
++ {
++ "capacities": [
++ "dynamic",
++ "installation",
++ "renew"
++ ],
++ "commitment": 0,
++ "description": "Creation of a .com domain name - 1 year",
++ "interval": 12,
++ "intervalUnit": "month",
++ "mode": "create-premium",
++ "phase": 0,
++ "price": 99999900000000,
++ "strategy": "tiered",
++ "type": "rental"
++ },
++ {
++ "capacities": [
++ "dynamic",
++ "renew"
++ ],
++ "commitment": 0,
++ "description": "Renewal of a .com domain name - 1 year",
++ "interval": 12,
++ "intervalUnit": "month",
++ "mode": "create-premium",
++ "phase": 1,
++ "price": 99999900000000,
++ "strategy": "tiered",
++ "type": "rental"
++ },
++ {
++ "capacities": [
++ "installation",
++ "renew"
++ ],
++ "commitment": 0,
++ "description": "Transfer request of a .com domain name",
++ "interval": 12,
++ "intervalUnit": "month",
++ "mode": "transfer-default",
++ "phase": 0,
++ "price": 920000000,
++ "strategy": "tiered",
++ "type": "rental"
++ },
++ {
++ "capacities": [
++ "renew"
++ ],
++ "commitment": 0,
++ "description": "Renewal of a .com domain name - 1 year",
++ "interval": 12,
++ "intervalUnit": "month",
++ "mode": "transfer-default",
++ "phase": 1,
++ "price": 1649000000,
++ "strategy": "tiered",
++ "type": "rental"
++ }
++ ]
++ }
++ ]
++}
\\ No newline at end of file
ddiff --git a/test/hosting-sources.test.js b/test/hosting-sources.test.js
iindex 6479d52..d13e3c8 100644
---- a/test/hosting-sources.test.js
++++ b/test/hosting-sources.test.js
@@@ -91,6 +91,25 @@ t('Porkbun prices: one item per TLD from the keyed price list, the prices as Por
assert.strictEqual(by['0zebra'].special_type, 'handshake', 'Handshake names are marked, for Host to leave out');
});
++t('OVHcloud domain prices: one item per ending, the first year, renewal and transfer from the no-premium price modes', () => {
++ const s = seed('ovhcloud-domain-pricing');
++ assert.deepStrictEqual([s.type, s.category, s.enabled, s.review_required], ['api', 'hosting', false, true]);
++ const ep = endpoint('ovhcloud-domain-pricing');
++ assert.strictEqual(api.checkMapping(ep), null);
++ assert.strictEqual(validateSource(s).max_items, 1200, 'OVHcloud sells about 950 endings; the whole list fits');
++ assert.strictEqual(ep.max_bytes, 32 * 1024 * 1024, 'the whole catalog is about 30 MB');
++ const r = api.parse(fx('ovhcloud-domain-pricing.json'), { url: ep.url, endpoint: ep, maxItems: 1200 });
++ assert.strictEqual(r.skipped, 0);
++ assert.deepStrictEqual(r.items.map((i) => i.identity), ['family', 'io', 'shop', 'co.uk', 'dev', 'com']);
++ assert.ok(r.items.every((i) => i.kind === 'tld_price'));
++ assert.strictEqual(r.items.find((i) => i.identity === 'co.uk').title, '.co.uk');
++ const by = Object.fromEntries(r.items.map((i) => [i.identity, i.fields]));
++ // Hundred-millionths of a dollar, as the catalog gives them: $9.20 the first year, $16.49 a renewal.
++ assert.deepStrictEqual(by.com, { currency: 'USD', register: 920000000, renew: 1649000000, transfer: 920000000 });
++ assert.deepStrictEqual(by.io, { currency: 'USD', register: 6299000000, renew: 6269000000, transfer: 5069000000 }, 'never the premium sentinel');
++ assert.strictEqual(by['co.uk'].transfer, 0, 'a free transfer stays 0; Host decides what 0 means per field');
++});
++
t('the registry accepts category hosting and refuses an unknown category', () => {
const rec = validateSource(seed('vultr-plans'));
assert.strictEqual(rec.category, 'hosting');