Automatically keeps the Expiration Date on each domain in JSM Assets in sync with the real expiration date from the domain’s registrar. Today it reads GoDaddy; the registrar layer is pluggable so more registrars drop in later.
Source (read) → registrar APIs (GoDaddy) Target (write) → JSM Assets objects, matched by domain name
Registrar maps to a configured provider. Everything
else is reported and left untouched.Every run writes a JSON log, a markdown report, and a styled standalone HTML report next to the script (all gitignored). On a live run it also publishes a monthly page to Confluence and attaches the HTML report to it (see Confluence publishing).
domain_expiry_sync/
sync-domain-expiry.js # orchestrator: fetch → load → join → dry-run/write → report → publish
lib/
http.js # shared https helper (matches repo idiom)
dates.js # tz-safe calendar-date compare/format (unit-tested)
assets.js # JSM Assets client with runtime schema self-discovery
report_html.js # styled standalone HTML report (the attached file)
report_confluence.js # same report rendered as Confluence storage format
confluence.js # Confluence REST client (create/update page, attach file)
registrars/
index.js # provider registry + registrar-name matching
godaddy.js # GoDaddy provider (multi-account)
rdap.js # keyless RDAP fallback for registrars with no API
Some registrars — Network Solutions is the notable one here — offer no customer API, so there’s no key to get. Instead of leaving those domains unsynced, the sync falls back to RDAP (RFC 9083), the registry’s keyless, structured successor to WHOIS. Every gTLD registry is required by ICANN to serve it, so we read the real expiration date straight from the registry regardless of who the registrar is.
.com / .net / .org and most gTLDs resolve reliably. Some ccTLDs (e.g.
.ai) don’t serve RDAP — those stay flagged, unchanged.RDAP_MAX caps lookups per run (default
200). Set RDAP_FALLBACK=false to disable.To add a real API for a registrar later (better than RDAP where one exists),
drop a lib/registrars/<name>.js exporting { key, label, aliases, fetch,
isConfigured } and list it in index.js — the orchestrator is unchanged.
Use an Atlassian account with write permission on the Assets object schema.
Create a token at https://id.atlassian.com/manage-profile/security/api-tokens
(same token type as okta_audit_log’s CONFLUENCE_API_TOKEN — it can be the
same token if that account has Assets write access).
GoDaddy restricts API access: production keys only return data for an account holding 10+ domains (you qualify). Have the account owner (Anthony) generate, for each GoDaddy account that holds domains:
Test/OTE keys from the same page point at
api.ote-godaddy.comand won’t see production domains — make sure they’re Production keys.
cd integrations/domain_expiry_sync
npm install
cp .env.example .env # then fill it in
Put every GoDaddy account into GODADDY_CREDENTIALS as a JSON array:
[
{"account":"56054964","key":"KEY_1","secret":"SECRET_1"},
{"account":"12345678","key":"KEY_2","secret":"SECRET_2"}
]
(For local runs you can instead drop that array into a gitignored
godaddy-accounts.json beside the script.)
Because this writes to production Assets, walk it up carefully:
# 1. Dry run. This also confirms the self-discovered schema is correct.
npm run dry-run
ASSETS_* var in .env
and re-run. (Defaults are guessed from the JSM screenshot:
schema Domain Name Services, object type Domain Name Accounts.)sync-report-*.md.# 2. Write just a couple of objects and eyeball them in Jira first.
node sync-domain-expiry.js --limit 2
ASSETS_DATE_FORMAT=ISO in .env and retry.# 3. Once you trust it, run the whole set.
npm run run-live
| Flag | Effect |
|---|---|
--dry-run |
Print the diff, write nothing. |
--limit N |
Cap the number of live updates (safety for first runs). |
--registrar godaddy |
Only process one provider’s assets. |
On a live run (never on a dry run), the sync creates or updates one page per
calendar month and attaches the styled HTML report to it — mirroring how
okta_audit_log keeps its monthly pages. This is what the IT and Helpdesk
directors read; the page is live, searchable, and permission-controlled by
Confluence.
CONFLUENCE_PARENT_ID is the folder id from its URL).YYYY-MM Domain Expiry Report (e.g. 2026-08 Domain Expiry Report).
Re-running the same month updates that page in place — no duplicates.ATLASSIAN_EMAIL / ATLASSIAN_API_TOKEN (that token already
has Confluence write scope). Publishing is best-effort: a Confluence error
logs a warning and never fails the JSM sync, which has already succeeded.Written against the Confluence v2 API on purpose — v2’s parentId accepts a
folder as the parent, which the older v1 ancestors field does not.
Set CONFLUENCE_PUBLISH=false to disable, or unset CONFLUENCE_SPACE_KEY /
CONFLUENCE_PARENT_ID to skip publishing without failing. See .env.example.
.github/workflows/domain_expiry_sync.yml runs on the 1st of each month: it
writes the live updates to JSM, publishes the monthly Confluence page, and also
uploads the report as a build artifact (90-day retention). Manual runs via
workflow_dispatch default to dry run (which skips both writes and Confluence).
Add these repository secrets (Settings → Secrets and variables → Actions):
ATLASSIAN_EMAILATLASSIAN_API_TOKENGODADDY_CREDENTIALS (the JSON array above, as a single secret)Non-secret config (base URL, schema/attribute names, date format, and the
Confluence space key + parent folder id) is set as plain env: in the
workflow — adjust there if the .env overrides changed.
lib/dates.js, registrar matching, attribute
reads) and timezone-safe. The live API call shapes (Assets AQL/PUT,
GoDaddy list pagination) follow the documented endpoints but have not yet
been exercised against the live tenant — validate on the first dry run per
the steps above. This is expected to surface exactly two things to confirm:
the real schema/attribute display names, and the Date attribute’s accepted
format.lib/registrars/<name>.js implementing
{ key, label, aliases, fetch, isConfigured } and list it in index.js.
The “Unsupported / blank registrar” section of the report ranks which
registrars cover the most remaining domains, so you can prioritize.outage_slack_ping’s
pattern) for domains expiring within N days.