-
Notifications
You must be signed in to change notification settings - Fork 1
Add managed ENS publication helper #388
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,34 @@ | ||
| 'use strict'; | ||
|
|
||
| const db = require('../../lib/db'); | ||
| const { requireAdminAuth } = require('./_auth'); | ||
| const { buildManagedEnsPublicationPackage } = require('../../lib/claims/managed-ens-publication'); | ||
|
|
||
| module.exports = async function handler(req, res) { | ||
| res.setHeader('Content-Type', 'application/json; charset=utf-8'); | ||
| res.setHeader('Cache-Control', 'no-store'); | ||
| if (req.method !== 'POST') { res.setHeader('Allow', 'POST'); return res.status(405).json({ ok: false, status: 'METHOD_NOT_ALLOWED' }); } | ||
| if (!requireAdminAuth(req, res)) return; | ||
| const claimId = req.body && (req.body.claim_id || req.body.claimId); | ||
| if (!claimId) return res.status(400).json({ ok: false, status: 'CLAIM_ID_REQUIRED' }); | ||
| try { | ||
| const result = await db.query('select * from claim_requests where claim_id = $1 limit 1', [claimId]); | ||
| const claim = result.rows[0]; | ||
| if (!claim) return res.status(404).json({ ok: false, status: 'CLAIM_NOT_FOUND' }); | ||
| const publication = buildManagedEnsPublicationPackage(claim); | ||
| await db.query( | ||
| `update claim_requests | ||
| set managed_ens_publication_status = 'ready_to_publish', | ||
| managed_ens_required_txt_records = $2::jsonb, | ||
| managed_ens_publication_instructions = $3::jsonb, | ||
| managed_ens_parent_namespace = $4, | ||
| managed_ens_publication_error = null, | ||
| updated_at = now() | ||
| where claim_id = $1`, | ||
| [claimId, JSON.stringify(publication.required_txt_records), JSON.stringify(publication.instructions), publication.parent_namespace] | ||
| ); | ||
| return res.status(200).json({ ok: true, claim_id: claimId, publication }); | ||
| } catch (error) { | ||
| return res.status(400).json({ ok: false, status: error.status || 'MANAGED_ENS_PUBLICATION_PREPARE_FAILED', error: error.message }); | ||
| } | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,42 @@ | ||
| 'use strict'; | ||
|
|
||
| const db = require('../../lib/db'); | ||
| const { requireAdminAuth } = require('./_auth'); | ||
| const { verifyManagedEnsPublication } = require('../../lib/claims/managed-ens-publication'); | ||
|
|
||
| module.exports = async function handler(req, res) { | ||
| res.setHeader('Content-Type', 'application/json; charset=utf-8'); | ||
| res.setHeader('Cache-Control', 'no-store'); | ||
| if (req.method !== 'POST') { res.setHeader('Allow', 'POST'); return res.status(405).json({ ok: false, status: 'METHOD_NOT_ALLOWED' }); } | ||
| if (!requireAdminAuth(req, res)) return; | ||
| const claimId = req.body && (req.body.claim_id || req.body.claimId); | ||
| if (!claimId) return res.status(400).json({ ok: false, status: 'CLAIM_ID_REQUIRED' }); | ||
| try { | ||
| const result = await db.query('select * from claim_requests where claim_id = $1 limit 1', [claimId]); | ||
| const claim = result.rows[0]; | ||
| if (!claim) return res.status(404).json({ ok: false, status: 'CLAIM_NOT_FOUND' }); | ||
| const verification = await verifyManagedEnsPublication(claim, req.verifyOptions || {}); | ||
| if (verification.ok) { | ||
| await db.query( | ||
| `update claim_requests | ||
| set managed_ens_publication_status = 'verified', tenant_signer_record_status = 'records_verified', | ||
| tenant_signer_records_verified_at = now(), managed_ens_verified_at = now(), | ||
| managed_ens_publication_error = null, tenant_signer_verification_error = null, updated_at = now() | ||
| where claim_id = $1`, | ||
| [claimId] | ||
| ); | ||
| } else { | ||
| await db.query( | ||
| `update claim_requests | ||
| set managed_ens_publication_status = $2, managed_ens_publication_error = $3, | ||
| tenant_signer_verification_error = $3, updated_at = now() | ||
| where claim_id = $1`, | ||
| [claimId, verification.status, verification.error] | ||
| ); | ||
| } | ||
| return res.status(200).json({ ok: true, claim_id: claimId, verification }); | ||
| } catch (error) { | ||
| try { await db.query(`update claim_requests set managed_ens_publication_status = 'failed', managed_ens_publication_error = $2, updated_at = now() where claim_id = $1`, [claimId, error.message]); } catch (_) {} | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When the ENS/RPC resolver throws, this catch persists the raw Useful? React with 👍 / 👎. |
||
| return res.status(400).json({ ok: false, status: error.status || 'MANAGED_ENS_PUBLICATION_VERIFY_FAILED', error: error.message }); | ||
| } | ||
| }; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| alter table if exists claim_requests | ||
| add column if not exists managed_ens_publication_status text default 'not_started', | ||
| add column if not exists managed_ens_parent_namespace text, | ||
| add column if not exists managed_ens_publication_instructions jsonb, | ||
| add column if not exists managed_ens_required_txt_records jsonb, | ||
| add column if not exists managed_ens_verified_at timestamptz, | ||
| add column if not exists managed_ens_publication_error text; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,59 @@ | ||
| 'use strict'; | ||
|
|
||
| const { buildSignerRecords, CANONICALIZATION, resolveRequiredSignerRecords, compareSignerRecords } = require('./signer-records'); | ||
|
|
||
| const APPROVED_MANAGED_PARENTS = ['attestagent.eth', 'approveagent.eth', 'verifyagent.eth', 'authorizeagent.eth']; | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
For CL-mode claims submitted with the current Trust Verification pack, Useful? React with 👍 / 👎. |
||
| const PUBLICATION_STATUSES = ['not_started', 'ready_to_publish', 'published_pending_verification', 'verified', 'failed']; | ||
|
|
||
| function normalizeEns(value) { return String(value || '').trim().toLowerCase(); } | ||
| function tenantRootEns(claim) { return normalizeEns(claim.tenant_root_ens || claim.root_ens || (claim.tenant ? `${claim.tenant}.eth` : '')); } | ||
| function parentNamespace(signerEns) { const parts = normalizeEns(signerEns).split('.'); return parts.length > 2 ? parts.slice(1).join('.') : ''; } | ||
| function isApprovedParent(parent) { return APPROVED_MANAGED_PARENTS.includes(normalizeEns(parent)); } | ||
| function readReceiptSigner(claim) { | ||
| const pkg = claim.claim_package || claim.request_json || claim.receipt_json || {}; | ||
| return normalizeEns(pkg?.cl?.receipt?.signer || pkg?.['cl.receipt.signer'] || pkg?.receipt?.signer || claim.receipt_signer || ''); | ||
| } | ||
| function validationError(status, error) { const e = new Error(error); e.status = status; return e; } | ||
|
|
||
| function validateManagedEnsPublicationClaim(claim) { | ||
| if (!claim || claim.activation_mode !== 'managed_namespace') throw validationError('MANAGED_NAMESPACE_REQUIRED', 'Managed ENS publication is only available for managed namespace claims.'); | ||
| const signerEns = normalizeEns(claim.tenant_signer_ens); | ||
| if (!signerEns) throw validationError('TENANT_SIGNER_ENS_REQUIRED', 'tenant_signer_ens is required.'); | ||
| const root = tenantRootEns(claim); | ||
| if (root && signerEns === root) throw validationError('TENANT_ROOT_ENS_NOT_ALLOWED', 'Managed signer ENS must not equal the tenant root ENS.'); | ||
| const parent = parentNamespace(signerEns); | ||
| if (!parent || !isApprovedParent(parent)) throw validationError('MANAGED_PARENT_NOT_APPROVED', 'tenant_signer_ens must end in an approved managed parent namespace.'); | ||
| if (!claim.tenant_signer_public_key || !claim.tenant_signer_kid || !claim.tenant_signer_canonicalization) throw validationError('SIGNER_RECORD_FIELDS_REQUIRED', 'public key, kid, and canonicalization are required.'); | ||
| const receiptSigner = readReceiptSigner(claim); | ||
| if (receiptSigner && receiptSigner !== signerEns) throw validationError('RECEIPT_SIGNER_MISMATCH', 'claim package cl.receipt.signer must match tenant_signer_ens.'); | ||
| return { signerEns, parent, tenant: claim.tenant || signerEns.split('.')[0] }; | ||
| } | ||
|
|
||
| function buildManagedEnsPublicationPackage(claim) { | ||
| const { signerEns, parent, tenant } = validateManagedEnsPublicationClaim(claim); | ||
| const required = buildSignerRecords({ publicKey: claim.tenant_signer_public_key, kid: claim.tenant_signer_kid, canonicalization: claim.tenant_signer_canonicalization || CANONICALIZATION, signerEns }); | ||
| return { | ||
| signer_ens: signerEns, | ||
| parent_namespace: parent, | ||
| tenant, | ||
| required_txt_records: required, | ||
| agent_records: { | ||
| 'cl.capability': 'attest', | ||
| 'cl.runtime': 'https://runtime.commandlayer.org', | ||
| 'cl.verifier': 'https://runtime.commandlayer.org/verify', | ||
| 'cl.trust_verification_entry': 'https://runtime.commandlayer.org/trust-verification/attest/v1.0.0', | ||
|
Comment on lines
+41
to
+44
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
For managed claims whose signer is one of the other allowed parents, such as Useful? React with 👍 / 👎. |
||
| 'cl.agent.card': 'pending_provisioning', | ||
| }, | ||
| instructions: ['Open ENS Manager', 'Select the managed signer name', 'Add the required TXT records', 'Save changes', 'Return to CommandLayer and run verification'], | ||
| }; | ||
| } | ||
|
|
||
| async function verifyManagedEnsPublication(claim, options = {}) { | ||
| const pkg = buildManagedEnsPublicationPackage(claim); | ||
| const resolved = await resolveRequiredSignerRecords(pkg.signer_ens, options); | ||
| const { checks, verified } = compareSignerRecords({ ...claim, tenant_signer_ens: pkg.signer_ens }, resolved); | ||
| const missing = Object.values(resolved).some((value) => !value); | ||
| return { ok: verified && !missing, status: verified && !missing ? 'verified' : (missing ? 'published_pending_verification' : 'failed'), signer_ens: pkg.signer_ens, parent_namespace: pkg.parent_namespace, required_txt_records: pkg.required_txt_records, resolved_txt_records: resolved, checks, error: verified && !missing ? null : (missing ? 'required_txt_record_missing' : 'required_txt_record_mismatch') }; | ||
| } | ||
|
|
||
| module.exports = { APPROVED_MANAGED_PARENTS, PUBLICATION_STATUSES, validateManagedEnsPublicationClaim, buildManagedEnsPublicationPackage, verifyManagedEnsPublication }; | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When live TXT records are present but mismatched, this branch records only the managed publication status/error and leaves
tenant_signer_record_statusat its previous value (commonlyrecords_generated). Since both the public pipeline and admin list readtenant_signer_record_statusfor ENS record state, an operator-run failed verification still surfaces stale signer-record status instead ofrecords_mismatch/records_unavailable; update it the same way the existing signer-record verifier does.Useful? React with 👍 / 👎.