apiV4Js constant
String
const apiV4Js
API v4 console JavaScript code
Implementation
static const String apiV4Js = r'''(function () {
const REGISTRAR_ENVS = {
dev: 'https://my.atsign.wtf',
prod: 'https://my.atsign.com'
};
const STATUS_ENVS = {
dev: 'https://directory.atsign.wtf',
prod: 'https://wavi.ng'
};
const REQUEST_TIMEOUT_MS = 15000;
const activationPollers = new Map();
const PRIMARY_ENDPOINTS = [
{
id: 'lookup-atsign-v4',
title: 'Lookup an Atsign',
method: 'POST',
baseType: 'registrar',
path: '/api/app/v4/register-atsign/',
authRequired: true,
params: [
{
name: 'atSign',
label: 'Atsign',
required: false,
type: 'text',
placeholder: 'meow01',
help: 'Optional. Do not include the prefix/postfix. The prefix/postfix is already implied by the Authorization API Key. So for example, entering "test01" as your Atsign may provision "test01_np". Do not include the @ symbol.'
},
{ name: 'operation', required: true, type: 'select', options: ['lookup'], defaultValue: 'lookup', disabled: true, help: 'Required' },
{ name: 'startAtServer', required: false, type: 'select', options: ['true', 'false'], defaultValue: 'true', help: 'Optional' }
]
},
{
id: 'register-atsign-v4',
title: 'Register an Atsign',
method: 'POST',
baseType: 'registrar',
path: '/api/app/v4/register-atsign/',
authRequired: true,
params: [
{
name: 'atSign',
label: 'Atsign',
required: false,
type: 'text',
placeholder: 'meow01',
help: 'Optional. Do not include the prefix/postfix. The prefix/postfix is already implied by the Authorization API Key. So for example, entering "test01" as your Atsign may provision "test01_np". Do not include the @ symbol.'
},
{ name: 'operation', required: true, type: 'select', options: ['register'], defaultValue: 'register', disabled: true, help: 'Required' },
{ name: 'startAtServer', required: false, type: 'select', options: ['true', 'false'], defaultValue: 'true', help: 'Optional' }
]
},
{
id: 'register-atsign-v4-batch',
title: 'Register an Atsign (v4) (BATCH)',
method: 'POST',
baseType: 'registrar',
path: '/api/app/v4/register-atsign/',
authRequired: true,
type: 'batch-register',
params: [
{
name: 'atSigns',
label: 'Atsigns',
required: true,
type: 'textarea',
placeholder: 'meow01\nmeow02\nmeow03',
help: 'One Atsign per line. Do not include the prefix/postfix. The prefix/postfix is already implied by the Authorization API Key. So for example, entering "test01", "test02", and "test03" as your Atsigns will provision "test01_np", "test02_np", and "test03_np". Do not include the @ symbol.'
},
{ name: 'startAtServer', required: false, type: 'select', options: ['true', 'false'], defaultValue: 'true', help: 'Optional' }
]
},
{
id: 'reset-atsign',
title: 'Reset an Atsign',
method: 'POST',
baseType: 'registrar',
path: '/api/app/v4/reset-atsign/',
authRequired: true,
params: [
{ name: 'atSign', label: 'Atsign', required: true, type: 'text', placeholder: 'meow01_jttest', help: 'Required. Include the full prefix/postfix. For example, if your postfix is _jttest, enter meow01_jttest. This intentionally differs from the other Atsign endpoints.' }
]
},
{
id: 'delete-atsigns',
title: 'Delete Atsigns',
method: 'POST',
baseType: 'registrar',
path: '/api/app/v4/manage-atsigns',
authRequired: true,
type: 'delete-atsigns',
params: [
{
name: 'atSigns',
label: 'Atsigns',
required: true,
type: 'textarea',
placeholder: 'meow01_jttest\nmeow02_jttest',
help: 'Required. One Atsign per line. Include the full prefix/postfix, the same way the reset endpoint does. The Authorization API Key can only delete Atsigns it created, so anything else comes back as skipped. Do not include the @ symbol. A name that has already been deleted once is refused on later deletes, so avoid reusing one.'
}
]
}
];
const OTHER_ENDPOINTS = [
{
id: 'get-free-atsign-get',
title: 'Get Free Atsign',
method: 'GET',
baseType: 'registrar',
path: '/api/app/v4/get-free-atsign/',
authRequired: true,
params: []
},
{
id: 'get-free-atsign-post',
title: 'Get Free Atsign by Category',
method: 'POST',
baseType: 'registrar',
path: '/api/app/v4/get-free-atsign/',
authRequired: true,
params: [
{ name: 'category', required: false, type: 'csv-array', placeholder: 'animals,movies', help: 'Comma-separated list' }
]
},
{
id: 'register-person',
title: 'Register Person',
method: 'POST',
baseType: 'registrar',
path: '/api/app/v4/register-person/',
authRequired: true,
params: [
{ name: 'atsign', label: 'Atsign', required: true, type: 'text', placeholder: 'wisefrog', help: 'Generated free Atsign' },
{ name: 'email', required: true, type: 'text', placeholder: 'user@example.com', help: 'Required' },
{ name: 'oldEmail', required: false, type: 'text', placeholder: 'old@example.com', help: 'Optional' }
]
},
{
id: 'validate-person',
title: 'Validate Person',
method: 'POST',
baseType: 'registrar',
path: '/api/app/v4/validate-person/',
authRequired: true,
params: [
{ name: 'atsign', label: 'Atsign', required: true, type: 'text', placeholder: 'wisefrog', help: 'Required' },
{ name: 'email', required: true, type: 'text', placeholder: 'user@example.com', help: 'Required' },
{ name: 'otp', required: true, type: 'text', placeholder: 'AB12', help: '4-character code' },
{ name: 'confirmation', required: false, type: 'boolean', placeholder: 'false', help: 'Optional, defaults to false' }
]
},
{
id: 'authenticate-person-atsign',
title: 'Authenticate Person with Atsign',
method: 'POST',
baseType: 'registrar',
path: '/api/app/v4/authenticate/atsign',
authRequired: true,
params: [
{ name: 'atsign', label: 'Atsign', required: true, type: 'text', placeholder: '@alice', help: 'Registered Atsign' }
]
},
{
id: 'validate-person-atsign-otp',
title: 'Validate Person with Atsign and OTP',
method: 'POST',
baseType: 'registrar',
path: '/api/app/v4/authenticate/atsign/activate',
authRequired: true,
params: [
{ name: 'atsign', label: 'Atsign', required: true, type: 'text', placeholder: '@alice', help: 'Registered Atsign' },
{ name: 'otp', required: true, type: 'text', placeholder: 'AB12', help: '4-character code' }
]
},
{
id: 'get-atsign-v3',
title: 'Get Atsign and ActivationKey (v3)',
method: 'POST',
baseType: 'registrar',
path: '/api/app/v3/get-atsign/',
authRequired: true,
params: [
{ name: 'atSign', label: 'Atsign', required: false, type: 'text', placeholder: 'embergarden', help: 'Optional' },
{ name: 'ActivationKey', required: false, type: 'text', placeholder: '4V3GbA4u', help: 'Optional' }
]
},
{
id: 'get-atsign-v4',
title: 'Get Atsign and ActivationKey (v4)',
method: 'POST',
baseType: 'registrar',
path: '/api/app/v4/get-atsign/',
authRequired: true,
params: [
{ name: 'atSign', label: 'Atsign', required: false, type: 'text', placeholder: 'emberly', help: 'Optional' },
{ name: 'ActivationKey', required: false, type: 'text', placeholder: '4V3GbA4u', help: 'Optional' }
]
},
{
id: 'activate-atsign',
title: 'Activate an Atsign',
method: 'POST',
baseType: 'registrar',
path: '/api/app/v4/activate-atsign/',
authRequired: false,
params: [
{ name: 'atSign', label: 'Atsign', required: true, type: 'text', placeholder: '@alice', help: 'Required' },
{ name: 'activationKey', required: true, type: 'text', placeholder: '4V3GbA4u', help: 'Required' }
]
},
{
id: 'register-atsign-v3',
title: 'Register an Atsign (v3)',
method: 'POST',
baseType: 'registrar',
path: '/api/app/v3/register-atsign/',
authRequired: true,
params: [
{ name: 'atSign', label: 'Atsign', required: false, type: 'text', placeholder: 'mydog_01', help: 'Optional' },
{ name: 'operation', required: true, type: 'select', options: ['lookup', 'register'], help: 'Required' }
]
}
];
const ALL_ENDPOINTS = [...PRIMARY_ENDPOINTS, ...OTHER_ENDPOINTS];
function envOptions(baseType) {
const source = baseType === 'status' ? STATUS_ENVS : REGISTRAR_ENVS;
return Object.keys(source)
.map((key) => `<option value="${key}" ${key === 'prod' ? 'selected' : ''}>${key} - ${source[key]}</option>`)
.join('');
}
function resolveBase(baseType, env) {
return baseType === 'status' ? STATUS_ENVS[env] : REGISTRAR_ENVS[env];
}
function paramInput(param) {
if (param.type === 'select') {
return `
<select data-param-name="${param.name}" ${param.disabled ? 'disabled' : ''}>
<option value=""></option>
${param.options.map((opt) => `<option value="${opt}" ${param.defaultValue === opt ? 'selected' : ''}>${opt}</option>`).join('')}
</select>
`;
}
if (param.type === 'boolean') {
return `
<select data-param-name="${param.name}">
<option value=""></option>
<option value="true">true</option>
<option value="false">false</option>
</select>
`;
}
if (param.type === 'textarea') {
return `<textarea data-param-name="${param.name}" rows="8" placeholder="${param.placeholder || ''}"></textarea>`;
}
return `<input type="text" data-param-name="${param.name}" placeholder="${param.placeholder || ''}" />`;
}
function requiredSummary(endpoint) {
const required = endpoint.params.filter((p) => p.required).map((p) => p.name);
return required.length ? `Required parameters: ${required.join(', ')}` : 'Required parameters: none';
}
function normalizeParamHelpText(helpText) {
if (!helpText) {
return '';
}
return String(helpText).replace(/^(required|optional)\.?\s*/i, '').trim();
}
function endpointMarkup(endpoint) {
return `
<div class="api-v4-row" data-endpoint-id="${endpoint.id}">
<button type="button" class="api-v4-row-head" data-role="toggle" aria-expanded="false">
<div>
<div class="api-v4-row-title">${endpoint.title}</div>
<div class="api-v4-param-summary">${requiredSummary(endpoint)}</div>
</div>
<div class="api-v4-row-route">
<span class="api-v4-method api-v4-method-${endpoint.method.toLowerCase()}">${endpoint.method}</span>
<code>${endpoint.path}</code>
<span class="api-v4-toggle-indicator" aria-hidden="true"></span>
</div>
</button>
<div class="api-v4-row-body" data-role="body" hidden>
<div class="api-v4-param-grid">
<label class="api-v4-field">
<span>Environment</span>
<select data-role="env">${envOptions(endpoint.baseType)}</select>
</label>
<label class="api-v4-field">
<span>Authorization API Key ${endpoint.authRequired ? '<small>(required)</small>' : '<small>(not required for this call)</small>'}</span>
<input type="password" data-role="api-key" placeholder="Your registrar API key" ${endpoint.authRequired ? '' : ''} />
</label>
${endpoint.params.map((param) => `
<label class="api-v4-field">
<span>${param.label || param.name} <small>${param.required ? 'required' : 'optional'}${normalizeParamHelpText(param.help) ? ` ยท ${normalizeParamHelpText(param.help)}` : ''}</small></span>
${paramInput(param)}
</label>
`).join('')}
</div>
<div class="api-v4-inline-warning" data-role="warning" hidden></div>
<div class="api-v4-actions">
<button type="button" data-role="send">Send</button>
<button type="button" class="api-v4-secondary" data-role="clear">Clear Output</button>
</div>
<div class="api-v4-result" data-role="result" hidden></div>
<div class="api-v4-batch-results" data-role="batch-results" hidden></div>
<div class="api-v4-status" data-role="status">Ready</div>
<pre class="api-v4-output" data-role="output"></pre>
</div>
</div>
`;
}
function otherSectionMarkup() {
if (OTHER_ENDPOINTS.length === 0) {
return '';
}
return `
<details class="api-v4-section">
<summary>Other</summary>
<div class="api-v4-section-body">
${OTHER_ENDPOINTS.map(endpointMarkup).join('')}
</div>
</details>
`;
}
function parseJsonObject(value, fieldName) {
const trimmed = value.trim();
if (!trimmed) return {};
try {
const parsed = JSON.parse(trimmed);
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
return parsed;
}
throw new Error(`${fieldName} must be a JSON object`);
} catch (err) {
throw new Error(`Invalid ${fieldName}: ${err.message}`);
}
}
function collectPayload(row, endpoint) {
const payload = {};
const warning = row.querySelector('[data-role="warning"]');
if (warning) {
warning.hidden = true;
warning.textContent = '';
}
for (const param of endpoint.params) {
const el = row.querySelector(`[data-param-name="${param.name}"]`);
let raw = (el ? el.value : '').trim();
if (!raw) {
if (param.required) {
throw new Error(`Missing required parameter: ${param.name}`);
}
continue;
}
if ((endpoint.id === 'register-atsign-v4-batch' || endpoint.id === 'delete-atsigns') && param.name === 'atSigns') {
const normalizedValues = raw
.split('\n')
.map((value) => normalizeAtSignForRequest(value.trim()))
.filter(Boolean);
payload[param.name] = normalizedValues;
if (el) {
el.value = normalizedValues.join('\n');
}
if (warning && normalizedValues.some((value, index) => value !== raw.split('\n').map((entry) => entry.trim()).filter(Boolean)[index])) {
warning.textContent = 'Warning: leading @ was removed from one or more Atsigns before sending.';
warning.hidden = false;
}
continue;
}
if (
(
endpoint.id === 'register-atsign-v4' ||
endpoint.id === 'reset-atsign'
) &&
param.name === 'atSign' &&
raw.startsWith('@')
) {
raw = raw.substring(1);
if (el) {
el.value = raw;
}
if (warning) {
warning.textContent = 'Warning: leading @ was removed from Atsign before sending.';
warning.hidden = false;
}
}
if (param.type === 'csv-array') {
payload[param.name] = raw.split(',').map((v) => v.trim()).filter(Boolean);
} else if (param.type === 'boolean') {
payload[param.name] = raw === 'true';
} else {
payload[param.name] = raw;
}
}
return payload;
}
function buildUrl(endpoint, env, payload) {
let path = endpoint.path;
if (path.includes('{atSign}')) {
path = path.replace('{atSign}', encodeURIComponent(payload.atSign || payload.atsign || ''));
delete payload.atSign;
}
return `${resolveBase(endpoint.baseType, env)}${path}`;
}
function renderPrimarySummary(row, endpoint, parsedBody) {
const result = row.querySelector('[data-role="result"]');
if (!result) return;
if (
!PRIMARY_ENDPOINTS.some((item) => item.id === endpoint.id) ||
endpoint.id === 'register-atsign-v4-batch' ||
!parsedBody ||
typeof parsedBody !== 'object'
) {
result.hidden = true;
result.innerHTML = '';
return;
}
const apiStatus = parsedBody.status === 'success' ? 'success' : 'error';
const badgeLabel = apiStatus === 'success' ? 'Success' : 'Error';
let summaryText = parsedBody.message || 'Request completed.';
if (endpoint.id === 'register-atsign-v4') {
if (apiStatus === 'success' && parsedBody.cramkey) {
summaryText = 'Atsign registered successfully.';
} else if (parsedBody.message) {
summaryText = parsedBody.message;
}
}
if (endpoint.id === 'lookup-atsign-v4' && parsedBody.message) {
summaryText = parsedBody.message;
}
if (endpoint.id === 'reset-atsign' && parsedBody.message) {
summaryText = parsedBody.message;
}
const cramkeyBlock = parsedBody.cramkey
? `<div class="api-v4-cramkey-block"><div class="api-v4-cramkey-label">cramkey</div><code>${escapeHtml(parsedBody.cramkey)}</code></div>`
: '';
result.innerHTML = `
<div class="api-v4-result-card api-v4-result-${apiStatus}">
<div class="api-v4-result-head">
<span class="api-v4-result-badge api-v4-result-badge-${apiStatus}">${badgeLabel}</span>
<span class="api-v4-result-text">${escapeHtml(summaryText)}</span>
</div>
${cramkeyBlock}
</div>
`;
result.hidden = false;
}
function escapeHtml(value) {
return String(value)
.replaceAll('&', '&')
.replaceAll('<', '<')
.replaceAll('>', '>')
.replaceAll('"', '"')
.replaceAll("'", ''');
}
function extractAtSignFromCramkey(cramkey) {
if (!cramkey || typeof cramkey !== 'string') {
return '';
}
const separatorIndex = cramkey.indexOf(':');
return separatorIndex >= 0 ? cramkey.substring(0, separatorIndex) : cramkey;
}
function extractCramKeyValue(cramkey) {
if (!cramkey || typeof cramkey !== 'string') {
return '';
}
const separatorIndex = cramkey.indexOf(':');
return separatorIndex >= 0 ? cramkey.substring(separatorIndex + 1) : cramkey;
}
function parseRequestBody(data) {
const request = data && data.request ? data.request : {};
const body = request.body;
if (!body || typeof body !== 'string') {
return {};
}
try {
const parsed = JSON.parse(body);
return parsed && typeof parsed === 'object' ? parsed : {};
} catch (_) {
return {};
}
}
function transactionTypeLabel(type) {
if (type === 'register-atsign') {
return 'Register an Atsign';
}
if (type === 'reset-atsign') {
return 'Reset an Atsign';
}
if (type === 'delete-atsign') {
return 'Delete Atsigns';
}
return type || 'Transaction';
}
function summarizeTransaction(item) {
const data = item.data || {};
const request = data.request || {};
const response = data.response || {};
const responseBody = response.body || {};
const requestBody = parseRequestBody(data);
const cramkey = responseBody.cramkey || '';
const cramAtSign = extractAtSignFromCramkey(cramkey);
const requestedAtSign = requestBody.atSign ? `@${String(requestBody.atSign).replace(/^@/, '')}` : '';
const requestedAtSigns = Array.isArray(requestBody.atSigns)
? requestBody.atSigns.map((value) => `@${String(value).replace(/^@/, '')}`).join(', ')
: '';
const label = cramAtSign || requestedAtSign || requestedAtSigns || item.filename || 'Transaction';
const apiStatus = responseBody.status || (response.ok ? 'success' : 'error');
const message = responseBody.message || '';
const createdAt = data.createdAtUtc || '';
return {
data,
request,
response,
responseBody,
cramkey,
label,
apiStatus,
message,
createdAt,
typeLabel: transactionTypeLabel(data.type)
};
}
function renderActivationCramKeys(items) {
const list = document.getElementById('activation-list');
if (!list) return;
if (!items || items.length === 0) {
list.innerHTML = '<div class="api-v4-transaction-card"><div class="api-v4-transaction-meta">No pending CRAM key files found.</div></div>';
return;
}
list.innerHTML = items.map((item) => {
const data = item.data || {};
const atSign = data.atSign || item.filename || 'Unknown';
const cramKey = data.cramKey || '';
const createdAt = data.createdAtUtc || '';
const sourceTransactionPath = data.sourceTransactionPath || '';
return `
<details class="api-v4-transaction-card">
<summary class="api-v4-transaction-summary">
<div class="api-v4-transaction-title-wrap">
<div class="api-v4-transaction-title">${escapeHtml(atSign)}</div>
</div>
<div class="api-v4-transaction-summary-right">
<button type="button" class="api-v4-secondary" data-activate-cramkey="${escapeHtml(atSign)}">Activate</button>
<span class="api-v4-result-badge api-v4-result-badge-success" data-activation-badge="${escapeHtml(atSign)}">pending</span>
<span class="api-v4-transaction-created">${escapeHtml(createdAt)}</span>
</div>
</summary>
<div class="api-v4-transaction-body">
<div class="api-v4-transaction-meta"><strong>Path:</strong> ${escapeHtml(item.path || '')}</div>
${sourceTransactionPath ? `<div class="api-v4-transaction-meta"><strong>Source transaction:</strong> ${escapeHtml(sourceTransactionPath)}</div>` : ''}
<div class="api-v4-transaction-cramkey">
<code>${escapeHtml(cramKey)}</code>
<button type="button" class="api-v4-secondary" data-copy-activation-cramkey="${escapeHtml(cramKey)}">Copy CRAM Key</button>
</div>
<div class="api-v4-status" data-activation-item-status="${escapeHtml(atSign)}">Ready</div>
<div class="api-v4-transaction-meta" data-activation-command="${escapeHtml(atSign)}" hidden></div>
<pre class="api-v4-activation-log" data-activation-log="${escapeHtml(atSign)}" hidden></pre>
</div>
</details>
`;
}).join('');
list.querySelectorAll('[data-copy-activation-cramkey]').forEach((button) => {
button.addEventListener('click', async function (event) {
event.preventDefault();
event.stopPropagation();
const value = button.getAttribute('data-copy-activation-cramkey') || '';
await navigator.clipboard.writeText(value);
const originalText = button.textContent;
button.textContent = 'Copied';
setTimeout(() => {
button.textContent = originalText;
}, 1500);
});
});
list.querySelectorAll('[data-activate-cramkey]').forEach((button) => {
button.addEventListener('click', async function (event) {
event.preventDefault();
event.stopPropagation();
const atSign = button.getAttribute('data-activate-cramkey') || '';
await activateCramKey(atSign, button);
});
});
}
function renderOnboardedCramKeys(items) {
const list = document.getElementById('activation-onboarded-list');
if (!list) return;
if (!items || items.length === 0) {
list.innerHTML = '<div class="api-v4-transaction-card"><div class="api-v4-transaction-meta">No onboarded CRAM key files found.</div></div>';
return;
}
list.innerHTML = items.map((item) => {
const data = item.data || {};
const atSign = data.atSign || item.filename || 'Unknown';
const onboardedAt = data.onboardedAtUtc || '';
const sourceTransactionPath = data.sourceTransactionPath || '';
return `
<details class="api-v4-transaction-card">
<summary class="api-v4-transaction-summary">
<div class="api-v4-transaction-title-wrap">
<div class="api-v4-transaction-title">${escapeHtml(atSign)}</div>
<div class="api-v4-transaction-subtitle">Onboarded ${escapeHtml(onboardedAt || 'Unknown time')}</div>
</div>
<div class="api-v4-transaction-summary-right">
<button type="button" class="api-v4-secondary" data-open-onboarded-apkam="${escapeHtml(atSign)}">Go to APKAM Management</button>
</div>
</summary>
<div class="api-v4-transaction-body">
<div class="api-v4-transaction-meta"><strong>Path:</strong> ${escapeHtml(item.path || '')}</div>
${sourceTransactionPath ? `<div class="api-v4-transaction-meta"><strong>Source transaction:</strong> ${escapeHtml(sourceTransactionPath)}</div>` : ''}
${onboardedAt ? `<div class="api-v4-transaction-meta"><strong>Onboarded at:</strong> ${escapeHtml(onboardedAt)}</div>` : ''}
</div>
</details>
`;
}).join('');
list.querySelectorAll('[data-open-onboarded-apkam]').forEach((button) => {
button.addEventListener('click', async function (event) {
event.preventDefault();
event.stopPropagation();
const atSign = button.getAttribute('data-open-onboarded-apkam') || '';
try {
await openAtSignInApkamManagement(atSign);
} catch (err) {
const status = document.getElementById('activation-status');
if (status) status.textContent = `Failed to open APKAM Management: ${err.message}`;
}
});
});
}
function renderTransactions(transactions) {
const list = document.getElementById('api-v4-transactions-list');
if (!list) return;
if (!transactions || transactions.length === 0) {
list.innerHTML = '<div class="api-v4-transaction-card"><div class="api-v4-transaction-meta">No transaction files found.</div></div>';
return;
}
list.innerHTML = transactions.map((item) => {
const summary = summarizeTransaction(item);
const badgeClass = summary.apiStatus === 'success' ? 'success' : 'error';
const title = summary.label;
const subtitle = summary.message || summary.typeLabel;
return `
<details class="api-v4-transaction-card">
<summary class="api-v4-transaction-summary">
<div class="api-v4-transaction-title-wrap">
<div class="api-v4-transaction-title">${escapeHtml(title)}</div>
<div class="api-v4-transaction-subtitle">${escapeHtml(subtitle)}</div>
</div>
<div class="api-v4-transaction-summary-right">
<span class="api-v4-result-badge api-v4-result-badge-${badgeClass}">${escapeHtml(summary.apiStatus)}</span>
<span class="api-v4-transaction-created">${escapeHtml(summary.createdAt)}</span>
</div>
</summary>
<div class="api-v4-transaction-body">
<div class="api-v4-transaction-meta"><strong>Type:</strong> ${escapeHtml(summary.typeLabel)}</div>
<div class="api-v4-transaction-meta"><strong>URL:</strong> ${escapeHtml(summary.request.url || '')}</div>
<div class="api-v4-transaction-meta"><strong>Path:</strong> ${escapeHtml(item.path || '')}</div>
${summary.message ? `<div class="api-v4-transaction-meta"><strong>Message:</strong> ${escapeHtml(summary.message)}</div>` : ''}
${summary.cramkey ? `
<div class="api-v4-transaction-cramkey">
<code>${escapeHtml(summary.cramkey)}</code>
<button type="button" class="api-v4-secondary" data-copy-transaction-cramkey="${escapeHtml(extractCramKeyValue(summary.cramkey))}">Copy CRAM Key</button>
</div>
` : ''}
<details class="api-v4-transaction-raw">
<summary>Raw JSON</summary>
<pre class="api-v4-transaction-json">${escapeHtml(JSON.stringify(summary.data, null, 2))}</pre>
</details>
</div>
</details>
`;
}).join('');
list.querySelectorAll('[data-copy-transaction-cramkey]').forEach((button) => {
button.addEventListener('click', async function (event) {
event.preventDefault();
event.stopPropagation();
const value = button.getAttribute('data-copy-transaction-cramkey') || '';
await navigator.clipboard.writeText(value);
const originalText = button.textContent;
button.textContent = 'Copied';
setTimeout(() => {
button.textContent = originalText;
}, 1500);
});
});
}
async function loadApiV4Transactions() {
const status = document.getElementById('api-v4-transactions-status');
const list = document.getElementById('api-v4-transactions-list');
if (!status || !list) return;
status.textContent = 'Loading transactions...';
list.innerHTML = '';
try {
const response = await fetch('/api/transactions');
const data = await response.json();
if (!response.ok || data.error) {
throw new Error(data.error || 'Failed to load transactions');
}
renderTransactions(data.transactions || []);
status.textContent = `Loaded ${data.transactions ? data.transactions.length : 0} transaction files`;
} catch (err) {
status.textContent = `Load failed: ${err.message}`;
list.innerHTML = '<div class="api-v4-transaction-card"><div class="api-v4-transaction-meta">Failed to load transactions.</div></div>';
}
}
async function openTransactionsFolder() {
const status = document.getElementById('api-v4-transactions-status') || document.getElementById('api-v4-save-status');
if (status) status.textContent = 'Opening transactions folder...';
try {
const response = await fetch('/api/transactions/open-folder', {
method: 'POST'
});
const data = await response.json();
if (!response.ok || data.error) {
throw new Error(data.error || 'Failed to open transactions folder');
}
if (status) status.textContent = `Opened transactions folder: ${data.path}`;
} catch (err) {
if (status) status.textContent = `Open failed: ${err.message}`;
}
}
async function loadActivationCramKeys() {
const status = document.getElementById('activation-status');
const list = document.getElementById('activation-list');
const onboardedList = document.getElementById('activation-onboarded-list');
if (!status || !list || !onboardedList) return;
status.textContent = 'Loading CRAM key files...';
list.innerHTML = '';
onboardedList.innerHTML = '';
try {
const [pendingResponse, archivedResponse] = await Promise.all([
fetch('/api/cramkeys'),
fetch('/api/cramkeys/archived')
]);
const pendingData = await pendingResponse.json();
const archivedData = await archivedResponse.json();
if (!pendingResponse.ok || pendingData.error) {
throw new Error(pendingData.error || 'Failed to load CRAM key files');
}
if (!archivedResponse.ok || archivedData.error) {
throw new Error(archivedData.error || 'Failed to load onboarded CRAM key files');
}
renderActivationCramKeys(pendingData.cramKeys || []);
renderOnboardedCramKeys(archivedData.cramKeys || []);
status.textContent = `Loaded ${pendingData.cramKeys ? pendingData.cramKeys.length : 0} pending and ${archivedData.cramKeys ? archivedData.cramKeys.length : 0} onboarded CRAM key files`;
} catch (err) {
status.textContent = `Load failed: ${err.message}`;
list.innerHTML = '<div class="api-v4-transaction-card"><div class="api-v4-transaction-meta">Failed to load CRAM key files.</div></div>';
onboardedList.innerHTML = '<div class="api-v4-transaction-card"><div class="api-v4-transaction-meta">Failed to load onboarded CRAM key files.</div></div>';
}
}
async function openCramKeysFolder() {
const status = document.getElementById('activation-status');
if (status) status.textContent = 'Opening cramkeys folder...';
try {
const response = await fetch('/api/cramkeys/open-folder', {
method: 'POST'
});
const data = await response.json();
if (!response.ok || data.error) {
throw new Error(data.error || 'Failed to open cramkeys folder');
}
if (status) status.textContent = `Opened cramkeys folder: ${data.path}`;
} catch (err) {
if (status) status.textContent = `Open failed: ${err.message}`;
}
}
async function activateCramKey(atSign, button) {
const itemStatus = document.querySelector(`[data-activation-item-status="${CSS.escape(atSign)}"]`);
const badgeEl = document.querySelector(`[data-activation-badge="${CSS.escape(atSign)}"]`);
const commandEl = document.querySelector(`[data-activation-command="${CSS.escape(atSign)}"]`);
const logEl = document.querySelector(`[data-activation-log="${CSS.escape(atSign)}"]`);
if (button) {
button.disabled = true;
}
if (itemStatus) {
itemStatus.textContent = 'Activating...';
}
if (badgeEl) {
badgeEl.textContent = 'running';
badgeEl.classList.remove('api-v4-result-badge-neutral');
badgeEl.classList.add('api-v4-result-badge-success');
}
if (commandEl) {
commandEl.hidden = true;
commandEl.textContent = '';
}
if (logEl) {
logEl.hidden = true;
logEl.textContent = '';
}
try {
const response = await fetch('/api/cramkeys/activate/start', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ atSign })
});
const data = await response.json();
if (!response.ok || data.error) {
throw new Error(data.error || 'Activation failed');
}
if (commandEl) {
commandEl.hidden = false;
commandEl.innerHTML = `<strong>Command:</strong> <code>${escapeHtml(data.command || '')}</code>`;
}
if (logEl) {
logEl.hidden = false;
logEl.textContent = 'Activation started...\n';
}
await pollActivationJob(data.jobId, data.atSign, button);
} catch (err) {
if (itemStatus) {
itemStatus.textContent = `Activation failed: ${err.message}`;
}
if (badgeEl) {
badgeEl.textContent = 'error';
badgeEl.classList.remove('api-v4-result-badge-success', 'api-v4-result-badge-neutral');
badgeEl.classList.add('api-v4-result-badge-error');
}
if (button) {
button.disabled = false;
}
} finally {
}
}
async function pollActivationJob(jobId, atSign, button) {
const itemStatus = document.querySelector(`[data-activation-item-status="${CSS.escape(atSign)}"]`);
const badgeEl = document.querySelector(`[data-activation-badge="${CSS.escape(atSign)}"]`);
const logEl = document.querySelector(`[data-activation-log="${CSS.escape(atSign)}"]`);
const renderJob = function (job) {
if (logEl) {
const stdout = Array.isArray(job.stdout) ? job.stdout : [];
const stderr = Array.isArray(job.stderr) ? job.stderr : [];
const lines = [];
if (stdout.length) {
lines.push(...stdout.map((line) => `[stdout] ${line}`));
}
if (stderr.length) {
lines.push(...stderr.map((line) => `[stderr] ${line}`));
}
logEl.hidden = false;
logEl.textContent = lines.length ? lines.join('\n') : 'No process output yet...';
}
if (job.status === 'running') {
if (itemStatus) {
itemStatus.textContent = 'Running...';
}
return false;
}
if (job.status === 'success') {
if (itemStatus) {
itemStatus.textContent = `Success. Archived to ${job.archivedPath || 'old/'}`;
}
if (badgeEl) {
badgeEl.textContent = 'Onboarded';
badgeEl.classList.remove('api-v4-result-badge-success', 'api-v4-result-badge-error');
badgeEl.classList.add('api-v4-result-badge-neutral');
}
loadActivationCramKeys();
} else {
if (itemStatus) {
itemStatus.textContent = `Failed. Exit code: ${job.exitCode}`;
}
if (badgeEl) {
badgeEl.textContent = 'error';
badgeEl.classList.remove('api-v4-result-badge-success', 'api-v4-result-badge-neutral');
badgeEl.classList.add('api-v4-result-badge-error');
}
}
if (button) {
button.disabled = false;
}
return true;
};
const runPoll = async function () {
try {
const response = await fetch(`/api/cramkeys/activate/status/${encodeURIComponent(jobId)}`);
const job = await response.json();
if (!response.ok || job.error) {
throw new Error(job.error || 'Failed to load activation status');
}
const finished = renderJob(job);
if (finished) {
clearInterval(activationPollers.get(jobId));
activationPollers.delete(jobId);
}
} catch (err) {
if (itemStatus) {
itemStatus.textContent = `Status check failed: ${err.message}`;
}
if (badgeEl) {
badgeEl.textContent = 'error';
badgeEl.classList.remove('api-v4-result-badge-success', 'api-v4-result-badge-neutral');
badgeEl.classList.add('api-v4-result-badge-error');
}
if (button) {
button.disabled = false;
}
clearInterval(activationPollers.get(jobId));
activationPollers.delete(jobId);
}
};
await runPoll();
const intervalId = setInterval(runPoll, 1000);
activationPollers.set(jobId, intervalId);
}
async function openAtSignInApkamManagement(atSign) {
const normalized = atSign.startsWith('@') ? atSign : `@${atSign}`;
const response = await fetch('/api/select-atsign', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ atsign: normalized })
});
const data = await response.json();
if (!response.ok || data.error) {
throw new Error(data.error || 'Failed to open atSign');
}
if (typeof switchTab === 'function' && typeof loadEnrollments === 'function') {
switchTab('apkam-management');
loadEnrollments();
return;
}
window.location.href = '/';
}
async function persistSuccessfulTransaction(endpoint, payload) {
const allowed = {
'register-atsign-v4': 'register-atsign',
'register-atsign-v3': 'register-atsign',
'register-atsign-v4-batch': 'register-atsign',
'reset-atsign': 'reset-atsign',
'delete-atsigns': 'delete-atsign'
};
const type = allowed[endpoint.id];
if (!type) {
return null;
}
const responseBody = payload && payload.response ? payload.response.body : null;
if (!responseBody || responseBody.status !== 'success') {
return null;
}
const response = await fetch('/api/transactions/save', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
type,
transaction: payload
})
});
const data = await response.json();
if (!response.ok || data.error) {
throw new Error(data.error || 'Failed to save register transaction');
}
if (data.cramKeyPath) {
const saveStatus = document.getElementById('api-v4-save-status');
if (saveStatus) {
saveStatus.textContent = `Saved CRAM key file: ${data.cramKeyPath}`;
}
}
return data.path || null;
}
function normalizeAtSignForRequest(value) {
return value.startsWith('@') ? value.substring(1) : value;
}
function buildRequestDetails(endpoint, env, apiKey, payload) {
const url = buildUrl(endpoint, env, payload);
const headers = {
'Content-Type': 'application/json'
};
if (endpoint.authRequired && !apiKey) {
throw new Error('Authorization API key is required for this call');
}
if (apiKey) {
headers.authorization = apiKey;
}
const requestInit = {
method: endpoint.method,
headers
};
if (endpoint.method !== 'GET') {
requestInit.body = JSON.stringify(payload);
}
return { url, headers, requestInit };
}
async function ensureInternalDartApiReady() {
let response;
try {
response = await fetch('/api/health', {
cache: 'no-store',
headers: {
'Cache-Control': 'no-cache, no-store, must-revalidate',
Pragma: 'no-cache'
}
});
} catch (_) {
throw new Error('Internal Dart service is unreachable');
}
let payload = null;
try {
payload = await response.json();
} catch (_) {
payload = null;
}
if (!response.ok || !payload || payload.status !== 'ok') {
throw new Error(payload && payload.error ? payload.error : 'Internal Dart service is unavailable');
}
}
async function executeApiRequest(endpoint, env, apiKey, payload) {
await ensureInternalDartApiReady();
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS);
const details = buildRequestDetails(endpoint, env, apiKey, payload);
details.requestInit.signal = controller.signal;
let response;
try {
response = await fetch(details.url, details.requestInit);
} finally {
clearTimeout(timeoutId);
}
const rawText = await response.text();
let parsedBody = rawText;
try {
parsedBody = rawText ? JSON.parse(rawText) : '';
} catch (_) {}
const responseHeaders = {};
response.headers.forEach((value, key) => {
responseHeaders[key] = value;
});
return {
response,
parsedBody,
resultPayload: {
request: {
environment: env,
url: details.url,
method: endpoint.method,
headers: details.headers,
body: details.requestInit.body || null
},
response: {
status: response.status,
statusText: response.statusText,
ok: response.ok,
headers: responseHeaders,
body: parsedBody
}
}
};
}
async function sendBatchRegisterRequest(row, endpoint) {
const statusOutput = row.querySelector('[data-role="status"]');
const output = row.querySelector('[data-role="output"]');
const result = row.querySelector('[data-role="result"]');
const batchResults = row.querySelector('[data-role="batch-results"]');
const button = row.querySelector('[data-role="send"]');
const warning = row.querySelector('[data-role="warning"]');
const startedAt = Date.now();
statusOutput.textContent = 'Preparing batch request...';
button.disabled = true;
output.textContent = '';
if (result) {
result.hidden = true;
result.innerHTML = '';
}
if (batchResults) {
batchResults.hidden = true;
batchResults.innerHTML = '';
}
if (warning) {
warning.hidden = true;
warning.textContent = '';
}
try {
const env = row.querySelector('[data-role="env"]').value;
const apiKey = row.querySelector('[data-role="api-key"]').value.trim();
const payload = collectPayload(row, endpoint);
const atSigns = (payload.atSigns || []).filter(Boolean);
if (atSigns.length === 0) {
throw new Error('Enter at least one Atsign for batch registration');
}
const preview = atSigns.join('\n');
const confirmed = window.confirm(`Are you really sure you want to register this list of Atsigns?\n\n${preview}`);
if (!confirmed) {
statusOutput.textContent = 'Batch request cancelled';
return;
}
if (batchResults) {
batchResults.hidden = false;
batchResults.innerHTML = atSigns
.map((atSign) => `
<div class="api-v4-batch-item" data-batch-atsign="${escapeHtml(atSign)}">
<div class="api-v4-batch-item-head">
<span class="api-v4-batch-atsign">${escapeHtml(atSign)}</span>
<span class="api-v4-batch-state">Queued</span>
</div>
<div class="api-v4-batch-message"></div>
<div class="api-v4-batch-cramkey" hidden></div>
</div>
`)
.join('');
}
const allResults = [];
let successCount = 0;
for (let index = 0; index < atSigns.length; index += 1) {
const atSign = atSigns[index];
statusOutput.textContent = `Processing ${index + 1} of ${atSigns.length}...`;
const item = batchResults
? Array.from(batchResults.querySelectorAll('[data-batch-atsign]')).find((element) => element.getAttribute('data-batch-atsign') === atSign)
: null;
if (item) {
item.querySelector('.api-v4-batch-state').textContent = 'Sending...';
}
const itemPayload = {
atSign,
operation: 'register'
};
if (payload.startAtServer) {
itemPayload.startAtServer = payload.startAtServer;
}
try {
const executed = await executeApiRequest(endpoint, env, apiKey, itemPayload);
allResults.push(executed.resultPayload);
const transactionPath = await persistSuccessfulTransaction(endpoint, executed.resultPayload);
const apiStatus = executed.parsedBody && executed.parsedBody.status === 'success' ? 'success' : 'error';
if (item) {
item.classList.add(`api-v4-batch-item-${apiStatus}`);
item.querySelector('.api-v4-batch-state').textContent = apiStatus === 'success' ? 'Success' : 'Error';
item.querySelector('.api-v4-batch-message').textContent = executed.parsedBody.message || (apiStatus === 'success' ? 'Registered successfully' : 'Request completed');
if (executed.parsedBody.cramkey) {
const cramkeyEl = item.querySelector('.api-v4-batch-cramkey');
cramkeyEl.hidden = false;
cramkeyEl.innerHTML = `
<code>${escapeHtml(executed.parsedBody.cramkey)}</code>
<button type="button" class="api-v4-secondary" data-copy-cramkey="${escapeHtml(executed.parsedBody.cramkey)}">Copy</button>
`;
}
if (transactionPath) {
item.querySelector('.api-v4-batch-message').textContent += ` | Saved: ${transactionPath}`;
}
}
if (apiStatus === 'success') {
successCount += 1;
}
} catch (err) {
allResults.push({ atSign, error: err && err.message ? err.message : String(err) });
if (item) {
item.classList.add('api-v4-batch-item-error');
item.querySelector('.api-v4-batch-state').textContent = 'Error';
item.querySelector('.api-v4-batch-message').textContent = err && err.message ? err.message : String(err);
}
}
}
if (batchResults) {
batchResults.querySelectorAll('[data-copy-cramkey]').forEach((copyBtn) => {
copyBtn.addEventListener('click', async function () {
const value = copyBtn.getAttribute('data-copy-cramkey') || '';
await navigator.clipboard.writeText(value);
copyBtn.textContent = 'Copied';
setTimeout(() => {
copyBtn.textContent = 'Copy';
}, 1500);
});
});
}
const elapsed = Date.now() - startedAt;
statusOutput.textContent = `Batch completed: ${successCount}/${atSigns.length} succeeded (${elapsed} ms). One transaction file was created per attempted Atsign.`;
output.textContent = JSON.stringify(allResults, null, 2);
} catch (err) {
const elapsed = Date.now() - startedAt;
if (err && err.name === 'AbortError') {
statusOutput.textContent = `Batch timed out after ${REQUEST_TIMEOUT_MS / 1000} seconds`;
output.textContent = 'The batch request exceeded the 15 second timeout and was cancelled.';
} else {
statusOutput.textContent = `Batch failed (${elapsed} ms)`;
output.textContent = err && err.stack ? err.stack : String(err);
}
} finally {
button.disabled = false;
}
}
function normalizeAtSignKey(value) {
return String(value || '').trim().replace(/^@/, '').toLowerCase();
}
function sameAtSign(left, right) {
return normalizeAtSignKey(left) === normalizeAtSignKey(right);
}
function extractAtSignNames(value) {
if (!Array.isArray(value)) {
return [];
}
return value
.map((item) => {
if (item && typeof item === 'object') {
return String(item.atSign || item.atsign || item.name || '');
}
return String(item || '');
})
.map((item) => item.trim())
.filter(Boolean);
}
// The registrar reports a generic top-level message and puts the real
// reason on each entry of failed/skippedatSigns, so keep those by Atsign.
function extractAtSignMessages(value) {
const messages = new Map();
if (!Array.isArray(value)) {
return messages;
}
value.forEach((item) => {
if (!item || typeof item !== 'object') {
return;
}
const name = normalizeAtSignKey(item.atSign || item.atsign || item.name || '');
const message = String(item.message || item.reason || '').trim();
if (name && message) {
messages.set(name, message);
}
});
return messages;
}
function renderDeleteItems(container, atSigns) {
if (!container) return;
container.hidden = false;
container.innerHTML = atSigns
.map((atSign) => `
<div class="api-v4-batch-item" data-batch-atsign="${escapeHtml(atSign)}">
<div class="api-v4-batch-item-head">
<span class="api-v4-batch-atsign">${escapeHtml(atSign)}</span>
<span class="api-v4-batch-state">Queued</span>
</div>
<div class="api-v4-batch-message"></div>
</div>
`)
.join('');
}
function updateDeleteItem(container, atSign, state, message) {
if (!container) return;
const item = Array.from(container.querySelectorAll('[data-batch-atsign]'))
.find((element) => sameAtSign(element.getAttribute('data-batch-atsign'), atSign));
if (!item) return;
item.classList.add(state === 'Deleted' ? 'api-v4-batch-item-success' : 'api-v4-batch-item-error');
item.querySelector('.api-v4-batch-state').textContent = state;
item.querySelector('.api-v4-batch-message').textContent = message;
}
// Deleting an Atsign destroys its atServer, which leaves any local keys for
// it useless. We report them rather than removing them: a local .atKeys file
// may be an enrollment copy rather than the primary keys, and it is matched
// only by name, so it may well belong to a different environment.
async function warnAboutStaleLocalKeys(deletedAtSigns, warning) {
if (!warning) return;
let local;
try {
const response = await fetch('/api/atsigns');
const data = await response.json();
if (!response.ok || data.error) {
throw new Error(data.error || 'Failed to list local Atsigns');
}
local = (data.atsigns || []).filter((entry) =>
deletedAtSigns.some((atSign) => sameAtSign(entry.name, atSign)));
} catch (_) {
return;
}
if (local.length === 0) return;
const services = local.reduce((total, entry) => total + (entry.activeAutoApprovalCount || 0), 0);
warning.textContent = `Local keys were found for ${local.map((entry) => entry.name).join(', ')}: `
+ `${local.map((entry) => entry.path).join(', ')}. Their atServers have just been deleted, so these `
+ 'keys are stale and we advise deleting them if they are the corresponding atKeys. This console '
+ 'leaves them in place because a local key file may be an enrollment copy, or may belong to an '
+ 'Atsign of the same name in another environment.'
+ (services > 0 ? ` ${services} auto approval service(s) are still running against them.` : '');
warning.hidden = false;
}
async function sendDeleteAtSignsRequest(row, endpoint) {
const statusOutput = row.querySelector('[data-role="status"]');
const output = row.querySelector('[data-role="output"]');
const batchResults = row.querySelector('[data-role="batch-results"]');
const button = row.querySelector('[data-role="send"]');
const warning = row.querySelector('[data-role="warning"]');
const startedAt = Date.now();
const executedPayloads = [];
clearRowOutput(row);
statusOutput.textContent = 'Preparing delete request...';
button.disabled = true;
try {
const env = row.querySelector('[data-role="env"]').value;
const apiKey = row.querySelector('[data-role="api-key"]').value.trim();
const payload = collectPayload(row, endpoint);
const atSigns = (payload.atSigns || []).filter(Boolean);
if (atSigns.length === 0) {
throw new Error('Enter at least one Atsign to delete');
}
const preview = atSigns.join('\n');
const confirmation = window.prompt(`Type DELETE to permanently delete ${atSigns.length} Atsign(s). This cannot be undone.\n\n${preview}`, '');
if (confirmation === null) {
statusOutput.textContent = 'Delete cancelled';
output.textContent = 'Delete request cancelled before sending.';
return;
}
if (confirmation.trim() !== 'DELETE') {
throw new Error('Confirmation did not match. Type exactly: DELETE');
}
renderDeleteItems(batchResults, atSigns);
statusOutput.textContent = 'Requesting delete token...';
const tokenExecuted = await executeApiRequest(endpoint, env, apiKey, {
atSigns,
operation: 'deletetoken'
});
executedPayloads.push(tokenExecuted.resultPayload);
const tokenBody = tokenExecuted.parsedBody && typeof tokenExecuted.parsedBody === 'object' ? tokenExecuted.parsedBody : {};
const tokenData = tokenBody.data && typeof tokenBody.data === 'object' ? tokenBody.data : {};
const skipped = extractAtSignNames(tokenData.skippedatSigns);
const skippedMessages = extractAtSignMessages(tokenData.skippedatSigns);
const token = tokenData.token ? String(tokenData.token) : '';
skipped.forEach((atSign) => {
updateDeleteItem(batchResults, atSign, 'Skipped',
skippedMessages.get(normalizeAtSignKey(atSign))
|| 'The Authorization API Key did not create this Atsign, so it cannot delete it.');
});
const deletable = atSigns.filter((atSign) => !skipped.some((entry) => sameAtSign(entry, atSign)));
if (deletable.length === 0) {
throw new Error('The Authorization API Key did not create any of these Atsigns, so none of them can be deleted');
}
if (!token) {
throw new Error(tokenBody.message || 'Registrar did not return a delete token');
}
statusOutput.textContent = `Deleting ${deletable.length} of ${atSigns.length}...`;
const deleteExecuted = await executeApiRequest(endpoint, env, apiKey, {
token,
atSigns: deletable,
operation: 'delete'
});
executedPayloads.push(deleteExecuted.resultPayload);
const deleteBody = deleteExecuted.parsedBody && typeof deleteExecuted.parsedBody === 'object' ? deleteExecuted.parsedBody : {};
const deleteData = deleteBody.data && typeof deleteBody.data === 'object' ? deleteBody.data : {};
const failed = extractAtSignNames(deleteData.failed);
// The spec also allows ineligible Atsigns to come back as skipped
// on this call, though the registrar has only been seen using failed.
const skippedOnDelete = extractAtSignNames(deleteData.skippedatSigns);
const reasons = new Map([
...extractAtSignMessages(deleteData.failed),
...extractAtSignMessages(deleteData.skippedatSigns)
]);
const deleted = Array.isArray(deleteData.deleted)
? extractAtSignNames(deleteData.deleted)
: (deleteBody.status === 'success'
? deletable.filter((atSign) => !failed.some((entry) => sameAtSign(entry, atSign)))
: []);
deleted.forEach((atSign) => {
updateDeleteItem(batchResults, atSign, 'Deleted', 'Deleted from the registrar.');
});
const unresolved = deletable.filter((atSign) => !deleted.some((entry) => sameAtSign(entry, atSign)));
const failureReasons = [];
unresolved.forEach((atSign) => {
const reason = reasons.get(normalizeAtSignKey(atSign))
|| deleteBody.message
|| 'The registrar did not report this Atsign as deleted.';
if (!failureReasons.includes(reason)) {
failureReasons.push(reason);
}
const state = skippedOnDelete.some((entry) => sameAtSign(entry, atSign)) ? 'Skipped' : 'Failed';
updateDeleteItem(batchResults, atSign, state, reason);
});
output.textContent = JSON.stringify(executedPayloads, null, 2);
let transactionPath = null;
try {
transactionPath = await persistSuccessfulTransaction(endpoint, deleteExecuted.resultPayload);
} catch (persistError) {
if (warning) {
warning.textContent = `Warning: the delete request completed, but the transaction file was not saved: ${persistError.message}`;
warning.hidden = false;
}
}
if (deleted.length > 0) {
await warnAboutStaleLocalKeys(deleted, warning);
}
const elapsed = Date.now() - startedAt;
statusOutput.textContent = `Deleted ${deleted.length}/${atSigns.length} (${elapsed} ms).`
+ (skipped.length ? ` Skipped ${skipped.length}.` : '')
+ (unresolved.length ? ` Failed ${unresolved.length}.` : '')
// One shared reason is the common case, and it is the only thing
// worth reading; per-Atsign reasons stay on the rows below.
+ (failureReasons.length === 1 ? ` ${failureReasons[0]}` : '')
+ (transactionPath ? ` Transaction saved to: ${transactionPath}` : '');
} catch (err) {
const elapsed = Date.now() - startedAt;
const details = executedPayloads.length ? `${JSON.stringify(executedPayloads, null, 2)}\n\n` : '';
if (err && err.name === 'AbortError') {
statusOutput.textContent = `Request timed out after ${REQUEST_TIMEOUT_MS / 1000} seconds`;
output.textContent = `${details}The request exceeded the 15 second timeout and was cancelled.`;
} else {
statusOutput.textContent = `Delete failed (${elapsed} ms)`;
output.textContent = details + (err && err.stack ? err.stack : String(err));
}
} finally {
button.disabled = false;
}
}
async function sendRequest(row, endpoint) {
const statusOutput = row.querySelector('[data-role="status"]');
const output = row.querySelector('[data-role="output"]');
const button = row.querySelector('[data-role="send"]');
const startedAt = Date.now();
statusOutput.textContent = 'Sending request...';
button.disabled = true;
try {
const env = row.querySelector('[data-role="env"]').value;
const apiKey = row.querySelector('[data-role="api-key"]').value.trim();
const payload = collectPayload(row, endpoint);
if (endpoint.id === 'reset-atsign') {
const atSignToReset = String(payload.atSign || '').trim();
const confirmation = window.prompt(`Type the exact Atsign to reset: ${atSignToReset}`, '');
if (confirmation === null) {
statusOutput.textContent = 'Reset cancelled';
output.textContent = 'Reset request cancelled before sending.';
return;
}
if (confirmation.trim() !== atSignToReset) {
throw new Error(`Confirmation did not match. Type exactly: ${atSignToReset}`);
}
}
const executed = await executeApiRequest(endpoint, env, apiKey, payload);
const parsedBody = executed.parsedBody;
const resultPayload = executed.resultPayload;
let transactionPath = null;
try {
transactionPath = await persistSuccessfulTransaction(endpoint, resultPayload);
} catch (persistError) {
const warning = row.querySelector('[data-role="warning"]');
if (warning) {
warning.textContent = `Warning: request completed, but transaction file was not saved: ${persistError.message}`;
warning.hidden = false;
}
}
const elapsed = Date.now() - startedAt;
statusOutput.textContent = `${executed.response.status} ${executed.response.statusText} (${elapsed} ms)`;
renderPrimarySummary(row, endpoint, parsedBody);
if (transactionPath) {
const result = row.querySelector('[data-role="result"]');
if (result && !result.hidden) {
result.innerHTML += `<div class="api-v4-transaction-path">Transaction saved to: <code>${escapeHtml(transactionPath)}</code></div>`;
}
}
output.textContent = JSON.stringify(resultPayload, null, 2);
} catch (err) {
const elapsed = Date.now() - startedAt;
if (err && err.name === 'AbortError') {
statusOutput.textContent = `Request timed out after ${REQUEST_TIMEOUT_MS / 1000} seconds`;
output.textContent = 'The request exceeded the 15 second timeout and was cancelled.';
} else {
statusOutput.textContent = `Request failed (${elapsed} ms)`;
output.textContent = err && err.stack ? err.stack : String(err);
}
renderPrimarySummary(row, endpoint, null);
} finally {
button.disabled = false;
}
}
function clearRowOutput(row) {
const result = row.querySelector('[data-role="result"]');
if (result) {
result.hidden = true;
result.innerHTML = '';
}
const batchResults = row.querySelector('[data-role="batch-results"]');
if (batchResults) {
batchResults.hidden = true;
batchResults.innerHTML = '';
}
row.querySelector('[data-role="status"]').textContent = 'Ready';
row.querySelector('[data-role="output"]').textContent = '';
const warning = row.querySelector('[data-role="warning"]');
if (warning) {
warning.hidden = true;
warning.textContent = '';
}
}
function bindRow(root, endpoint) {
const row = root.querySelector(`[data-endpoint-id="${endpoint.id}"]`);
const toggle = row.querySelector('[data-role="toggle"]');
const body = row.querySelector('[data-role="body"]');
row.querySelector('[data-role="send"]').addEventListener('click', function () {
if (endpoint.type === 'batch-register') {
sendBatchRegisterRequest(row, endpoint);
} else if (endpoint.type === 'delete-atsigns') {
sendDeleteAtSignsRequest(row, endpoint);
} else {
sendRequest(row, endpoint);
}
});
row.querySelector('[data-role="clear"]').addEventListener('click', function () {
clearRowOutput(row);
});
toggle.addEventListener('click', function () {
const expanded = toggle.getAttribute('aria-expanded') === 'true';
toggle.setAttribute('aria-expanded', expanded ? 'false' : 'true');
body.hidden = expanded;
});
}
function syncApiKeyInputs(root, value, visible) {
if (!root) return;
root.querySelectorAll('[data-role="api-key"]').forEach((input) => {
input.value = value;
input.type = visible ? 'text' : 'password';
});
}
async function loadSavedConfig(statusEl, configFields, root, isVisible) {
try {
const response = await fetch('/api/config');
const data = await response.json();
if (!response.ok || data.error) {
throw new Error(data.error || 'Failed to load config');
}
configFields.apiKey.value = data.registrarApiKey || '';
configFields.prefix.value = data.atsignPrefix || '';
configFields.postfix.value = data.atsignPostfix || '';
syncApiKeyInputs(root, configFields.apiKey.value, isVisible);
if (statusEl) {
statusEl.textContent = 'Loaded config from ~/.atsign/at_activate_web/config.json';
}
} catch (err) {
if (statusEl) {
statusEl.textContent = `Load failed: ${err.message}`;
}
}
}
function bindConfigPanel(root) {
const globalApiKey = document.getElementById('api-v4-global-api-key');
const atsignPrefix = document.getElementById('api-v4-atsign-prefix');
const atsignPostfix = document.getElementById('api-v4-atsign-postfix');
const toggleButton = document.getElementById('api-v4-toggle-api-key');
const copyButton = document.getElementById('api-v4-copy-api-key');
const saveButton = document.getElementById('api-v4-save-api-key');
const refreshButton = document.getElementById('api-v4-refresh-config');
const openFolderButton = document.getElementById('api-v4-open-config-folder');
const saveStatus = document.getElementById('api-v4-save-status');
if (!globalApiKey || !atsignPrefix || !atsignPostfix) return;
let isVisible = false;
const configFields = {
apiKey: globalApiKey,
prefix: atsignPrefix,
postfix: atsignPostfix
};
const sync = function () {
syncApiKeyInputs(root, globalApiKey.value, isVisible);
};
globalApiKey.addEventListener('input', sync);
if (toggleButton) {
toggleButton.addEventListener('click', function () {
isVisible = !isVisible;
globalApiKey.type = isVisible ? 'text' : 'password';
toggleButton.textContent = isVisible ? 'Hide' : 'Show';
sync();
});
}
if (copyButton) {
copyButton.addEventListener('click', async function () {
try {
await navigator.clipboard.writeText(globalApiKey.value);
if (saveStatus) saveStatus.textContent = 'API key copied to clipboard';
} catch (_) {
if (saveStatus) saveStatus.textContent = 'Copy failed';
}
});
}
if (saveButton) {
saveButton.addEventListener('click', async function () {
saveButton.disabled = true;
if (saveStatus) saveStatus.textContent = 'Saving...';
try {
const response = await fetch('/api/config', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
registrarApiKey: globalApiKey.value,
atsignPrefix: atsignPrefix.value,
atsignPostfix: atsignPostfix.value
})
});
const data = await response.json();
if (!response.ok || data.error) {
throw new Error(data.error || 'Failed to save config');
}
if (saveStatus) saveStatus.textContent = 'Saved to ~/.atsign/at_activate_web/config.json';
} catch (err) {
if (saveStatus) saveStatus.textContent = `Save failed: ${err.message}`;
} finally {
saveButton.disabled = false;
}
});
}
if (refreshButton) {
refreshButton.addEventListener('click', async function () {
refreshButton.disabled = true;
if (saveStatus) saveStatus.textContent = 'Refreshing...';
try {
await loadSavedConfig(saveStatus, configFields, root, isVisible);
} finally {
refreshButton.disabled = false;
}
});
}
if (openFolderButton) {
openFolderButton.addEventListener('click', async function () {
openFolderButton.disabled = true;
if (saveStatus) saveStatus.textContent = 'Opening config folder...';
try {
const response = await fetch('/api/config/open-folder', {
method: 'POST'
});
const data = await response.json();
if (!response.ok || data.error) {
throw new Error(data.error || 'Failed to open config folder');
}
if (saveStatus) saveStatus.textContent = `Opened config folder: ${data.path}`;
} catch (err) {
if (saveStatus) saveStatus.textContent = `Open failed: ${err.message}`;
} finally {
openFolderButton.disabled = false;
}
});
}
globalApiKey.type = 'password';
sync();
loadSavedConfig(saveStatus, configFields, root, isVisible);
}
async function initializeApiV4Console() {
const root = document.getElementById('api-v4-rows');
if (!root) return;
root.innerHTML = PRIMARY_ENDPOINTS.map(endpointMarkup).join('') + otherSectionMarkup();
ALL_ENDPOINTS.forEach((endpoint) => bindRow(root, endpoint));
bindConfigPanel(root);
window.loadApiV4Transactions = loadApiV4Transactions;
window.openTransactionsFolder = openTransactionsFolder;
window.loadActivationCramKeys = loadActivationCramKeys;
window.openCramKeysFolder = openCramKeysFolder;
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initializeApiV4Console);
} else {
initializeApiV4Console();
}
})();
''';