Skip to content
Merged
14 changes: 13 additions & 1 deletion .github/workflows/playwright.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,15 @@ jobs:
NEXTAUTH_SECRET: "testsecret123"
GOOGLE_CLIENT_ID: "test"
GOOGLE_CLIENT_SECRET: "test"
# Firebase: required for next build (collecting page data). Add repo secrets to override.
NEXT_PUBLIC_FIREBASE_API_KEY: ${{ secrets.NEXT_PUBLIC_FIREBASE_API_KEY || 'AIzaSyDUMMYDUMMYDUMMYDUMMYDUMMYDUMMYDUMMY1' }}
NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN: ${{ secrets.NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN || 'demo-project.firebaseapp.com' }}
NEXT_PUBLIC_FIREBASE_PROJECT_ID: ${{ secrets.NEXT_PUBLIC_FIREBASE_PROJECT_ID || 'demo-project' }}
NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET: ${{ secrets.NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET || 'demo-project.appspot.com' }}
NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID: ${{ secrets.NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID || '123456789012' }}
NEXT_PUBLIC_FIREBASE_APP_ID: ${{ secrets.NEXT_PUBLIC_FIREBASE_APP_ID || '1:123456789012:web:abcdef1234567890abcdef' }}
# Resend: required for next build (verification/send route). Add RESEND_API_KEY secret to override.
NEXT_PUBLIC_RESEND_API_KEY: ${{ secrets.NEXT_PUBLIC_RESEND_API_KEY || secrets.RESEND_API_KEY || 're_dummy_for_playwright' }}
steps:
- uses: actions/checkout@v3

Expand All @@ -24,7 +33,10 @@ jobs:
node-version: 20

- name: Install dependencies
run: yarn install --frozen-lockfile
run: yarn install

- name: Build the project
run: yarn build

- name: Install Playwright Browsers
run: npx playwright install --with-deps
Expand Down
5 changes: 4 additions & 1 deletion app/[formType]/form/types/Types.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { ISkill } from 'hr-context'

// Interfaces for the credential data
export interface Address {
addressCountry: string
Expand Down Expand Up @@ -83,7 +85,8 @@ export interface FormData {
company?: string
employeeName?: string
employeeJobTitle?: string
[key: string]: string | number | boolean | Portfolio[] | string[] | undefined
skills?: ISkill[]
[key: string]: string | number | boolean | Portfolio[] | string[] | ISkill[] | undefined
}

// Component Props for the form
Expand Down
36 changes: 36 additions & 0 deletions app/utils/normalization/hrContextSkillClaim.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { ISkill } from 'hr-context'
import { FormData } from '../../[formType]/form/types/Types'
export type SkillClaimFormData = {
personName: string
personId?: string
skills: ISkill[]
evidence?: Array<{ id: string; name: string; type?: string; description?: string }>
}

export function normalizeSkillClaimFormData(formData: FormData): SkillClaimFormData {
const skill = formData.skills?.[0] // pick the first skill in the array
const skillName = skill?.name ?? formData.credentialName ?? ''
const skillDescription = formData.credentialDescription ?? undefined
const narrative = typeof formData.description === 'string'
? formData.description
: typeof formData.credentialDescription === 'string'
? formData.credentialDescription
: ''

const skills = [
{
name: skillName,
description: skillDescription,
durationPerformed: formData.credentialDuration ?? '',
narrative,
image: formData.evidenceLink ? { id: formData.evidenceLink, type: 'Image' } : undefined
}
] as ISkill[]

const evidence = formData.portfolio.length ? formData.portfolio.map((p: any) => ({ id: p.url, name: p.name, description: p.description })) : []
return {
personName: formData.fullName ?? '',
skills,
evidence: evidence.length ? evidence : []
}
}
12 changes: 5 additions & 7 deletions app/utils/signCred.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { CredentialEngine, GoogleDriveStorage } from '@cooperation/vc-storage'
import { FormData } from '../[formType]/form/types/Types'
import { normalizeSkillClaimFormData, SkillClaimFormData } from './normalization/hrContextSkillClaim'
import { ISkillClaimCredential } from 'hr-context'

interface RecommendationI {
recommendationText: string
Expand Down Expand Up @@ -113,13 +115,9 @@ const signCred = async (
case 'skill':
case 'identity-verification':
default:
// Use generic signVC for skills and other types
signedVC = await credentialEngine.signVC({
data: processedData,
type: 'VC',
keyPair,
issuerId: issuerDid
})
// Sign skill claim credential using the HR Context data model
const normalizedData: SkillClaimFormData = normalizeSkillClaimFormData(processedData as unknown as FormData)
signedVC = await credentialEngine.signSkillClaimVC(normalizedData as unknown as ISkillClaimCredential, keyPair, issuerDid)
break
}
}
Expand Down
67 changes: 67 additions & 0 deletions app/utils/signSkillClaim.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import {
CredentialEngine,
GoogleDriveStorage,
saveToGoogleDrive
} from '@cooperation/vc-storage'
import type { ISkillClaimCredential } from 'hr-context'
import type { FormData } from '../[formType]/form/types/Types'
import { normalizeSkillClaimFormData, SkillClaimFormData } from './normalization/hrContextSkillClaim'

/**
* Sign a SkillClaimCredential using the HR Context data model.
* Uses the shared storage and engine. Creates a DID if keyPair/issuerId are not provided.
*
* @param storage - Shared GoogleDriveStorage instance (e.g. from useGoogleDrive)
* @param engine - Shared CredentialEngine instance (e.g. from getCredentialEngine)
* @param input - Form data + skills (or pre-built SkillClaimFormData)
* @param options - Optional keyPair and issuerId (if already have DID); saveToDrive to persist
* @returns The signed SkillClaimCredential
*/
export async function signSkillClaim(
storage: GoogleDriveStorage,
engine: CredentialEngine,
formData: FormData,
options?: {
keyPair?: any
issuerId?: string
saveToDrive?: boolean
}
): Promise<ISkillClaimCredential | { signedVC: ISkillClaimCredential; file: any }> {
if (!storage || !engine)
throw new Error('Storage and CredentialEngine are required.')

const normalizedData: SkillClaimFormData = normalizeSkillClaimFormData(formData)

let keyPair = options?.keyPair
let issuerId = options?.issuerId

if (!keyPair || !issuerId) {
const { didDocument, keyPair: kp } = await engine.createDID()
keyPair = kp
issuerId = didDocument.id
normalizedData.personId = normalizedData.personId ?? issuerId
}

if (!issuerId) throw new Error('Issuer DID is required.')

try {
const signedVC = await engine.signSkillClaimVC(
normalizedData as unknown as ISkillClaimCredential,
keyPair,
issuerId
)
if (options?.saveToDrive) {
const file = await saveToGoogleDrive({
storage,
data: signedVC,
type: 'VC'
})
return { signedVC, file }
}

return signedVC
} catch (error) {
console.error('🚀 ~ signSkillClaim ~ error:', JSON.stringify(error, null, 2))
throw error
}
}
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
"test:e2e": "playwright test"
},
"dependencies": {
"@cooperation/vc-storage": "^1.0.40",
"@cooperation/vc-storage": "^1.0.44",
"@emotion/react": "^11.11.4",
"@emotion/styled": "^11.11.5",
"@mui/icons-material": "^5.15.14",
Expand All @@ -24,6 +24,7 @@
"ethers": "^6.13.2",
"firebase": "^11.3.1",
"googleapis": "^144.0.0",
"hr-context": "^0.1.6",
"js-cookie": "^3.0.5",
"lru-cache": "^11.0.1",
"lucide-react": "^0.469.0",
Expand Down