s-m-r-t

@happyvertical/smrt-profiles

Comprehensive profile management system with flexible metadata, complex relationships, and multi-provider authentication support (OIDC, Nostr, API keys).

v0.19.0IdentityAuthESM

Overview

The @happyvertical/smrt-profiles package enables managing profiles of any type (people, organizations, robots) with flexible metadata, complex relationships, and integrated authentication support.

Key Features

  • Flexible Profile Types: Define custom types (Person, Organization, Team, Robot)
  • Controlled Metadata: EAV pattern with controlled vocabulary and validation
  • Complex Relationships: Reciprocal, directional, with temporal terms and context
  • Multi-Provider Auth: OIDC (Google, GitHub, Keycloak), Nostr, API keys
  • Email-Based Linking: Same verified email = same profile
  • Temporal Tracking: Employment history, membership periods
  • Audit Logging: Complete action trail for compliance
  • Multi-Tenancy: Database-level isolation

Architecture

┌─────────────────────────────────────────────────────────────┐
│                   SMRT Profiles System                       │
├─────────────────────────────────────────────────────────────┤
│                                                               │
│  ┌──────────────────────────────────────────────────────┐  │
│  │              Core Models                              │  │
│  ├──────────────────────────────────────────────────────┤  │
│  │ • Profile (Person, Org, Robot, etc.)                 │  │
│  │ • ProfileType (type definitions with slugs)          │  │
│  │ • ProfileMetadata + ProfileMetafield (controlled)    │  │
│  │ • ProfileRelationship + ProfileRelationshipType      │  │
│  │ • ProfileRelationshipTerm (temporal tracking)        │  │
│  └──────────────────────────────────────────────────────┘  │
│                                                               │
│  ┌──────────────────────────────────────────────────────┐  │
│  │         Authentication Models                        │  │
│  ├──────────────────────────────────────────────────────┤  │
│  │ • OidcIdentity (Google, GitHub, Keycloak)           │  │
│  │ • NostrIdentity (encrypted keypairs)                 │  │
│  │ • ApiKey (hashed, scoped, expirable)                 │  │
│  │ • MagicLinkToken (passwordless)                     │  │
│  └──────────────────────────────────────────────────────┘  │
│                                                               │
│  ┌──────────────────────────────────────────────────────┐  │
│  │           Security & Compliance                      │  │
│  ├──────────────────────────────────────────────────────┤  │
│  │ • AuditLog (all actions tracked)                    │  │
│  │ • Email verification (OIDC)                          │  │
│  │ • Master secret encryption (Nostr)                   │  │
│  └──────────────────────────────────────────────────────┘  │
└─────────────────────────────────────────────────────────────┘
			

Use Cases

  • Multi-tenant SaaS platforms with user profiles
  • Social networks and collaboration tools
  • Employee directories and organizational charts
  • Web3 identity systems with Nostr
  • Healthcare provider networks
  • Educational institutions (students, teachers, courses)

Installation

Using pnpm (recommended)

bash
pnpm add @happyvertical/smrt-profiles

Using npm

bash
npm install @happyvertical/smrt-profiles

Using bun

bash
bun add @happyvertical/smrt-profiles

Database Setup

Requires database configuration (SQLite, PostgreSQL, etc.):

typescript
import { smrt } from '@happyvertical/smrt-core';

await smrt.initialize({
  db: { type: 'sqlite', url: 'profiles.db' }
});

Quick Start (5 Minutes)

1. Create Profile Type

typescript
import {
  ProfileCollection,
  ProfileTypeCollection
} from '@happyvertical/smrt-profiles';

// Create profile type
const typeCollection = await ProfileTypeCollection.create({ db: {...} });
const humanType = await typeCollection.create({ name: 'Human' });
await humanType.save();
console.log(humanType.slug); // 'human'

2. Create Profile

typescript
// Create profile
const profileCollection = await ProfileCollection.create({ db: {...} });
const person = await profileCollection.create({
  typeId: humanType.id,
  name: 'Alice Johnson',
  email: 'alice@example.com',
  description: 'Software engineer'
});
await person.save();

3. Add Metadata

typescript
import { ProfileMetafieldCollection } from '@happyvertical/smrt-profiles';

// Define metafield with validation
const fieldCollection = await ProfileMetafieldCollection.create({ db: {...} });
const location = await fieldCollection.create({
  name: 'Location',
  validation: { type: 'string', maxLength: 100 }
});
await location.save();

// Add metadata to profile
await person.addMetadata('location', 'San Francisco, CA');
const metadata = await person.getMetadata();
// { location: 'San Francisco, CA' }

4. Create Relationships

typescript
import {
  ProfileRelationshipTypeCollection
} from '@happyvertical/smrt-profiles';

// Create relationship type (reciprocal)
const relTypeCollection = await ProfileRelationshipTypeCollection.create({ db: {...} });
const friendType = await relTypeCollection.create({
  name: 'Friend',
  reciprocal: true // Auto-creates inverse
});
await friendType.save();

// Create relationship (automatically creates reciprocal)
const bob = await profileCollection.get({ email: 'bob@example.com' });
await person.addRelationship(bob, 'friend');

// Query relationships
const friends = await person.getRelatedProfiles('friend');
console.log(friends.length); // 1 (Bob)

Core Concepts

1. Profile Type System

ProfileType is a lookup table that classifies profiles (Person, Organization, Robot, Team, etc.).

typescript
// Create multiple types
const personType = await typeCollection.create({ name: 'Person' });
const orgType = await typeCollection.create({ name: 'Organization' });
const teamType = await typeCollection.create({ name: 'Team' });

// Use type when creating profiles
const employee = await profileCollection.create({
  typeId: personType.id,
  name: 'Jane Doe'
});

// Get type slug
const slug = await employee.getTypeSlug(); // 'person'

// Set type by slug
await employee.setTypeBySlug('organization');

2. Metadata System (Controlled EAV)

Flexible key-value storage with controlled vocabulary and validation.

Architecture

  • ProfileMetafield: Defines allowed keys (controlled vocabulary)
  • ProfileMetadata: Stores actual values
  • Validation: Each metafield can define validation rules
typescript
// Define metafield with validation
const phone = await fieldCollection.create({
  name: 'Phone Number',
  validation: {
    type: 'string',
    pattern: '^\+?[0-9]{10,15}$',
    message: 'Must be valid phone number'
  }
});
await phone.save();

// Add metadata (validated before save)
await person.addMetadata('phone-number', '+14155551234');

// Get all metadata
const metadata = await person.getMetadata();
// { 'phone-number': '+14155551234', location: 'SF' }

// Update multiple
await person.updateMetadata({
  location: 'New York',
  'job-title': 'Senior Engineer'
});

// Remove metadata
await person.removeMetadata('location');

Validation Types

TypePropertiesExample
stringpattern, minLength, maxLengthEmail, phone, URL
numbermin, maxAge, salary, score
boolean-Active, verified
date-Start date, birthdate
json-Complex objects

3. Relationship System

ProfileRelationship connects two profiles with a relationship type. Relationships can be directional or reciprocal.

Reciprocal Relationships

typescript
// Create reciprocal type
const spouseType = await relTypeCollection.create({
  name: 'Spouse',
  reciprocal: true
});
await spouseType.save();

// Add relationship (auto-creates inverse)
await alice.addRelationship(bob, 'spouse');

// Both profiles now have the relationship
const aliceSpouses = await alice.getRelatedProfiles('spouse'); // [Bob]
const bobSpouses = await bob.getRelatedProfiles('spouse'); // [Alice]

Directional Relationships

typescript
// Create directional type
const managerType = await relTypeCollection.create({
  name: 'Manager',
  reciprocal: false
});
await managerType.save();

// Add one-way relationship
await employee.addRelationship(manager, 'manager');

// Only employee has manager
const managers = await employee.getRelatedProfiles('manager'); // [Manager]
const reports = await manager.getRelatedProfiles('manager'); // []

Context Profiles

Optional tertiary profile for modeling complex relationships.

typescript
// Colleagues through a company
const company = await profileCollection.create({
  typeId: orgType.id,
  name: 'Acme Corp'
});
await company.save();

await alice.addRelationship(bob, 'colleague', company);

// Query with context
const colleagues = await alice.getRelatedProfiles('colleague');
// Can filter by context in advanced queries

4. Temporal Relationships (Terms)

ProfileRelationshipTerm tracks time-based periods for relationships (employment history, memberships, etc.).

typescript
// Add relationship with term
const employeeRel = await employee.addRelationship(company, 'works-at');
await employeeRel.addTerm(new Date('2023-01-15'), null); // Currently employed

// End current employment
await employeeRel.endCurrentTerm(new Date('2024-06-30'));

// Add new employment period
await employeeRel.addTerm(new Date('2024-07-01'), null);

// Query history
const terms = await employeeRel.getTerms();
// [
//   { startDate: 2023-01-15, endDate: 2024-06-30 },
//   { startDate: 2024-07-01, endDate: null }
// ]

// Get current active term
const activeTerm = await employeeRel.getActiveTerm();

Authentication

OIDC Integration

Support for any OpenID Connect provider (Google, GitHub, Keycloak, custom) with email-based account linking.

typescript
import {
  createProfileFromOidc,
  resolveIdentity
} from '@happyvertical/smrt-profiles';

// On OIDC login callback
const oidcClaims = {
  sub: 'google-user-123',
  iss: 'https://accounts.google.com',
  email: 'user@example.com',
  email_verified: true,
  name: 'John Doe'
};

// Create or link profile
const { profile, oidcIdentity, created } = await createProfileFromOidc(
  oidcClaims,
  'google',
  { db: {...} }
);

if (created) {
  console.log('New profile created');
} else {
  console.log('Linked to existing profile via email');
}

Email-Based Account Linking

  • Same verified email = same profile across providers
  • Security: only links when email_verified: true
  • Multiple OIDC identities per profile supported

Authentication Resolution

typescript
// In authentication middleware
const { profile, source } = await resolveIdentity({
  oidcSession: event.locals.session,
  apiKey: event.request.headers.get('X-API-Key'),
  db: event.locals.db
});

if (!profile) {
  return error(401, 'Unauthorized');
}

// Store in context
event.locals.profile = profile;
event.locals.authSource = source; // 'oidc', 'api-key', etc.

Resolution Priority

  1. API key header (highest priority for programmatic access)
  2. OIDC session (web users)
  3. Nostr authentication (NIP-42 signed events)
  4. Actor header (CI pass-through)
  5. None (anonymous)

API Keys

Secure API keys for programmatic access with scopes and expiration.

typescript
// Generate API key for profile
const apiKey = await profile.generateApiKey({
  name: 'CI/CD Bot',
  scopes: ['read:profiles', 'write:profiles'],
  expiresAt: new Date('2025-12-31')
});

console.log(apiKey.key); // 'smrt_1234567890abcdef...'
console.log(apiKey.keyPrefix); // 'smrt_123' (for identification)

// Verify API key
const verified = await ApiKey.verify(apiKey.key);
if (verified?.isValid()) {
  const profile = await verified.getProfile();
  if (verified.hasScope('read:profiles')) {
    // Allow access
  }
}

// Revoke key
await apiKey.revoke();

// List active keys
const activeKeys = await profile.getActiveApiKeys();

API Key Security

  • SHA-256 hashed storage (never plaintext)
  • Prefix stored for identification
  • Scopes/permissions system
  • Expiration and revocation support
  • Usage tracking (lastUsedAt)

Nostr Identity

Decentralized identity with custodial keypair management.

typescript
import {
  generateNostrKeypair,
  createMagicLinkToken
} from '@happyvertical/smrt-profiles';

// Generate keypair (encrypted with master secret)
const keypair = await generateNostrKeypair('user@example.com');

// Create magic link token
const token = await createMagicLinkToken(profile, keypair);

// Send email with magic link
await sendEmail({
  to: profile.email,
  subject: 'Sign in to your account',
  body: `Click here: https://app.example.com/auth/magic?${token}`
});

// Verify magic link
const verified = await verifyMagicLinkToken(token);
if (verified) {
  // Sign in user
}

Tutorials

Tutorial 1: Building an Employee Directory

Create an employee directory with organizational structure, metadata, and relationship tracking.

Step 1: Create Profile Types

typescript
const personType = await typeCollection.create({ name: 'Person' });
const orgType = await typeCollection.create({ name: 'Organization' });
const deptType = await typeCollection.create({ name: 'Department' });
await Promise.all([personType.save(), orgType.save(), deptType.save()]);

Step 2: Create Organization and Departments

typescript
const company = await profileCollection.create({
  typeId: orgType.id,
  name: 'Acme Corporation',
  description: 'Technology company'
});
await company.save();

const engineering = await profileCollection.create({
  typeId: deptType.id,
  name: 'Engineering'
});
await engineering.save();

await engineering.addRelationship(company, 'part-of');

Step 3: Create Employees with Metadata

typescript
// Define metadata fields
const jobTitleField = await fieldCollection.create({
  name: 'Job Title',
  validation: { type: 'string', maxLength: 100 }
});
const startDateField = await fieldCollection.create({
  name: 'Start Date',
  validation: { type: 'date' }
});
await Promise.all([jobTitleField.save(), startDateField.save()]);

// Create employee
const employee = await profileCollection.create({
  typeId: personType.id,
  name: 'Alice Johnson',
  email: 'alice@acme.com'
});
await employee.save();

// Add metadata
await employee.addMetadata('job-title', 'Senior Software Engineer');
await employee.addMetadata('start-date', new Date('2023-01-15'));

Step 4: Create Relationships with Terms

typescript
// Employee works at company
const employmentRel = await employee.addRelationship(company, 'works-at');
await employmentRel.addTerm(new Date('2023-01-15'), null); // Current

// Employee in department
await employee.addRelationship(engineering, 'member-of');

// Colleague relationship with context
const bob = await profileCollection.create({
  typeId: personType.id,
  name: 'Bob Smith',
  email: 'bob@acme.com'
});
await bob.save();

await employee.addRelationship(bob, 'colleague', company);

Step 5: Query and Analytics

typescript
// Get all employees
const employees = await profileCollection.list({
  where: { typeId: personType.id }
});

// Get employee's colleagues
const colleagues = await employee.getRelatedProfiles('colleague');

// Get employment history
const workRel = await employee.getRelationships({ slug: 'works-at' });
const terms = await workRel[0].getTerms();

Tutorial 2: OIDC-Based Multi-Tenant SaaS

Implement OIDC authentication with email-based account linking across tenants.

Step 1: Configure OIDC Providers

typescript
// Configure in your auth provider (e.g., Auth.js, Keycloak)
const providers = [
  GoogleProvider({
    clientId: process.env.GOOGLE_CLIENT_ID,
    clientSecret: process.env.GOOGLE_CLIENT_SECRET
  }),
  GitHubProvider({
    clientId: process.env.GITHUB_CLIENT_ID,
    clientSecret: process.env.GITHUB_CLIENT_SECRET
  })
];

Step 2: Handle OIDC Callback

typescript
// In your OIDC callback handler
export async function handleOidcCallback(oidcClaims, provider) {
  const { profile, oidcIdentity, created } = await createProfileFromOidc(
    oidcClaims,
    provider,
    { db: getTenantDb() }
  );

  // Store tenant ID in metadata
  if (created) {
    await profile.addMetadata('tenant-id', getCurrentTenantId());
  }

  // Create session
  const session = await createSession({
    profileId: profile.id,
    provider: oidcIdentity.provider
  });

  return session;
}

Step 3: Authentication Middleware

typescript
// In hooks.server.ts or middleware
export async function handle({ event, resolve }) {
  const { profile, source } = await resolveIdentity({
    oidcSession: await getSession(event.cookies),
    apiKey: event.request.headers.get('X-API-Key'),
    db: event.locals.db
  });

  if (profile) {
    event.locals.profile = profile;
    event.locals.authSource = source;

    // Get tenant from metadata
    const metadata = await profile.getMetadata();
    event.locals.tenantId = metadata['tenant-id'];
  }

  return resolve(event);
}

Tutorial 3: Social Network with Reciprocal Relationships

Build a social network with friends, followers, and custom relationship types.

typescript
// Create relationship types
const friendType = await relTypeCollection.create({
  name: 'Friend',
  reciprocal: true
});
const followerType = await relTypeCollection.create({
  name: 'Follower',
  reciprocal: false
});
await Promise.all([friendType.save(), followerType.save()]);

// Add friends (auto-reciprocal)
await alice.addRelationship(bob, 'friend');

// Both have the relationship
const aliceFriends = await alice.getRelatedProfiles('friend'); // [Bob]
const bobFriends = await bob.getRelatedProfiles('friend'); // [Alice]

// Add follower (one-way)
await alice.addRelationship(charlie, 'follower');

// Query friend network at depth 2
const friendsOfFriends = await alice.getRelationshipNetwork({
  slug: 'friend',
  depth: 2
});

// Analytics
const followerCount = (await alice.getRelatedProfiles('follower')).length;
const mutualFriends = await findMutualFriends(alice, bob);

Examples

Example 1: Healthcare Provider Network

typescript
// Profile types
const physicianType = await typeCollection.create({ name: 'Physician' });
const patientType = await typeCollection.create({ name: 'Patient' });
const clinicType = await typeCollection.create({ name: 'Clinic' });

// Create profiles
const drSmith = await profileCollection.create({
  typeId: physicianType.id,
  name: 'Dr. Sarah Smith',
  email: 'sarah.smith@clinic.com'
});

const clinic = await profileCollection.create({
  typeId: clinicType.id,
  name: 'Downtown Medical Clinic'
});

// Metadata
await drSmith.addMetadata('specialty', 'Cardiology');
await drSmith.addMetadata('license-number', 'CA12345');

// Relationships
await drSmith.addRelationship(clinic, 'works-at');

// Referrals with context
const drJones = await profileCollection.create({
  typeId: physicianType.id,
  name: 'Dr. Michael Jones'
});

await drSmith.addRelationship(drJones, 'refers-to', clinic);

// Audit logging
await drSmith.recordAction({
  action: 'patient_viewed',
  resourceType: 'Patient',
  resourceId: 'patient-123',
  source: 'web',
  metadata: { reason: 'Follow-up appointment' }
});

Example 2: Educational Institution

typescript
// Profile types
const studentType = await typeCollection.create({ name: 'Student' });
const teacherType = await typeCollection.create({ name: 'Teacher' });
const courseType = await typeCollection.create({ name: 'Course' });

// Create profiles
const student = await profileCollection.create({
  typeId: studentType.id,
  name: 'Emma Wilson',
  email: 'emma.wilson@university.edu'
});

const course = await profileCollection.create({
  typeId: courseType.id,
  name: 'Computer Science 101'
});

// Enrollment with term
const enrollmentRel = await student.addRelationship(course, 'enrolled-in');
await enrollmentRel.addTerm(
  new Date('2024-09-01'),
  new Date('2024-12-15')
);

// Metadata
await student.addMetadata('grade-level', 'Freshman');
await student.addMetadata('gpa', 3.8);

// Query courses
const courses = await student.getRelatedProfiles('enrolled-in');

// Get enrollment history
const enrollments = await student.getRelationships({ slug: 'enrolled-in' });
const history = await Promise.all(
  enrollments.map(async (rel) => ({
    course: await rel.getToProfile(),
    terms: await rel.getTerms()
  }))
);

Integration with Other Modules

smrt-users

The profiles package provides identity and profile management that complements smrt-users:

  • smrt-users: Authentication, sessions, permissions
  • smrt-profiles: Profile data, relationships, metadata
typescript
// User logs in via OIDC (handled by smrt-users)
// Profile is created or retrieved via smrt-profiles
const { profile } = await resolveIdentity(authContext);

// Store profile reference in user session
event.locals.profile = profile;

// Use profile for authorization
const tenantId = (await profile.getMetadata())['tenant-id'];
const membership = await getUserTenantMembership(profile.id, tenantId);

smrt-core

  • All models extend SmrtObject for database persistence
  • Automatic API generation (REST, CLI, MCP)
  • AI-powered methods (generateBio(), matches())

Multi-Tenancy Pattern

typescript
// Tenant A database
const profilesA = await ProfileCollection.create({
  db: { type: 'postgres', url: DATABASE_URL_TENANT_A }
});

// Tenant B database
const profilesB = await ProfileCollection.create({
  db: { type: 'postgres', url: DATABASE_URL_TENANT_B }
});

// Complete isolation at database level

Best Practices

1. Profile Design

  • Use descriptive names for profile types (Person vs User, Team vs Group)
  • Normalize emails to lowercase for reliable lookups
  • Always provide profile descriptions for context
  • Don't mix profile types unnecessarily

2. Metadata Management

  • Define metafields (vocabulary) upfront before adding metadata
  • Use appropriate validation types and constraints
  • Namespace keys with dot notation (contact.phone, social.github)
  • Use batch operations for efficiency
typescript
// Good - namespaced
await profile.updateMetadata({
  'contact.phone': '+14155551234',
  'contact.email': 'user@example.com',
  'social.github': 'username',
  'social.linkedin': 'username'
});

// Bad - no namespace
await profile.updateMetadata({
  phone: '+14155551234',
  email: 'user@example.com',
  github: 'username'
});

3. Relationships

  • Choose relationship types that accurately represent connections
  • Use reciprocal: true for symmetric relationships (friend, spouse)
  • Add context profiles when relationships depend on intermediary
  • Always add terms for temporal relationships (employment, membership)

4. Authentication

  • Only link OIDC accounts with email_verified: true
  • Implement regular API key rotation and revocation
  • Use specific scopes rather than * permission
  • Combine OIDC with additional verification for sensitive operations

5. Security

  • Record all sensitive actions in audit logs
  • Never store plaintext API keys (handled automatically)
  • Secure SERVER_MASTER_SECRET for Nostr decryption
  • Implement rate limiting at middleware level

6. Performance

  • Use batch operations for bulk imports/updates
  • Use pagination (limit and offset) for large result sets
  • Limit relationship network traversal depth (default 2-3)
  • Consider caching frequently accessed metadata

Troubleshooting

Profile type not found

Solution: Create profile types before creating profiles

typescript
// Wrong order
const profile = await profileCollection.create({
  typeId: 'person', // Error: type doesn't exist
  name: 'John'
});

// Correct order
const personType = await typeCollection.create({ name: 'Person' });
await personType.save();

const profile = await profileCollection.create({
  typeId: personType.id, // Works
  name: 'John'
});

OIDC identity not linking to profile

Solution: Ensure email_verified is true

typescript
// Won't link (security)
const claims = {
  email: 'user@example.com',
  email_verified: false // ❌
};

// Will link
const claims = {
  email: 'user@example.com',
  email_verified: true // ✅
};

Reciprocal relationship not creating inverse

Solution: Set reciprocal: true on relationship type

typescript
// Wrong - no reciprocal
const friendType = await relTypeCollection.create({
  name: 'Friend',
  reciprocal: false // ❌
});

// Correct
const friendType = await relTypeCollection.create({
  name: 'Friend',
  reciprocal: true // ✅
});

Metadata validation failing

Solution: Check validation rules and value format

typescript
// Define validation
const phoneField = await fieldCollection.create({
  name: 'Phone',
  validation: {
    type: 'string',
    pattern: '^\+?[0-9]{10,15}$'
  }
});

// Will fail
await profile.addMetadata('phone', '123'); // Too short

// Will succeed
await profile.addMetadata('phone', '+14155551234');

Multi-tenant data leaking

Solution: Pass correct database URL for each tenant

typescript
// Wrong - shared database
const collection = await ProfileCollection.create({ db: sharedDb });

// Correct - tenant-specific
const tenantACollection = await ProfileCollection.create({
  db: { type: 'postgres', url: TENANT_A_URL }
});
const tenantBCollection = await ProfileCollection.create({
  db: { type: 'postgres', url: TENANT_B_URL }
});

API Reference

Profile Model

MethodReturnsDescription
addMetadata(slug, value)Promise<void>Add single metadata value
getMetadata()Promise<object>Get all metadata as key-value object
updateMetadata(obj)Promise<void>Update multiple metadata values
removeMetadata(slug)Promise<void>Remove metadata by slug
addRelationship(to, slug, context?)Promise<ProfileRelationship>Create relationship
getRelationships(options?)Promise<ProfileRelationship[]>Get relationships with filtering
getRelatedProfiles(slug?)Promise<Profile[]>Get related profiles
generateApiKey(options)Promise<ApiKey>Generate new API key
getActiveApiKeys()Promise<ApiKey[]>List non-expired, non-revoked keys
getOidcIdentities()Promise<OidcIdentity[]>List linked OIDC identities
recordAction(options)Promise<AuditLog>Log action for audit trail
generateBio()Promise<string>Generate professional bio using AI

ProfileRelationship Model

MethodReturnsDescription
addTerm(start, end?)Promise<ProfileRelationshipTerm>Add time-based period
endCurrentTerm(endDate)Promise<void>Close active term
getActiveTerm()Promise&lt;{ profile, source } | null&gt;Get current active term
getTerms()Promise<ProfileRelationshipTerm[]>Get all historical terms
getTypeSlug()Promise<string>Get relationship type slug

Utility Functions

FunctionReturnsDescription
createProfileFromOidc(claims, provider, options)Promise&lt;{ profile, oidcIdentity, created }&gt;Create or link profile from OIDC
resolveIdentity(context)Promise&lt;{ profile, source } | null&gt;Resolve authentication
ApiKey.verify(key)Promise<ApiKey | null>Verify and validate API key