Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions .idea/.gitignore

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 9 additions & 0 deletions .idea/biome.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 9 additions & 0 deletions .idea/eudi-wallet-functionality.iml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions .idea/jsLibraryMappings.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions .idea/misc.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 8 additions & 0 deletions .idea/modules.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions .idea/vcs.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 7 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
},
"publishConfig": {
"access": "public",
"main": "./dist/index.mjs",
"module": "./dist/index.mjs",
"types": "./dist/index.d.mts",
"exports": {
Expand Down Expand Up @@ -52,6 +53,12 @@
"typescript": "~5.9.3"
},
"dependencies": {
"@animo-id/eudi-wallet-ts12-validation": "workspace:*",
"@animo-id/eudi-wallet-ts12-credential-metadata": "workspace:*",
"@animo-id/eudi-wallet-ts12-credential-metadata-wallet": "workspace:*",
"@animo-id/eudi-wallet-ts12-credential-metadata-provider": "workspace:*",
"@animo-id/eudi-wallet-ts12-credential-metadata-provider-credo": "workspace:*",
"@animo-id/eudi-wallet-ts12-credential-metadata-wallet-credo": "workspace:*",
"zod": "^4.3.5"
}
}
45 changes: 45 additions & 0 deletions packages/credential-metadata-provider-credo/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
{
"name": "@animo-id/eudi-wallet-ts12-credential-metadata-provider-credo",
"description": "EUDI Wallet TS12 credential metadata provider — Credo-ts module, JwtSigner adapter, and Express router for serving signed credential metadata per Section 5",
"version": "0.1.0",
"license": "Apache-2.0",
"author": "Frederic Artus Nieto for DSGV",
"exports": "./src/index.ts",
"files": [
"dist"
],
"engines": {
"node": ">=22"
},
"publishConfig": {
"access": "public",
"main": "./dist/index.mjs",
"module": "./dist/index.mjs",
"types": "./dist/index.d.mts",
"exports": {
".": "./dist/index.mjs",
"./package.json": "./package.json"
}
},
"repository": {
"type": "git",
"url": "https://github.com/animo/eudi-wallet-functionality",
"directory": "packages/credential-metadata-provider-credo"
},
"scripts": {
"types:check": "tsc --noEmit",
"build": "tsdown src/index.ts --format esm --dts --clean --sourcemap"
},
"dependencies": {
"@animo-id/eudi-wallet-ts12-credential-metadata": "workspace:*",
"@animo-id/eudi-wallet-ts12-credential-metadata-provider": "workspace:*",
"@animo-id/eudi-wallet-ts12-validation": "workspace:*"
},
"peerDependencies": {
"@credo-ts/core": "*"
},
"devDependencies": {
"tsdown": "^0.18.4",
"typescript": "~5.9.3"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import type { AgentContext } from '@credo-ts/core'
import {
CredentialMetadataProvider,
type CredentialMetadataResponse,
} from '@animo-id/eudi-wallet-ts12-credential-metadata-provider'
import { CredentialMetadataProviderModule } from './credential-metadata-provider-module'
import { createCredentialMetadataHandler, type CredentialMetadataRouterHandler } from './credential-metadata-router'

/**
* Public API for the credential metadata provider.
*
* Exposed on the Agent as `agent.credentialMetadataProvider.*` when the
* `CredentialMetadataProviderModule` is registered.
*/
export class CredentialMetadataProviderApi {
private provider: CredentialMetadataProvider

constructor(agentContext: AgentContext) {
const module = agentContext.dependencyManager.resolve(CredentialMetadataProviderModule)
this.provider = new CredentialMetadataProvider(module.config)
}

/**
* Handle a credential metadata request.
*
* @param credentialId The credential configuration ID from the URL path.
* @param headers The HTTP request headers (accept, acceptLanguage).
*/
async handle(
credentialId: string,
headers: { accept?: string; acceptLanguage?: string }
): Promise<CredentialMetadataResponse> {
return this.provider.handle(credentialId, headers)
}

/**
* Create a request handler for `GET /:credentialId`.
*
* Wire this into your HTTP framework's router:
* @example
* ```typescript
* import { Router } from 'express'
* const router = Router()
* router.get('/:credentialId', agent.credentialMetadataProvider.createHandler())
* app.use('/credential-metadata', router)
* ```
*/
createHandler(): CredentialMetadataRouterHandler {
return createCredentialMetadataHandler(this.provider)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import type { DependencyManager, Module } from '@credo-ts/core'
import type { CredentialMetadataProviderConfig } from '@animo-id/eudi-wallet-ts12-credential-metadata-provider'
import { CredentialMetadataProviderApi } from './credential-metadata-provider-api'

/**
* Credo-ts module for serving signed credential metadata per TS12 Section 5.
*
* @example
* ```typescript
* import {
* CredentialMetadataProviderModule,
* createCredoJwtSigner,
* createInMemoryCredentialMetadataStore,
* } from '@animo-id/eudi-wallet-ts12-credential-metadata-provider-credo'
*
* const agent = new Agent({
* modules: {
* credentialMetadataProvider: new CredentialMetadataProviderModule({
* issuerIdentifier: 'https://issuer.example.com',
* expiresInSeconds: 86400,
* signer: createCredoJwtSigner(agentContext, { x5c: [leafCert, rootCert] }),
* store: createInMemoryCredentialMetadataStore({
* 'WeroSca': {
* credentialType: 'https://example.com/wero-sca',
* format: 'dc+sd-jwt',
* credentialMetadataUri: 'https://issuer.example.com/credential-metadata/WeroSca',
* credentialMetadata: { display: [...], transaction_data_types: {...} },
* },
* }),
* }),
* }
* })
*
* // Mount the router
* app.use('/credential-metadata', agent.credentialMetadataProvider.createRouter())
* ```
*/
export class CredentialMetadataProviderModule implements Module {
readonly api = CredentialMetadataProviderApi
readonly config: CredentialMetadataProviderConfig

constructor(config: CredentialMetadataProviderConfig) {
this.config = config
}

register(_dependencyManager: DependencyManager): void {
// The provider is stateless and config-driven — DI wiring happens in the API class
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import type { CredentialMetadataProvider } from '@animo-id/eudi-wallet-ts12-credential-metadata-provider'

/**
* Request handler for the `credential_metadata_uri` endpoint.
*
* Compatible with Express, Hono, and any framework using `(req, res) => void`.
*/
export type CredentialMetadataRouterHandler = (
req: { params: { credentialId: string }; headers: Record<string, string | string[] | undefined> },
res: { status(code: number): { type(contentType: string): { send(body: string): void } } }
) => Promise<void>

/**
* Create a request handler for `GET /:credentialId` that delegates to the
* agnostic {@link CredentialMetadataProvider}.
*
* @example Express
* ```typescript
* import { Router } from 'express'
* const router = Router()
* router.get('/:credentialId', createCredentialMetadataHandler(provider))
* app.use('/credential-metadata', router)
* ```
*/
export function createCredentialMetadataHandler(provider: CredentialMetadataProvider): CredentialMetadataRouterHandler {
return async (req, res) => {
const { credentialId } = req.params
const accept = typeof req.headers.accept === 'string' ? req.headers.accept : undefined
const acceptLanguage =
typeof req.headers['accept-language'] === 'string' ? req.headers['accept-language'] : undefined

const result = await provider.handle(credentialId, { accept, acceptLanguage })
res.status(result.status).type(result.contentType).send(result.body)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import type { JwtSigner } from '@animo-id/eudi-wallet-ts12-credential-metadata'
import type { AgentContext, X509Certificate } from '@credo-ts/core'
import { JwsService } from '@credo-ts/core'

export interface CredoJwtSignerOptions {
/**
* The signer's X.509 certificate chain. Leaf first.
* The leaf certificate's key is used for signing, and the full chain
* is included in the `x5c` JOSE header of every signed JWT.
*/
x5c: X509Certificate[]
}

/**
* Create a {@link JwtSigner} backed by Credo's JwsService and the signer's
* own X.509 certificate chain.
*
* The returned signer:
* - Uses the leaf certificate's private key for signing
* - Includes the full chain in the `x5c` protected header
* - Sets `typ: 'credential-metadata+jwt'` per TS12 Section 5
* - Derives `alg` from the leaf certificate's key type
*/
export function createCredoJwtSigner(agentContext: AgentContext, options: CredoJwtSignerOptions): JwtSigner {
const jwsService = agentContext.dependencyManager.resolve(JwsService)
const leafCert = options.x5c[0]
const keyId = leafCert.publicJwk.keyId
const alg = leafCert.publicJwk.supportedSignatureAlgorithms[0]
const x5c = options.x5c.map((cert) => cert.toString('base64'))

return {
x5c,

async sign(payload: Record<string, unknown>): Promise<string> {
return jwsService.createJwsCompact(agentContext, {
payload: Buffer.from(JSON.stringify(payload)),
keyId,
protectedHeaderOptions: {
alg,
typ: 'credential-metadata+jwt',
x5c,
},
})
},
}
}
52 changes: 52 additions & 0 deletions packages/credential-metadata-provider-credo/src/in-memory-store.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import type { CredentialMetadata } from '@animo-id/eudi-wallet-ts12-validation'
import type { CredentialInfo, CredentialMetadataStore } from '@animo-id/eudi-wallet-ts12-credential-metadata-provider'

/** Registration for a credential in the in-memory store. */
export interface CredentialRegistration {
/** The credential type identifier — vct or doctype. */
credentialType: string
/** The credential format identifier, e.g., 'dc+sd-jwt', 'mso_mdoc'. */
format: string
/** The URL at which this credential's metadata is served. */
credentialMetadataUri: string
/** The full credential metadata with all locales. */
credentialMetadata: CredentialMetadata
}

/**
* Create an in-memory {@link CredentialMetadataStore} from a static credential map.
*
* **Debug/development only.** Production deployments should implement
* {@link CredentialMetadataStore} with persistent storage.
*/
export function createInMemoryCredentialMetadataStore(
credentials: Record<string, CredentialRegistration>
): CredentialMetadataStore {
const jwtCache = new Map<string, string>()
const registeredAt = Date.now()

return {
async getCredentialInfo(credentialId): Promise<CredentialInfo | undefined> {
const reg = credentials[credentialId]
if (!reg) return undefined
return {
credentialType: reg.credentialType,
format: reg.format,
credentialMetadataUri: reg.credentialMetadataUri,
updatedAt: registeredAt,
}
},

async getCredentialMetadata(credentialId): Promise<CredentialMetadata | undefined> {
return credentials[credentialId]?.credentialMetadata
},

async getSignedJwt(credentialId, canonicalLocale): Promise<string | undefined> {
return jwtCache.get(`${credentialId}:${canonicalLocale}`)
},

async saveSignedJwt(credentialId, canonicalLocale, jwt): Promise<void> {
jwtCache.set(`${credentialId}:${canonicalLocale}`, jwt)
},
}
}
5 changes: 5 additions & 0 deletions packages/credential-metadata-provider-credo/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export { CredentialMetadataProviderApi } from './credential-metadata-provider-api'
export { CredentialMetadataProviderModule } from './credential-metadata-provider-module'
export { createCredoJwtSigner, type CredoJwtSignerOptions } from './credo-jwt-signer'
export { createInMemoryCredentialMetadataStore, type CredentialRegistration } from './in-memory-store'
export { createCredentialMetadataHandler, type CredentialMetadataRouterHandler } from './credential-metadata-router'
18 changes: 18 additions & 0 deletions packages/credential-metadata-provider-credo/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"compilerOptions": {
"module": "es2022",
"moduleResolution": "bundler",
"target": "ES2022",
"declaration": true,
"sourceMap": true,
"strict": true,
"skipLibCheck": true,
"noEmitOnError": true,
"lib": ["ES2022"],
"types": ["node"],
"esModuleInterop": true,
"allowSyntheticDefaultImports": true
},
"include": ["src"],
"exclude": ["dist"]
}
Loading
Loading