feat(certification): apply wedge spec via manifest + signing guard (closes #24) - #34
Merged
Merged
Conversation
…loses #24) Adds Credential schema (appendOnly, lifecycle issued→revoked|expired) with declarative calculations (daysUntilExpiry, expiryStatus, isOpenBadgesV3Signed, isExpiringIn90Days/30Days, isExpired), notifications (issuedToLearner, expiringSoonAlert, expiryT90, expiredAlert with alsoDispatchLifecycle:expire, revoked), and relations to LearnerProfile + Course. Legitimate PHP seams (ADR-031): CredentialSigningService (OB3 RS256 signing, lifecycle guard on issue transition), KeyManagementService (RSA-2048 keypair gen), CredentialIssuanceHandler (ObjectTransitionedEvent bridge), and CredentialVerifyController (public @publicpage verify endpoint). Manifest extended with CredentialDetail (type:detail) and CredentialVerify (type:custom, public:true) pages. CredentialVerify.vue renders verification card with valid/invalid badge, metadata, and QR code.
rubenvdlinde
force-pushed
the
feature/wedge/certification
branch
from
May 11, 2026 21:58
22c04b0 to
9451604
Compare
Contributor
Quality Report — ConductionNL/scholiq @
|
| Check | PHP | Vue | Security | License | Tests |
|---|---|---|---|---|---|
| lint | ✅ | ||||
| phpcs | ✅ | ||||
| phpmd | ✅ | ||||
| psalm | ✅ | ||||
| phpstan | ✅ | ||||
| phpmetrics | ✅ | ||||
| eslint | ❌ | ||||
| stylelint | ❌ | ||||
| composer | ✅ | ✅ 100/100 | |||
| npm | ❌ | ❌ | |||
| PHPUnit | ⏭️ | ||||
| Newman | ⏭️ | ||||
| Playwright | ⏭️ |
Quality workflow — 2026-05-11 22:01 UTC
Download the full PDF report from the workflow artifacts.
This was referenced May 12, 2026
Closed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Credentialinlib/Settings/scholiq_register.json—appendOnly: true, lifecycleissued → revoked | expiredwith a declarativeissuetransition guard (CredentialSigningService).issued(initial),revoked,expired. Theexpiringthreshold is modelled via calculations (isExpiringIn90Days,isExpiringIn30Days) that trigger idempotency-keyed notifications; the hardexpiredstate fires viaalsoDispatchLifecycle: expireon theexpiredAlertnotification — no TimedJob, no ExpiryDetectionService.daysUntilExpiry(ORdateDiffexpression) feedsisExpiringIn30DaysandisExpiringIn90Days. TheexpiredAlertnotification triggers whenisExpired = trueand carriesalsoDispatchLifecycle: expire.daysUntilExpiry,expiryStatus(string enum),isOpenBadgesV3Signed(bool),isExpiringIn90Days,isExpiringIn30Days,isExpired— allmaterialise: true.issuedToLearner(lifecycleEnter:issued),expiringSoonAlertT30 (idempotency-keyed),expiryT90(idempotency-keyed),expiredAlert(dispatchesexpiretransition),revoked(lifecycleEnter:revoked).lib/Service/CredentialSigningService.php— OB3 JSON-LD assembly + RS256 signing; acts as OR lifecycle guard onissuetransition viarequires[]in schema.lib/Service/KeyManagementService.php— RSA-2048 keypair generation; encrypts private key viaICrypto.lib/Listener/CredentialIssuanceHandler.php—ObjectTransitionedEventlistener; bridges Enrolment.completed → Credential save via OR.lib/Controller/CredentialVerifyController.php—@PublicPage @NoCSRFRequiredverify endpoint, no personal data.lib/Controller/KeyAdminController.php— admin-only generate-key + key-status endpoints.CredentialDetail(type:detail) andCredentialVerify(type:custom, public:true).src/views/CredentialVerify.vueadded.CredentialController,CredentialService,CredentialExpiryJob,AuditEventTypes::KNOWN, or app-local stores.Test plan
python3 -c "import json; json.load(open('lib/Settings/scholiq_register.json'))"passes.composer lint— all files pass, no syntax errors.expiresAt = today + 30, trigger OR calculation refresh →isExpiringIn30Days = true,expiringSoonAlertdispatched once.expiresAt = today - 1, trigger OR calculation refresh →isExpired = true,expiredAlertfires,expiretransition executes (lifecycle becomesexpired)./credentials/:id/verifywithout auth →{valid: true}+ QR code renders./api/credentials/admin/generate-key→ keypair stored, key-status returnsconfigured: true.completedon Course withcertificateTemplate→ Credential auto-issued within 30s,issuedToLearnernotification dispatched.🤖 Generated with Claude Code