@happyvertical/smrt-profiles 0.51.9 → 0.51.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +27 -0
- package/dist/chunks/{ApiKey-BTC7nWG1.js → ApiKey-DIqCzx-g.js} +4 -3
- package/dist/chunks/ApiKey-DIqCzx-g.js.map +1 -0
- package/dist/chunks/{ApiKeyCollection-DsxaLEwc.js → ApiKeyCollection-CNFptBxU.js} +3 -3
- package/dist/chunks/{ApiKeyCollection-DsxaLEwc.js.map → ApiKeyCollection-CNFptBxU.js.map} +1 -1
- package/dist/chunks/{AuditLogCollection-N5ItRtEK.js → AuditLogCollection-DxR3ZG6_.js} +2 -2
- package/dist/chunks/{AuditLogCollection-N5ItRtEK.js.map → AuditLogCollection-DxR3ZG6_.js.map} +1 -1
- package/dist/chunks/{NostrIdentity-C39j4O4k.js → NostrIdentity-CbnAJgg9.js} +5 -4
- package/dist/chunks/{NostrIdentity-C39j4O4k.js.map → NostrIdentity-CbnAJgg9.js.map} +1 -1
- package/dist/chunks/{NostrIdentityCollection-PzvQ54Nm.js → NostrIdentityCollection-BXtvB0Sd.js} +2 -2
- package/dist/chunks/{NostrIdentityCollection-PzvQ54Nm.js.map → NostrIdentityCollection-BXtvB0Sd.js.map} +1 -1
- package/dist/chunks/{OidcIdentity-DjV_QkWf.js → OidcIdentity-DISriiph.js} +3 -3
- package/dist/chunks/{OidcIdentity-DjV_QkWf.js.map → OidcIdentity-DISriiph.js.map} +1 -1
- package/dist/chunks/{OidcIdentityCollection-DjHxIEKC.js → OidcIdentityCollection-B6im_oL_.js} +3 -3
- package/dist/chunks/{OidcIdentityCollection-DjHxIEKC.js.map → OidcIdentityCollection-B6im_oL_.js.map} +1 -1
- package/dist/chunks/{ProfileAssetCollection-D1WJn8UN.js → ProfileAssetCollection-CuwaiTOk.js} +3 -3
- package/dist/chunks/{ProfileAssetCollection-D1WJn8UN.js.map → ProfileAssetCollection-CuwaiTOk.js.map} +1 -1
- package/dist/chunks/{ProfileCollection-kMs3qYEZ.js → ProfileCollection-BAC5weFA.js} +76 -76
- package/dist/chunks/{ProfileCollection-kMs3qYEZ.js.map → ProfileCollection-BAC5weFA.js.map} +1 -1
- package/dist/chunks/{ProfileMetadataCollection-BqOi6QaP.js → ProfileMetadataCollection-DyVxb-Tn.js} +2 -2
- package/dist/chunks/{ProfileMetadataCollection-BqOi6QaP.js.map → ProfileMetadataCollection-DyVxb-Tn.js.map} +1 -1
- package/dist/chunks/{ProfileMetafieldCollection-C-jB8Zdj.js → ProfileMetafieldCollection-KSYFXBxU.js} +2 -2
- package/dist/chunks/{ProfileMetafieldCollection-C-jB8Zdj.js.map → ProfileMetafieldCollection-KSYFXBxU.js.map} +1 -1
- package/dist/chunks/{ProfileRelationshipCollection-DvTSsUBr.js → ProfileRelationshipCollection-DigPX3kC.js} +5 -5
- package/dist/chunks/{ProfileRelationshipCollection-DvTSsUBr.js.map → ProfileRelationshipCollection-DigPX3kC.js.map} +1 -1
- package/dist/chunks/{ProfileRelationshipTermCollection-C2bd2BSh.js → ProfileRelationshipTermCollection-UkhmGHcr.js} +2 -2
- package/dist/chunks/{ProfileRelationshipTermCollection-C2bd2BSh.js.map → ProfileRelationshipTermCollection-UkhmGHcr.js.map} +1 -1
- package/dist/chunks/{ProfileRelationshipType-BtEked7O.js → ProfileRelationshipType-D1WLqC2B.js} +2 -2
- package/dist/chunks/{ProfileRelationshipType-BtEked7O.js.map → ProfileRelationshipType-D1WLqC2B.js.map} +1 -1
- package/dist/chunks/{ProfileRelationshipTypeCollection-zT0bdM5A.js → ProfileRelationshipTypeCollection-BapU-NS3.js} +3 -3
- package/dist/chunks/{ProfileRelationshipTypeCollection-zT0bdM5A.js.map → ProfileRelationshipTypeCollection-BapU-NS3.js.map} +1 -1
- package/dist/chunks/{oidcProvisioningPrimitives-BAlBZaYo.js → oidcProvisioningPrimitives-BPWWsNb7.js} +4 -4
- package/dist/chunks/{oidcProvisioningPrimitives-BAlBZaYo.js.map → oidcProvisioningPrimitives-BPWWsNb7.js.map} +1 -1
- package/dist/chunks/{resolveIdentity-DfXnbA4H.js → resolveIdentity-QEWj65PQ.js} +16 -16
- package/dist/chunks/{resolveIdentity-DfXnbA4H.js.map → resolveIdentity-QEWj65PQ.js.map} +1 -1
- package/dist/index.d.ts +49 -0
- package/dist/index.js +63 -23
- package/dist/index.js.map +1 -1
- package/dist/internal/oidc-provisioning.js +2 -2
- package/dist/manifest.json +4 -1
- package/dist/smrt-knowledge.json +5 -5
- package/dist/utils.js +4 -4
- package/package.json +7 -6
- package/dist/chunks/ApiKey-BTC7nWG1.js.map +0 -1
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":[],"sources":["../src/__smrt-register__.ts","../src/models/MagicLinkToken.ts","../src/collections/MagicLinkTokenCollection.ts","../src/auth/magicLinkService.ts","../src/auth/nip05Handler.ts"],"sourcesContent":["/**\n * Self-registers this package's build-time manifest before any @smrt() decorator\n * in the package fires. Fixes issue #1132: in consumer runtimes (tsx, SvelteKit\n * SSR, plain `vite dev`) the decorator's synchronous manifest lookup previously\n * missed because no step populated the global manifest cache — classes got\n * registered with zero fields and `save()` / `toJSON()` silently dropped every\n * declared property.\n *\n * Import this module as the first statement in `src/index.ts` so its top-level\n * side effect runs ahead of any class module's @smrt() decorator.\n *\n * Silent no-op in dev/test, where the vitest plugin already populates manifests\n * via a different path. Only needs to succeed in the published dist output.\n *\n * @see https://github.com/happyvertical/smrt/issues/1132\n */\nimport { ObjectRegistry } from '@happyvertical/smrt-core';\n\n// During library builds, smrtPlugin replaces this entire module with generated\n// code that embeds the scanned manifest inline (#1506/#1507) — published dists\n// never resolve this URL, so downstream bundlers cannot break registration by\n// relocating the compiled module away from dist/manifest.json. The runtime\n// lookup below is the fallback for source-mode runs without that transform.\nObjectRegistry.registerPackageManifest(\n new URL('./manifest.json', import.meta.url),\n);\n","/**\n * MagicLinkToken - Temporary tokens for email verification\n *\n * Stores hashed magic link tokens with expiration. Tokens are single-use\n * and automatically invalidated after verification or expiration.\n */\n\nimport { createHash, randomBytes } from 'node:crypto';\nimport {\n field,\n foreignKey,\n SmrtObject,\n type SmrtObjectOptions,\n smrt,\n} from '@happyvertical/smrt-core';\nimport type { NostrIdentity } from './NostrIdentity';\n\nexport interface MagicLinkTokenOptions extends SmrtObjectOptions {\n nostrIdentityId?: string;\n tokenHash?: string;\n email?: string;\n expiresAt?: Date;\n usedAt?: Date | null;\n requestedFromIp?: string;\n createdAt?: Date;\n}\n\nexport interface GenerateTokenResult {\n /** The plaintext token (only available at creation time) */\n token: string;\n /** The MagicLinkToken record */\n magicLinkToken: MagicLinkToken;\n}\n\n/** Default token expiration: 15 minutes */\nconst DEFAULT_EXPIRATION_MINUTES = 15;\n\n@smrt({\n tableName: 'magic_link_tokens',\n api: { exclude: ['*'] }, // No API access - internal only\n mcp: { exclude: ['*'] },\n cli: { include: ['list'] }, // Admin visibility only\n})\nexport class MagicLinkToken extends SmrtObject {\n /**\n * Link to the NostrIdentity this token is for\n */\n @foreignKey('NostrIdentity', { required: true })\n nostrIdentityId?: string;\n\n /**\n * SHA-256 hash of the token (never store plaintext).\n *\n * Indexed: `verify()` looks tokens up by this hash on every magic-link\n * click (#2364, epic #2382 finding A3).\n */\n @field({ indexed: true })\n tokenHash: string = '';\n\n /**\n * Email this token was sent to\n */\n email: string = '';\n\n /**\n * When this token expires\n */\n expiresAt: Date = new Date(\n Date.now() + DEFAULT_EXPIRATION_MINUTES * 60 * 1000,\n );\n\n /**\n * When this token was used (null = unused)\n */\n usedAt: Date | null = null;\n\n /**\n * IP address that requested the token (for rate limiting)\n */\n requestedFromIp: string = '';\n\n /**\n * When this token was created (for rate limiting)\n */\n createdAt: Date = new Date();\n\n constructor(options: MagicLinkTokenOptions = {}) {\n super(options);\n if (options.nostrIdentityId) this.nostrIdentityId = options.nostrIdentityId;\n if (options.tokenHash) this.tokenHash = options.tokenHash;\n if (options.email) this.email = options.email;\n if (options.expiresAt) this.expiresAt = options.expiresAt;\n if (options.usedAt !== undefined) this.usedAt = options.usedAt;\n if (options.requestedFromIp) this.requestedFromIp = options.requestedFromIp;\n if (options.createdAt) this.createdAt = options.createdAt;\n }\n\n /**\n * Hash a token using SHA-256\n */\n static hashToken(token: string): string {\n return createHash('sha256').update(token).digest('hex');\n }\n\n /**\n * Generate a new magic link token\n * @param nostrIdentity - The identity this token is for\n * @param email - The email address to send to\n * @param options - Additional options\n */\n static async generate(\n nostrIdentity: NostrIdentity,\n email: string,\n options: {\n expiresInMinutes?: number;\n requestedFromIp?: string;\n db?: SmrtObjectOptions['db'];\n } = {},\n ): Promise<GenerateTokenResult> {\n // Generate 32 bytes of random data (256 bits of entropy)\n const tokenBytes = randomBytes(32);\n const token = tokenBytes.toString('base64url');\n const tokenHash = MagicLinkToken.hashToken(token);\n\n const expiresInMinutes =\n options.expiresInMinutes ?? DEFAULT_EXPIRATION_MINUTES;\n const expiresAt = new Date(Date.now() + expiresInMinutes * 60 * 1000);\n\n const magicLinkToken = new MagicLinkToken({\n db: options.db ?? nostrIdentity.options?.db,\n nostrIdentityId: nostrIdentity.id as string,\n tokenHash,\n email: email.toLowerCase(),\n expiresAt,\n requestedFromIp: options.requestedFromIp || '',\n });\n\n await magicLinkToken.initialize();\n await magicLinkToken.save();\n\n return { token, magicLinkToken };\n }\n\n /**\n * Verify a token and return the MagicLinkToken if valid\n * @param token - The plaintext token to verify\n * @param options - Database options\n * @returns The MagicLinkToken if valid, null otherwise\n */\n static async verify(\n token: string,\n options: SmrtObjectOptions = {},\n ): Promise<MagicLinkToken | null> {\n const tokenHash = MagicLinkToken.hashToken(token);\n\n const { MagicLinkTokenCollection } = await import(\n '../collections/MagicLinkTokenCollection'\n );\n const collection = await MagicLinkTokenCollection.create(options);\n\n const magicLinkToken = await collection.findOne({\n where: { tokenHash },\n });\n\n if (!magicLinkToken) {\n return null;\n }\n\n // Check if valid\n if (!magicLinkToken.isValid()) {\n return null;\n }\n\n return magicLinkToken;\n }\n\n /**\n * Check if this token is valid (not expired and not used)\n */\n isValid(): boolean {\n if (this.usedAt !== null) {\n return false;\n }\n if (new Date() > this.expiresAt) {\n return false;\n }\n return true;\n }\n\n /**\n * Check if this token has expired\n */\n isExpired(): boolean {\n return new Date() > this.expiresAt;\n }\n\n /**\n * Check if this token has been used\n */\n isUsed(): boolean {\n return this.usedAt !== null;\n }\n\n /**\n * Mark this token as used\n */\n async markUsed(): Promise<void> {\n this.usedAt = new Date();\n await this.save();\n }\n\n /**\n * Get the NostrIdentity this token is for\n */\n async getNostrIdentity(): Promise<NostrIdentity | null> {\n return (await this.getRelated('nostrIdentityId')) as NostrIdentity | null;\n }\n\n /**\n * Get time remaining until expiration in seconds\n */\n getTimeRemainingSeconds(): number {\n const now = new Date();\n const diff = this.expiresAt.getTime() - now.getTime();\n return Math.max(0, Math.floor(diff / 1000));\n }\n}\n","/**\n * MagicLinkTokenCollection - Collection for managing magic link tokens\n */\n\nimport { SmrtCollection } from '@happyvertical/smrt-core';\nimport { MagicLinkToken } from '../models/MagicLinkToken';\n\nexport class MagicLinkTokenCollection extends SmrtCollection<MagicLinkToken> {\n static readonly _itemClass = MagicLinkToken;\n\n /**\n * Find token by hash\n */\n async findByTokenHash(tokenHash: string): Promise<MagicLinkToken | null> {\n return await this.findOne({\n where: { tokenHash },\n });\n }\n\n /**\n * Find active (non-expired, non-used) tokens for an email\n */\n async findActiveForEmail(email: string): Promise<MagicLinkToken[]> {\n const tokens = await this.list({\n where: { email: email.toLowerCase() },\n });\n return tokens.filter((token) => token.isValid());\n }\n\n /**\n * Find tokens for a NostrIdentity\n */\n async findByIdentity(nostrIdentityId: string): Promise<MagicLinkToken[]> {\n return await this.list({\n where: { nostrIdentityId },\n });\n }\n\n /**\n * Count tokens created for an email within a time window (for rate limiting)\n * @param email - Email address\n * @param withinMinutes - Time window in minutes\n */\n async countRecentByEmail(\n email: string,\n withinMinutes: number,\n ): Promise<number> {\n const cutoff = new Date(Date.now() - withinMinutes * 60 * 1000);\n const tokens = await this.list({\n where: { email: email.toLowerCase() },\n });\n\n // Filter tokens created after cutoff\n // Note: We use createdAt from SmrtObject base\n return tokens.filter((token) => {\n const createdAt = token.createdAt;\n return createdAt && new Date(createdAt) > cutoff;\n }).length;\n }\n\n /**\n * Count tokens created from an IP within a time window (for rate limiting)\n * @param ip - IP address\n * @param withinMinutes - Time window in minutes\n */\n async countRecentByIp(ip: string, withinMinutes: number): Promise<number> {\n const cutoff = new Date(Date.now() - withinMinutes * 60 * 1000);\n const tokens = await this.list({\n where: { requestedFromIp: ip },\n });\n\n // Filter tokens created after cutoff\n return tokens.filter((token) => {\n const createdAt = token.createdAt;\n return createdAt && new Date(createdAt) > cutoff;\n }).length;\n }\n\n /**\n * Verify a token and return it if valid\n */\n async verify(token: string): Promise<MagicLinkToken | null> {\n const tokenHash = MagicLinkToken.hashToken(token);\n const magicLinkToken = await this.findByTokenHash(tokenHash);\n\n if (!magicLinkToken) {\n return null;\n }\n\n if (!magicLinkToken.isValid()) {\n return null;\n }\n\n return magicLinkToken;\n }\n\n /**\n * Clean up expired tokens by deleting them\n * @returns Number of tokens deleted\n */\n async cleanupExpired(): Promise<number> {\n const now = new Date();\n const allTokens = await this.list({});\n let deleted = 0;\n\n for (const token of allTokens) {\n if (token.isExpired() || token.isUsed()) {\n await token.delete();\n deleted++;\n }\n }\n\n return deleted;\n }\n\n /**\n * Revoke all tokens for a NostrIdentity (e.g., when identity is deleted)\n */\n async revokeForIdentity(nostrIdentityId: string): Promise<number> {\n const tokens = await this.findByIdentity(nostrIdentityId);\n let revoked = 0;\n\n for (const token of tokens) {\n if (!token.isUsed()) {\n await token.markUsed(); // Mark as used to invalidate\n revoked++;\n }\n }\n\n return revoked;\n }\n\n /**\n * Check if rate limit is exceeded for email\n * @param email - Email address\n * @param maxPerHour - Maximum tokens allowed per hour (default: 5)\n */\n async isRateLimitedByEmail(\n email: string,\n maxPerHour: number = 5,\n ): Promise<boolean> {\n const count = await this.countRecentByEmail(email, 60);\n return count >= maxPerHour;\n }\n\n /**\n * Check if rate limit is exceeded for IP\n * @param ip - IP address\n * @param maxPerHour - Maximum tokens allowed per hour (default: 10)\n */\n async isRateLimitedByIp(\n ip: string,\n maxPerHour: number = 10,\n ): Promise<boolean> {\n const count = await this.countRecentByIp(ip, 60);\n return count >= maxPerHour;\n }\n}\n","/**\n * Magic Link Service\n *\n * Handles the magic link authentication flow including:\n * - Token generation and storage\n * - Email sending (via callback)\n * - Token verification\n * - Rate limiting checks\n */\n\nimport type { SmrtObjectOptions } from '@happyvertical/smrt-core';\nimport { MagicLinkTokenCollection } from '../collections/MagicLinkTokenCollection';\nimport { NostrIdentityCollection } from '../collections/NostrIdentityCollection';\nimport { MagicLinkToken } from '../models/MagicLinkToken';\nimport type { NostrIdentity } from '../models/NostrIdentity';\nimport type { Profile } from '../models/Profile';\nimport { encryptPrivkey, generateNostrKeypair } from './nostrCrypto';\n\nexport interface MagicLinkConfig {\n /** Base URL for magic links (e.g., \"https://example.com\") */\n baseUrl: string;\n /** Path for verification endpoint (default: \"/auth/verify\") */\n verifyPath?: string;\n /** Token expiration in minutes (default: 15) */\n expiresInMinutes?: number;\n /** Maximum tokens per email per hour (default: 5) */\n maxTokensPerEmailPerHour?: number;\n /** Maximum tokens per IP per hour (default: 10) */\n maxTokensPerIpPerHour?: number;\n /** Server master secret for encryption */\n masterSecret: string;\n /** Callback to send the email */\n sendEmail: (to: string, magicLink: string) => Promise<void>;\n /** Database options */\n db: SmrtObjectOptions['db'];\n}\n\nexport interface InitiateResult {\n success: boolean;\n /** The Nostr identity (existing or new) */\n nostrIdentity?: NostrIdentity;\n /** The profile (existing or new) */\n profile?: Profile;\n /** True if a new profile was created */\n created: boolean;\n /** Error message if success is false */\n error?: string;\n /** Error code for programmatic handling */\n errorCode?: 'RATE_LIMITED_EMAIL' | 'RATE_LIMITED_IP' | 'EMAIL_SEND_FAILED';\n}\n\nexport interface VerifyResult {\n success: boolean;\n /** The profile */\n profile?: Profile;\n /** The Nostr identity */\n nostrIdentity?: NostrIdentity;\n /** The decrypted keypair (only on success) */\n keypair?: {\n pubkey: string;\n privkey: string;\n npub: string;\n nsec: string;\n };\n /** Error message if success is false */\n error?: string;\n /** Error code for programmatic handling */\n errorCode?: 'INVALID_TOKEN' | 'EXPIRED_TOKEN' | 'USED_TOKEN' | 'NOT_FOUND';\n}\n\nexport interface MagicLinkService {\n /**\n * Initiate magic link flow - generates token and sends email\n */\n initiate(\n email: string,\n options?: {\n requestedFromIp?: string;\n nip05Username?: string;\n },\n ): Promise<InitiateResult>;\n\n /**\n * Verify a magic link token and return decrypted keypair\n */\n verify(token: string): Promise<VerifyResult>;\n}\n\n/**\n * Create a magic link service instance\n */\nexport function createMagicLinkService(\n config: MagicLinkConfig,\n): MagicLinkService {\n const {\n baseUrl,\n verifyPath = '/auth/verify',\n expiresInMinutes = 15,\n maxTokensPerEmailPerHour = 5,\n maxTokensPerIpPerHour = 10,\n masterSecret,\n sendEmail,\n db,\n } = config;\n\n return {\n async initiate(\n email: string,\n options: {\n requestedFromIp?: string;\n nip05Username?: string;\n } = {},\n ): Promise<InitiateResult> {\n const normalizedEmail = email.toLowerCase().trim();\n const { requestedFromIp, nip05Username } = options;\n\n // Create collections\n const identityCollection = await NostrIdentityCollection.create({ db });\n const tokenCollection = await MagicLinkTokenCollection.create({ db });\n\n // Rate limiting checks\n if (\n await tokenCollection.isRateLimitedByEmail(\n normalizedEmail,\n maxTokensPerEmailPerHour,\n )\n ) {\n return {\n success: false,\n created: false,\n error: 'Too many requests for this email. Please try again later.',\n errorCode: 'RATE_LIMITED_EMAIL',\n };\n }\n\n if (\n requestedFromIp &&\n (await tokenCollection.isRateLimitedByIp(\n requestedFromIp,\n maxTokensPerIpPerHour,\n ))\n ) {\n return {\n success: false,\n created: false,\n error: 'Too many requests from this IP. Please try again later.',\n errorCode: 'RATE_LIMITED_IP',\n };\n }\n\n // Check if identity exists\n let nostrIdentity = await identityCollection.findByEmail(normalizedEmail);\n let profile: Profile | null = null;\n let created = false;\n\n if (!nostrIdentity) {\n // Create new identity and profile\n const { Person } = await import('../models/ProfileTypes');\n const { ProfileTypeCollection } = await import(\n '../collections/ProfileTypeCollection'\n );\n\n // Profile.typeId is a required FK, so resolve (or seed) the canonical\n // 'person' type before creating the profile — otherwise `.save()`\n // fails required-field validation. Mirrors createProfileFromNostr /\n // createProfileFromOidc in resolveIdentity.ts.\n const typeCollection = await ProfileTypeCollection.create({ db });\n let personType = await typeCollection.getBySlug('person');\n if (!personType) {\n const { ProfileType } = await import('../models/ProfileType');\n personType = new ProfileType({\n db,\n slug: 'person',\n name: 'Person',\n description: 'Individual person profile',\n });\n await personType.initialize();\n await personType.save();\n }\n\n // Create a new profile for this user (Person is an STI subclass of Profile)\n profile = new Person({\n db,\n typeId: personType.id as string,\n email: normalizedEmail,\n name: normalizedEmail.split('@')[0], // Default name from email\n });\n await profile.initialize();\n await profile.save();\n\n // Generate keypair and encrypt\n const keypair = generateNostrKeypair();\n const encrypted = encryptPrivkey(keypair.privkey, masterSecret);\n\n // Create Nostr identity\n const { NostrIdentity: NostrIdentityClass } = await import(\n '../models/NostrIdentity'\n );\n nostrIdentity = new NostrIdentityClass({\n db,\n profileId: profile.id!,\n pubkey: keypair.pubkey,\n encryptedPrivkey: encrypted.ciphertext,\n encryptionIv: encrypted.iv,\n encryptionTag: encrypted.tag,\n email: normalizedEmail,\n nip05Username:\n nip05Username?.toLowerCase() || normalizedEmail.split('@')[0],\n });\n await nostrIdentity.initialize();\n await nostrIdentity.save();\n\n created = true;\n } else {\n profile = await nostrIdentity.getProfile();\n }\n\n // Generate magic link token\n const { token } = await MagicLinkToken.generate(\n nostrIdentity,\n normalizedEmail,\n {\n expiresInMinutes,\n requestedFromIp,\n db,\n },\n );\n\n // Build magic link URL\n const magicLink = `${baseUrl}${verifyPath}?token=${encodeURIComponent(token)}`;\n\n // Send email\n try {\n await sendEmail(normalizedEmail, magicLink);\n } catch (emailError) {\n return {\n success: false,\n created,\n nostrIdentity,\n profile: profile || undefined,\n error: 'Failed to send email. Please try again.',\n errorCode: 'EMAIL_SEND_FAILED',\n };\n }\n\n return {\n success: true,\n created,\n nostrIdentity,\n profile: profile || undefined,\n };\n },\n\n async verify(token: string): Promise<VerifyResult> {\n // Verify token\n const magicLinkToken = await MagicLinkToken.verify(token, { db });\n\n if (!magicLinkToken) {\n // Try to get more specific error\n const tokenHash = MagicLinkToken.hashToken(token);\n const tokenCollection = await MagicLinkTokenCollection.create({ db });\n const existingToken = await tokenCollection.findByTokenHash(tokenHash);\n\n if (!existingToken) {\n return {\n success: false,\n error: 'Invalid or expired token.',\n errorCode: 'NOT_FOUND',\n };\n }\n\n if (existingToken.isUsed()) {\n return {\n success: false,\n error: 'This link has already been used.',\n errorCode: 'USED_TOKEN',\n };\n }\n\n if (existingToken.isExpired()) {\n return {\n success: false,\n error: 'This link has expired. Please request a new one.',\n errorCode: 'EXPIRED_TOKEN',\n };\n }\n\n return {\n success: false,\n error: 'Invalid token.',\n errorCode: 'INVALID_TOKEN',\n };\n }\n\n // Mark token as used\n await magicLinkToken.markUsed();\n\n // Get the Nostr identity\n const nostrIdentity = await magicLinkToken.getNostrIdentity();\n if (!nostrIdentity) {\n return {\n success: false,\n error: 'Identity not found.',\n errorCode: 'NOT_FOUND',\n };\n }\n\n // Mark identity as verified\n await nostrIdentity.markVerified();\n\n // Get profile\n const profile = await nostrIdentity.getProfile();\n\n // Decrypt keypair\n const keypair = nostrIdentity.getKeypair(masterSecret);\n\n return {\n success: true,\n profile: profile || undefined,\n nostrIdentity,\n keypair,\n };\n },\n };\n}\n","/**\n * NIP-05 Handler\n *\n * Provides a helper for serving the /.well-known/nostr.json endpoint\n * for NIP-05 identity verification.\n *\n * @see https://github.com/nostr-protocol/nips/blob/master/05.md\n */\n\nimport type { SmrtObjectOptions } from '@happyvertical/smrt-core';\nimport {\n type Nip05Response,\n NostrIdentityCollection,\n} from '../collections/NostrIdentityCollection';\n\nexport interface Nip05HandlerConfig {\n /** Database options */\n db: SmrtObjectOptions['db'];\n /** Optional: Additional relays to include for all users */\n defaultRelays?: string[];\n /** Optional: Cache TTL in seconds (default: 300 = 5 minutes) */\n cacheTtlSeconds?: number;\n}\n\nexport interface Nip05Request {\n /** The 'name' query parameter from the request */\n name?: string | null;\n}\n\nexport interface Nip05HandlerResult {\n /** The NIP-05 response body */\n body: Nip05Response;\n /** Suggested headers for the response */\n headers: Record<string, string>;\n /** HTTP status code */\n status: number;\n}\n\n/**\n * Create a NIP-05 handler\n *\n * @example\n * // SvelteKit\n * import { createNip05Handler } from '@happyvertical/smrt-profiles';\n *\n * const handler = createNip05Handler({ db: { type: 'postgres', url: DATABASE_URL } });\n *\n * export async function GET({ url }) {\n * const result = await handler({ name: url.searchParams.get('name') });\n * return new Response(JSON.stringify(result.body), {\n * status: result.status,\n * headers: result.headers,\n * });\n * }\n *\n * @example\n * // Express\n * import { createNip05Handler } from '@happyvertical/smrt-profiles';\n *\n * const handler = createNip05Handler({ db: { type: 'postgres', url: DATABASE_URL } });\n *\n * app.get('/.well-known/nostr.json', async (req, res) => {\n * const result = await handler({ name: req.query.name });\n * res.status(result.status).set(result.headers).json(result.body);\n * });\n */\nexport function createNip05Handler(config: Nip05HandlerConfig) {\n const { db, defaultRelays = [], cacheTtlSeconds = 300 } = config;\n\n return async function handleNip05Request(\n request: Nip05Request,\n ): Promise<Nip05HandlerResult> {\n const { name } = request;\n\n const collection = await NostrIdentityCollection.create({ db });\n const response = await collection.getNip05Response(name || undefined);\n\n // Add default relays if configured\n if (defaultRelays.length > 0 && Object.keys(response.names).length > 0) {\n response.relays = {};\n for (const pubkey of Object.values(response.names)) {\n response.relays[pubkey] = defaultRelays;\n }\n }\n\n // If specific name requested but not found, return empty names\n if (name && Object.keys(response.names).length === 0) {\n return {\n body: { names: {} },\n headers: {\n 'Content-Type': 'application/json',\n 'Access-Control-Allow-Origin': '*',\n 'Cache-Control': `public, max-age=${cacheTtlSeconds}`,\n },\n status: 200, // NIP-05 spec says return 200 with empty names, not 404\n };\n }\n\n return {\n body: response,\n headers: {\n 'Content-Type': 'application/json',\n 'Access-Control-Allow-Origin': '*',\n 'Cache-Control': `public, max-age=${cacheTtlSeconds}`,\n },\n status: 200,\n };\n };\n}\n\n/**\n * Validate a NIP-05 identifier format\n * @param identifier - The identifier to validate (e.g., \"alice@example.com\")\n */\nexport function isValidNip05Identifier(identifier: string): boolean {\n // NIP-05 format: <local-part>@<domain>\n const parts = identifier.split('@');\n if (parts.length !== 2) {\n return false;\n }\n\n const [localPart, domain] = parts;\n\n // Local part: alphanumeric, _, -\n if (!/^[a-z0-9_-]+$/i.test(localPart)) {\n return false;\n }\n\n // Domain: basic domain validation\n if (!/^[a-z0-9.-]+\\.[a-z]{2,}$/i.test(domain)) {\n return false;\n }\n\n return true;\n}\n\n/**\n * Parse a NIP-05 identifier into its parts\n * @param identifier - The identifier to parse (e.g., \"alice@example.com\")\n */\nexport function parseNip05Identifier(identifier: string): {\n localPart: string;\n domain: string;\n} | null {\n if (!isValidNip05Identifier(identifier)) {\n return null;\n }\n\n const [localPart, domain] = identifier.split('@');\n return { localPart, domain };\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../src/__smrt-register__.ts","../src/agent-profile.ts","../src/models/MagicLinkToken.ts","../src/collections/MagicLinkTokenCollection.ts","../src/auth/magicLinkService.ts","../src/auth/nip05Handler.ts"],"sourcesContent":["/**\n * Self-registers this package's build-time manifest before any @smrt() decorator\n * in the package fires. Fixes issue #1132: in consumer runtimes (tsx, SvelteKit\n * SSR, plain `vite dev`) the decorator's synchronous manifest lookup previously\n * missed because no step populated the global manifest cache — classes got\n * registered with zero fields and `save()` / `toJSON()` silently dropped every\n * declared property.\n *\n * Import this module as the first statement in `src/index.ts` so its top-level\n * side effect runs ahead of any class module's @smrt() decorator.\n *\n * Silent no-op in dev/test, where the vitest plugin already populates manifests\n * via a different path. Only needs to succeed in the published dist output.\n *\n * @see https://github.com/happyvertical/smrt/issues/1132\n */\nimport { ObjectRegistry } from '@happyvertical/smrt-core';\n\n// During library builds, smrtPlugin replaces this entire module with generated\n// code that embeds the scanned manifest inline (#1506/#1507) — published dists\n// never resolve this URL, so downstream bundlers cannot break registration by\n// relocating the compiled module away from dist/manifest.json. The runtime\n// lookup below is the fallback for source-mode runs without that transform.\nObjectRegistry.registerPackageManifest(\n new URL('./manifest.json', import.meta.url),\n);\n","/**\n * Agent (bot) profile resolution (#2995).\n *\n * An automated agent that participates in a conversation is still an author,\n * and every authoring seam in the framework — `ChatMessage.senderProfileId`,\n * `ChatParticipant.profileId`, `ChatReaction.profileId`, a persona's\n * `actsAsProfileId` — is a `crossPackageRef` to a {@link Profile} and therefore\n * a native uuid column on PostgreSQL/DuckDB. A slug-style agent identifier\n * (`anytown_site_assistant`) is not a representable author there: it fails the\n * uuid cast (22P02) at the boundary that owns it.\n *\n * `Profile` already models this: the `bot` profile type is \"automated agents,\n * bots, and AI entities\". This module is the owning-package API that turns a\n * stable agent identifier into that Profile's uuid, so consumers never have to\n * mint bot profiles themselves and never have to weaken a uuid column to text.\n *\n * Identity is keyed on `(tenantId, slug = agentId, context =\n * {@link AGENT_PROFILE_CONTEXT})`. The reserved slug context keeps an agent\n * profile from ever colliding with an unrelated human/organization profile that\n * happens to share the slug, and matches the model's\n * `(tenant_id, slug, context, _meta_type)` unique key.\n *\n * @module\n */\n\nimport type { ProfileCollection } from './collections/ProfileCollection';\nimport { ProfileTypeCollection } from './collections/ProfileTypeCollection';\nimport type { Profile } from './models/Profile';\n\n/**\n * Reserved slug context for agent-owned profiles.\n *\n * Agent identifiers come from application configuration, not from the profile\n * namespace, so they are scoped into their own slug context rather than\n * competing with person/organization slugs.\n */\nexport const AGENT_PROFILE_CONTEXT = 'smrt:agent';\n\n/** Slug of the {@link ProfileType} agent profiles are classified under. */\nexport const AGENT_PROFILE_TYPE_SLUG = 'bot';\n\n/** Inputs for {@link resolveAgentProfile}. */\nexport interface ResolveAgentProfileParams {\n /** Stable agent identifier (e.g. `anytown_site_assistant`). */\n agentId: string;\n /** Owning tenant, or `null` for an untenanted agent. */\n tenantId?: string | null;\n /** Display name for a newly created profile. Defaults to `agentId`. */\n name?: string;\n}\n\n/**\n * Resolve — creating on first use — the {@link Profile} an agent authors as.\n *\n * The lookup is tenant-bound: an agent identifier resolves to a distinct\n * profile per tenant, so one tenant's assistant can never author into another\n * tenant's conversation. Resolution is idempotent; repeated calls for the same\n * `(tenantId, agentId)` return the same profile.\n *\n * @param profiles - Profile collection bound to the caller's database options.\n * @param params - Agent identity to resolve.\n * @returns The agent's profile, whose `id` is a real uuid.\n * @throws Error when `agentId` is empty.\n */\nexport async function resolveAgentProfile(\n profiles: ProfileCollection,\n params: ResolveAgentProfileParams,\n): Promise<Profile> {\n const agentId = params.agentId?.trim();\n if (!agentId) {\n throw new Error('resolveAgentProfile requires a non-empty agentId');\n }\n const tenantId = params.tenantId ?? null;\n const key = { slug: agentId, context: AGENT_PROFILE_CONTEXT, tenantId };\n\n // First use races (two simultaneous first turns for one agent) are resolved by\n // converging on the natural key rather than by whoever's INSERT won: each\n // attempt re-reads before and after writing, so a racer whose own write lost\n // to a revision conflict or to the `(tenant_id, slug, context)` upsert still\n // returns the surviving row instead of a dangling id. Two attempts suffice —\n // the loser of the first is a plain read on the second.\n let lastError: unknown;\n for (let attempt = 0; attempt < 2; attempt++) {\n const existing = await profiles.get(key);\n if (existing) return existing;\n\n try {\n const profileTypes = await ProfileTypeCollection.create(profiles.options);\n const agentType = await profileTypes.getOrCreateBySlug(\n AGENT_PROFILE_TYPE_SLUG,\n {\n name: 'Bot',\n description: 'Automated agents, bots, and AI entities',\n },\n );\n\n const profile = await profiles.create({\n ...key,\n typeId: agentType.id as string,\n name: params.name?.trim() || agentId,\n });\n await profile.save();\n } catch (error) {\n lastError = error;\n }\n\n const settled = await profiles.get(key);\n if (settled) return settled;\n }\n\n throw new Error(\n `Could not resolve an agent profile for '${agentId}'${\n lastError instanceof Error ? `: ${lastError.message}` : ''\n }`,\n );\n}\n\n/**\n * Resolve an agent profile and return its uuid.\n *\n * Convenience wrapper for the common case — an authoring seam that needs the\n * id, not the row.\n *\n * @param profiles - Profile collection bound to the caller's database options.\n * @param params - Agent identity to resolve.\n * @returns The agent profile's uuid.\n */\nexport async function resolveAgentProfileId(\n profiles: ProfileCollection,\n params: ResolveAgentProfileParams,\n): Promise<string> {\n const profile = await resolveAgentProfile(profiles, params);\n const id = profile.id;\n if (typeof id !== 'string' || id.length === 0) {\n throw new Error(\n `Agent profile for '${params.agentId}' was persisted without an id`,\n );\n }\n return id;\n}\n","/**\n * MagicLinkToken - Temporary tokens for email verification\n *\n * Stores hashed magic link tokens with expiration. Tokens are single-use\n * and automatically invalidated after verification or expiration.\n */\n\nimport { createHash, randomBytes } from 'node:crypto';\nimport {\n field,\n foreignKey,\n SmrtObject,\n type SmrtObjectOptions,\n smrt,\n} from '@happyvertical/smrt-core';\nimport type { NostrIdentity } from './NostrIdentity';\n\nexport interface MagicLinkTokenOptions extends SmrtObjectOptions {\n nostrIdentityId?: string;\n tokenHash?: string;\n email?: string;\n expiresAt?: Date;\n usedAt?: Date | null;\n requestedFromIp?: string;\n createdAt?: Date;\n}\n\nexport interface GenerateTokenResult {\n /** The plaintext token (only available at creation time) */\n token: string;\n /** The MagicLinkToken record */\n magicLinkToken: MagicLinkToken;\n}\n\n/** Default token expiration: 15 minutes */\nconst DEFAULT_EXPIRATION_MINUTES = 15;\n\n@smrt({\n tableName: 'magic_link_tokens',\n // Carries the login-token hash `verify()` looks up by. Never in the change\n // feed (#2937).\n sensitive: true,\n api: { exclude: ['*'] }, // No API access - internal only\n mcp: { exclude: ['*'] },\n cli: { include: ['list'] }, // Admin visibility only\n})\nexport class MagicLinkToken extends SmrtObject {\n /**\n * Link to the NostrIdentity this token is for\n */\n @foreignKey('NostrIdentity', { required: true })\n nostrIdentityId?: string;\n\n /**\n * SHA-256 hash of the token (never store plaintext).\n *\n * Indexed: `verify()` looks tokens up by this hash on every magic-link\n * click (#2364, epic #2382 finding A3).\n */\n @field({ indexed: true })\n tokenHash: string = '';\n\n /**\n * Email this token was sent to\n */\n email: string = '';\n\n /**\n * When this token expires\n */\n expiresAt: Date = new Date(\n Date.now() + DEFAULT_EXPIRATION_MINUTES * 60 * 1000,\n );\n\n /**\n * When this token was used (null = unused)\n */\n usedAt: Date | null = null;\n\n /**\n * IP address that requested the token (for rate limiting)\n */\n requestedFromIp: string = '';\n\n /**\n * When this token was created (for rate limiting)\n */\n createdAt: Date = new Date();\n\n constructor(options: MagicLinkTokenOptions = {}) {\n super(options);\n if (options.nostrIdentityId) this.nostrIdentityId = options.nostrIdentityId;\n if (options.tokenHash) this.tokenHash = options.tokenHash;\n if (options.email) this.email = options.email;\n if (options.expiresAt) this.expiresAt = options.expiresAt;\n if (options.usedAt !== undefined) this.usedAt = options.usedAt;\n if (options.requestedFromIp) this.requestedFromIp = options.requestedFromIp;\n if (options.createdAt) this.createdAt = options.createdAt;\n }\n\n /**\n * Hash a token using SHA-256\n */\n static hashToken(token: string): string {\n return createHash('sha256').update(token).digest('hex');\n }\n\n /**\n * Generate a new magic link token\n * @param nostrIdentity - The identity this token is for\n * @param email - The email address to send to\n * @param options - Additional options\n */\n static async generate(\n nostrIdentity: NostrIdentity,\n email: string,\n options: {\n expiresInMinutes?: number;\n requestedFromIp?: string;\n db?: SmrtObjectOptions['db'];\n } = {},\n ): Promise<GenerateTokenResult> {\n // Generate 32 bytes of random data (256 bits of entropy)\n const tokenBytes = randomBytes(32);\n const token = tokenBytes.toString('base64url');\n const tokenHash = MagicLinkToken.hashToken(token);\n\n const expiresInMinutes =\n options.expiresInMinutes ?? DEFAULT_EXPIRATION_MINUTES;\n const expiresAt = new Date(Date.now() + expiresInMinutes * 60 * 1000);\n\n const magicLinkToken = new MagicLinkToken({\n db: options.db ?? nostrIdentity.options?.db,\n nostrIdentityId: nostrIdentity.id as string,\n tokenHash,\n email: email.toLowerCase(),\n expiresAt,\n requestedFromIp: options.requestedFromIp || '',\n });\n\n await magicLinkToken.initialize();\n await magicLinkToken.save();\n\n return { token, magicLinkToken };\n }\n\n /**\n * Verify a token and return the MagicLinkToken if valid\n * @param token - The plaintext token to verify\n * @param options - Database options\n * @returns The MagicLinkToken if valid, null otherwise\n */\n static async verify(\n token: string,\n options: SmrtObjectOptions = {},\n ): Promise<MagicLinkToken | null> {\n const tokenHash = MagicLinkToken.hashToken(token);\n\n const { MagicLinkTokenCollection } = await import(\n '../collections/MagicLinkTokenCollection'\n );\n const collection = await MagicLinkTokenCollection.create(options);\n\n const magicLinkToken = await collection.findOne({\n where: { tokenHash },\n });\n\n if (!magicLinkToken) {\n return null;\n }\n\n // Check if valid\n if (!magicLinkToken.isValid()) {\n return null;\n }\n\n return magicLinkToken;\n }\n\n /**\n * Check if this token is valid (not expired and not used)\n */\n isValid(): boolean {\n if (this.usedAt !== null) {\n return false;\n }\n if (new Date() > this.expiresAt) {\n return false;\n }\n return true;\n }\n\n /**\n * Check if this token has expired\n */\n isExpired(): boolean {\n return new Date() > this.expiresAt;\n }\n\n /**\n * Check if this token has been used\n */\n isUsed(): boolean {\n return this.usedAt !== null;\n }\n\n /**\n * Mark this token as used\n */\n async markUsed(): Promise<void> {\n this.usedAt = new Date();\n await this.save();\n }\n\n /**\n * Get the NostrIdentity this token is for\n */\n async getNostrIdentity(): Promise<NostrIdentity | null> {\n return (await this.getRelated('nostrIdentityId')) as NostrIdentity | null;\n }\n\n /**\n * Get time remaining until expiration in seconds\n */\n getTimeRemainingSeconds(): number {\n const now = new Date();\n const diff = this.expiresAt.getTime() - now.getTime();\n return Math.max(0, Math.floor(diff / 1000));\n }\n}\n","/**\n * MagicLinkTokenCollection - Collection for managing magic link tokens\n */\n\nimport { SmrtCollection } from '@happyvertical/smrt-core';\nimport { MagicLinkToken } from '../models/MagicLinkToken';\n\nexport class MagicLinkTokenCollection extends SmrtCollection<MagicLinkToken> {\n static readonly _itemClass = MagicLinkToken;\n\n /**\n * Find token by hash\n */\n async findByTokenHash(tokenHash: string): Promise<MagicLinkToken | null> {\n return await this.findOne({\n where: { tokenHash },\n });\n }\n\n /**\n * Find active (non-expired, non-used) tokens for an email\n */\n async findActiveForEmail(email: string): Promise<MagicLinkToken[]> {\n const tokens = await this.list({\n where: { email: email.toLowerCase() },\n });\n return tokens.filter((token) => token.isValid());\n }\n\n /**\n * Find tokens for a NostrIdentity\n */\n async findByIdentity(nostrIdentityId: string): Promise<MagicLinkToken[]> {\n return await this.list({\n where: { nostrIdentityId },\n });\n }\n\n /**\n * Count tokens created for an email within a time window (for rate limiting)\n * @param email - Email address\n * @param withinMinutes - Time window in minutes\n */\n async countRecentByEmail(\n email: string,\n withinMinutes: number,\n ): Promise<number> {\n const cutoff = new Date(Date.now() - withinMinutes * 60 * 1000);\n const tokens = await this.list({\n where: { email: email.toLowerCase() },\n });\n\n // Filter tokens created after cutoff\n // Note: We use createdAt from SmrtObject base\n return tokens.filter((token) => {\n const createdAt = token.createdAt;\n return createdAt && new Date(createdAt) > cutoff;\n }).length;\n }\n\n /**\n * Count tokens created from an IP within a time window (for rate limiting)\n * @param ip - IP address\n * @param withinMinutes - Time window in minutes\n */\n async countRecentByIp(ip: string, withinMinutes: number): Promise<number> {\n const cutoff = new Date(Date.now() - withinMinutes * 60 * 1000);\n const tokens = await this.list({\n where: { requestedFromIp: ip },\n });\n\n // Filter tokens created after cutoff\n return tokens.filter((token) => {\n const createdAt = token.createdAt;\n return createdAt && new Date(createdAt) > cutoff;\n }).length;\n }\n\n /**\n * Verify a token and return it if valid\n */\n async verify(token: string): Promise<MagicLinkToken | null> {\n const tokenHash = MagicLinkToken.hashToken(token);\n const magicLinkToken = await this.findByTokenHash(tokenHash);\n\n if (!magicLinkToken) {\n return null;\n }\n\n if (!magicLinkToken.isValid()) {\n return null;\n }\n\n return magicLinkToken;\n }\n\n /**\n * Clean up expired tokens by deleting them\n * @returns Number of tokens deleted\n */\n async cleanupExpired(): Promise<number> {\n const now = new Date();\n const allTokens = await this.list({});\n let deleted = 0;\n\n for (const token of allTokens) {\n if (token.isExpired() || token.isUsed()) {\n await token.delete();\n deleted++;\n }\n }\n\n return deleted;\n }\n\n /**\n * Revoke all tokens for a NostrIdentity (e.g., when identity is deleted)\n */\n async revokeForIdentity(nostrIdentityId: string): Promise<number> {\n const tokens = await this.findByIdentity(nostrIdentityId);\n let revoked = 0;\n\n for (const token of tokens) {\n if (!token.isUsed()) {\n await token.markUsed(); // Mark as used to invalidate\n revoked++;\n }\n }\n\n return revoked;\n }\n\n /**\n * Check if rate limit is exceeded for email\n * @param email - Email address\n * @param maxPerHour - Maximum tokens allowed per hour (default: 5)\n */\n async isRateLimitedByEmail(\n email: string,\n maxPerHour: number = 5,\n ): Promise<boolean> {\n const count = await this.countRecentByEmail(email, 60);\n return count >= maxPerHour;\n }\n\n /**\n * Check if rate limit is exceeded for IP\n * @param ip - IP address\n * @param maxPerHour - Maximum tokens allowed per hour (default: 10)\n */\n async isRateLimitedByIp(\n ip: string,\n maxPerHour: number = 10,\n ): Promise<boolean> {\n const count = await this.countRecentByIp(ip, 60);\n return count >= maxPerHour;\n }\n}\n","/**\n * Magic Link Service\n *\n * Handles the magic link authentication flow including:\n * - Token generation and storage\n * - Email sending (via callback)\n * - Token verification\n * - Rate limiting checks\n */\n\nimport type { SmrtObjectOptions } from '@happyvertical/smrt-core';\nimport { MagicLinkTokenCollection } from '../collections/MagicLinkTokenCollection';\nimport { NostrIdentityCollection } from '../collections/NostrIdentityCollection';\nimport { MagicLinkToken } from '../models/MagicLinkToken';\nimport type { NostrIdentity } from '../models/NostrIdentity';\nimport type { Profile } from '../models/Profile';\nimport { encryptPrivkey, generateNostrKeypair } from './nostrCrypto';\n\nexport interface MagicLinkConfig {\n /** Base URL for magic links (e.g., \"https://example.com\") */\n baseUrl: string;\n /** Path for verification endpoint (default: \"/auth/verify\") */\n verifyPath?: string;\n /** Token expiration in minutes (default: 15) */\n expiresInMinutes?: number;\n /** Maximum tokens per email per hour (default: 5) */\n maxTokensPerEmailPerHour?: number;\n /** Maximum tokens per IP per hour (default: 10) */\n maxTokensPerIpPerHour?: number;\n /** Server master secret for encryption */\n masterSecret: string;\n /** Callback to send the email */\n sendEmail: (to: string, magicLink: string) => Promise<void>;\n /** Database options */\n db: SmrtObjectOptions['db'];\n}\n\nexport interface InitiateResult {\n success: boolean;\n /** The Nostr identity (existing or new) */\n nostrIdentity?: NostrIdentity;\n /** The profile (existing or new) */\n profile?: Profile;\n /** True if a new profile was created */\n created: boolean;\n /** Error message if success is false */\n error?: string;\n /** Error code for programmatic handling */\n errorCode?: 'RATE_LIMITED_EMAIL' | 'RATE_LIMITED_IP' | 'EMAIL_SEND_FAILED';\n}\n\nexport interface VerifyResult {\n success: boolean;\n /** The profile */\n profile?: Profile;\n /** The Nostr identity */\n nostrIdentity?: NostrIdentity;\n /** The decrypted keypair (only on success) */\n keypair?: {\n pubkey: string;\n privkey: string;\n npub: string;\n nsec: string;\n };\n /** Error message if success is false */\n error?: string;\n /** Error code for programmatic handling */\n errorCode?: 'INVALID_TOKEN' | 'EXPIRED_TOKEN' | 'USED_TOKEN' | 'NOT_FOUND';\n}\n\nexport interface MagicLinkService {\n /**\n * Initiate magic link flow - generates token and sends email\n */\n initiate(\n email: string,\n options?: {\n requestedFromIp?: string;\n nip05Username?: string;\n },\n ): Promise<InitiateResult>;\n\n /**\n * Verify a magic link token and return decrypted keypair\n */\n verify(token: string): Promise<VerifyResult>;\n}\n\n/**\n * Create a magic link service instance\n */\nexport function createMagicLinkService(\n config: MagicLinkConfig,\n): MagicLinkService {\n const {\n baseUrl,\n verifyPath = '/auth/verify',\n expiresInMinutes = 15,\n maxTokensPerEmailPerHour = 5,\n maxTokensPerIpPerHour = 10,\n masterSecret,\n sendEmail,\n db,\n } = config;\n\n return {\n async initiate(\n email: string,\n options: {\n requestedFromIp?: string;\n nip05Username?: string;\n } = {},\n ): Promise<InitiateResult> {\n const normalizedEmail = email.toLowerCase().trim();\n const { requestedFromIp, nip05Username } = options;\n\n // Create collections\n const identityCollection = await NostrIdentityCollection.create({ db });\n const tokenCollection = await MagicLinkTokenCollection.create({ db });\n\n // Rate limiting checks\n if (\n await tokenCollection.isRateLimitedByEmail(\n normalizedEmail,\n maxTokensPerEmailPerHour,\n )\n ) {\n return {\n success: false,\n created: false,\n error: 'Too many requests for this email. Please try again later.',\n errorCode: 'RATE_LIMITED_EMAIL',\n };\n }\n\n if (\n requestedFromIp &&\n (await tokenCollection.isRateLimitedByIp(\n requestedFromIp,\n maxTokensPerIpPerHour,\n ))\n ) {\n return {\n success: false,\n created: false,\n error: 'Too many requests from this IP. Please try again later.',\n errorCode: 'RATE_LIMITED_IP',\n };\n }\n\n // Check if identity exists\n let nostrIdentity = await identityCollection.findByEmail(normalizedEmail);\n let profile: Profile | null = null;\n let created = false;\n\n if (!nostrIdentity) {\n // Create new identity and profile\n const { Person } = await import('../models/ProfileTypes');\n const { ProfileTypeCollection } = await import(\n '../collections/ProfileTypeCollection'\n );\n\n // Profile.typeId is a required FK, so resolve (or seed) the canonical\n // 'person' type before creating the profile — otherwise `.save()`\n // fails required-field validation. Mirrors createProfileFromNostr /\n // createProfileFromOidc in resolveIdentity.ts.\n const typeCollection = await ProfileTypeCollection.create({ db });\n let personType = await typeCollection.getBySlug('person');\n if (!personType) {\n const { ProfileType } = await import('../models/ProfileType');\n personType = new ProfileType({\n db,\n slug: 'person',\n name: 'Person',\n description: 'Individual person profile',\n });\n await personType.initialize();\n await personType.save();\n }\n\n // Create a new profile for this user (Person is an STI subclass of Profile)\n profile = new Person({\n db,\n typeId: personType.id as string,\n email: normalizedEmail,\n name: normalizedEmail.split('@')[0], // Default name from email\n });\n await profile.initialize();\n await profile.save();\n\n // Generate keypair and encrypt\n const keypair = generateNostrKeypair();\n const encrypted = encryptPrivkey(keypair.privkey, masterSecret);\n\n // Create Nostr identity\n const { NostrIdentity: NostrIdentityClass } = await import(\n '../models/NostrIdentity'\n );\n nostrIdentity = new NostrIdentityClass({\n db,\n profileId: profile.id!,\n pubkey: keypair.pubkey,\n encryptedPrivkey: encrypted.ciphertext,\n encryptionIv: encrypted.iv,\n encryptionTag: encrypted.tag,\n email: normalizedEmail,\n nip05Username:\n nip05Username?.toLowerCase() || normalizedEmail.split('@')[0],\n });\n await nostrIdentity.initialize();\n await nostrIdentity.save();\n\n created = true;\n } else {\n profile = await nostrIdentity.getProfile();\n }\n\n // Generate magic link token\n const { token } = await MagicLinkToken.generate(\n nostrIdentity,\n normalizedEmail,\n {\n expiresInMinutes,\n requestedFromIp,\n db,\n },\n );\n\n // Build magic link URL\n const magicLink = `${baseUrl}${verifyPath}?token=${encodeURIComponent(token)}`;\n\n // Send email\n try {\n await sendEmail(normalizedEmail, magicLink);\n } catch (emailError) {\n return {\n success: false,\n created,\n nostrIdentity,\n profile: profile || undefined,\n error: 'Failed to send email. Please try again.',\n errorCode: 'EMAIL_SEND_FAILED',\n };\n }\n\n return {\n success: true,\n created,\n nostrIdentity,\n profile: profile || undefined,\n };\n },\n\n async verify(token: string): Promise<VerifyResult> {\n // Verify token\n const magicLinkToken = await MagicLinkToken.verify(token, { db });\n\n if (!magicLinkToken) {\n // Try to get more specific error\n const tokenHash = MagicLinkToken.hashToken(token);\n const tokenCollection = await MagicLinkTokenCollection.create({ db });\n const existingToken = await tokenCollection.findByTokenHash(tokenHash);\n\n if (!existingToken) {\n return {\n success: false,\n error: 'Invalid or expired token.',\n errorCode: 'NOT_FOUND',\n };\n }\n\n if (existingToken.isUsed()) {\n return {\n success: false,\n error: 'This link has already been used.',\n errorCode: 'USED_TOKEN',\n };\n }\n\n if (existingToken.isExpired()) {\n return {\n success: false,\n error: 'This link has expired. Please request a new one.',\n errorCode: 'EXPIRED_TOKEN',\n };\n }\n\n return {\n success: false,\n error: 'Invalid token.',\n errorCode: 'INVALID_TOKEN',\n };\n }\n\n // Mark token as used\n await magicLinkToken.markUsed();\n\n // Get the Nostr identity\n const nostrIdentity = await magicLinkToken.getNostrIdentity();\n if (!nostrIdentity) {\n return {\n success: false,\n error: 'Identity not found.',\n errorCode: 'NOT_FOUND',\n };\n }\n\n // Mark identity as verified\n await nostrIdentity.markVerified();\n\n // Get profile\n const profile = await nostrIdentity.getProfile();\n\n // Decrypt keypair\n const keypair = nostrIdentity.getKeypair(masterSecret);\n\n return {\n success: true,\n profile: profile || undefined,\n nostrIdentity,\n keypair,\n };\n },\n };\n}\n","/**\n * NIP-05 Handler\n *\n * Provides a helper for serving the /.well-known/nostr.json endpoint\n * for NIP-05 identity verification.\n *\n * @see https://github.com/nostr-protocol/nips/blob/master/05.md\n */\n\nimport type { SmrtObjectOptions } from '@happyvertical/smrt-core';\nimport {\n type Nip05Response,\n NostrIdentityCollection,\n} from '../collections/NostrIdentityCollection';\n\nexport interface Nip05HandlerConfig {\n /** Database options */\n db: SmrtObjectOptions['db'];\n /** Optional: Additional relays to include for all users */\n defaultRelays?: string[];\n /** Optional: Cache TTL in seconds (default: 300 = 5 minutes) */\n cacheTtlSeconds?: number;\n}\n\nexport interface Nip05Request {\n /** The 'name' query parameter from the request */\n name?: string | null;\n}\n\nexport interface Nip05HandlerResult {\n /** The NIP-05 response body */\n body: Nip05Response;\n /** Suggested headers for the response */\n headers: Record<string, string>;\n /** HTTP status code */\n status: number;\n}\n\n/**\n * Create a NIP-05 handler\n *\n * @example\n * // SvelteKit\n * import { createNip05Handler } from '@happyvertical/smrt-profiles';\n *\n * const handler = createNip05Handler({ db: { type: 'postgres', url: DATABASE_URL } });\n *\n * export async function GET({ url }) {\n * const result = await handler({ name: url.searchParams.get('name') });\n * return new Response(JSON.stringify(result.body), {\n * status: result.status,\n * headers: result.headers,\n * });\n * }\n *\n * @example\n * // Express\n * import { createNip05Handler } from '@happyvertical/smrt-profiles';\n *\n * const handler = createNip05Handler({ db: { type: 'postgres', url: DATABASE_URL } });\n *\n * app.get('/.well-known/nostr.json', async (req, res) => {\n * const result = await handler({ name: req.query.name });\n * res.status(result.status).set(result.headers).json(result.body);\n * });\n */\nexport function createNip05Handler(config: Nip05HandlerConfig) {\n const { db, defaultRelays = [], cacheTtlSeconds = 300 } = config;\n\n return async function handleNip05Request(\n request: Nip05Request,\n ): Promise<Nip05HandlerResult> {\n const { name } = request;\n\n const collection = await NostrIdentityCollection.create({ db });\n const response = await collection.getNip05Response(name || undefined);\n\n // Add default relays if configured\n if (defaultRelays.length > 0 && Object.keys(response.names).length > 0) {\n response.relays = {};\n for (const pubkey of Object.values(response.names)) {\n response.relays[pubkey] = defaultRelays;\n }\n }\n\n // If specific name requested but not found, return empty names\n if (name && Object.keys(response.names).length === 0) {\n return {\n body: { names: {} },\n headers: {\n 'Content-Type': 'application/json',\n 'Access-Control-Allow-Origin': '*',\n 'Cache-Control': `public, max-age=${cacheTtlSeconds}`,\n },\n status: 200, // NIP-05 spec says return 200 with empty names, not 404\n };\n }\n\n return {\n body: response,\n headers: {\n 'Content-Type': 'application/json',\n 'Access-Control-Allow-Origin': '*',\n 'Cache-Control': `public, max-age=${cacheTtlSeconds}`,\n },\n status: 200,\n };\n };\n}\n\n/**\n * Validate a NIP-05 identifier format\n * @param identifier - The identifier to validate (e.g., \"alice@example.com\")\n */\nexport function isValidNip05Identifier(identifier: string): boolean {\n // NIP-05 format: <local-part>@<domain>\n const parts = identifier.split('@');\n if (parts.length !== 2) {\n return false;\n }\n\n const [localPart, domain] = parts;\n\n // Local part: alphanumeric, _, -\n if (!/^[a-z0-9_-]+$/i.test(localPart)) {\n return false;\n }\n\n // Domain: basic domain validation\n if (!/^[a-z0-9.-]+\\.[a-z]{2,}$/i.test(domain)) {\n return false;\n }\n\n return true;\n}\n\n/**\n * Parse a NIP-05 identifier into its parts\n * @param identifier - The identifier to parse (e.g., \"alice@example.com\")\n */\nexport function parseNip05Identifier(identifier: string): {\n localPart: string;\n domain: string;\n} | null {\n if (!isValidNip05Identifier(identifier)) {\n return null;\n }\n\n const [localPart, domain] = identifier.split('@');\n return { localPart, domain };\n}\n"],"mappings":"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"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { d as
|
|
2
|
-
import { n as insertOidcProvisioningIdentity$1, t as createOidcProvisioningPerson$1 } from "../chunks/oidcProvisioningPrimitives-
|
|
1
|
+
import { d as isOidcProvisioningRaceConflict, f as saveOidcRaceArbiter, l as coordinateOidcProvisioning, u as isOidcAbortedTransactionError } from "../chunks/ProfileCollection-BAC5weFA.js";
|
|
2
|
+
import { n as insertOidcProvisioningIdentity$1, t as createOidcProvisioningPerson$1 } from "../chunks/oidcProvisioningPrimitives-BPWWsNb7.js";
|
|
3
3
|
//#region src/internal/oidc-provisioning.ts
|
|
4
4
|
async function createOidcProvisioningPerson(db, input) {
|
|
5
5
|
return await createOidcProvisioningPerson$1(db, input);
|
package/dist/manifest.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"version": "1.0.0",
|
|
3
3
|
"timestamp": 0,
|
|
4
4
|
"packageName": "@happyvertical/smrt-profiles",
|
|
5
|
-
"packageVersion": "0.51.
|
|
5
|
+
"packageVersion": "0.51.11",
|
|
6
6
|
"objects": {
|
|
7
7
|
"@happyvertical/smrt-profiles:ApiKeyCollection": {
|
|
8
8
|
"name": "apikeycollection",
|
|
@@ -3262,6 +3262,7 @@
|
|
|
3262
3262
|
},
|
|
3263
3263
|
"decoratorConfig": {
|
|
3264
3264
|
"tableName": "api_keys",
|
|
3265
|
+
"sensitive": true,
|
|
3265
3266
|
"api": {
|
|
3266
3267
|
"include": [
|
|
3267
3268
|
"list",
|
|
@@ -3841,6 +3842,7 @@
|
|
|
3841
3842
|
},
|
|
3842
3843
|
"decoratorConfig": {
|
|
3843
3844
|
"tableName": "magic_link_tokens",
|
|
3845
|
+
"sensitive": true,
|
|
3844
3846
|
"api": {
|
|
3845
3847
|
"exclude": [
|
|
3846
3848
|
"*"
|
|
@@ -4162,6 +4164,7 @@
|
|
|
4162
4164
|
},
|
|
4163
4165
|
"decoratorConfig": {
|
|
4164
4166
|
"tableName": "nostr_identities",
|
|
4167
|
+
"sensitive": true,
|
|
4165
4168
|
"api": {
|
|
4166
4169
|
"include": [
|
|
4167
4170
|
"list",
|
package/dist/smrt-knowledge.json
CHANGED
|
@@ -3,13 +3,13 @@
|
|
|
3
3
|
"sensitiveFieldsExcluded": true,
|
|
4
4
|
"generatedAt": "1970-01-01T00:00:00.000Z",
|
|
5
5
|
"packageName": "@happyvertical/smrt-profiles",
|
|
6
|
-
"packageVersion": "0.51.
|
|
6
|
+
"packageVersion": "0.51.11",
|
|
7
7
|
"sourceManifestPath": "dist/manifest.json",
|
|
8
8
|
"agentDocPath": "AGENTS.md",
|
|
9
9
|
"sourceHashes": {
|
|
10
|
-
"manifest": "
|
|
11
|
-
"packageJson": "
|
|
12
|
-
"agents": "
|
|
10
|
+
"manifest": "9aa1f35664246f9707fe9b97b0636cc0f8dc2bf88998a3db95825c5ab4aea585",
|
|
11
|
+
"packageJson": "f04e7dfc8c31f9a075b37576e8026d9b519cd395ef4359c9f3d061cefd816829",
|
|
12
|
+
"agents": "a6646a088f8c421b5556f945946db6f83d3a23d77b34c40bb02b726af30b4bd2"
|
|
13
13
|
},
|
|
14
14
|
"exports": [
|
|
15
15
|
".",
|
|
@@ -13945,5 +13945,5 @@
|
|
|
13945
13945
|
"polymorphicAssociations": 0,
|
|
13946
13946
|
"uuidColumns": 85
|
|
13947
13947
|
},
|
|
13948
|
-
"agentDoc": "# @happyvertical/smrt-profiles\n\nCentral identity system with multi-auth, relationships, controlled metadata, and audit logging.\n\n## Models\n\n- **Profile** (STI base → Bot, Organization, Person): email (optional; identity uniqueness is arbitrated by `oidc_profile_email_reservations.email_key`, not a DB constraint — legacy duplicates are tolerated and fail closed, #2359), readonly indexed `emailKey` derived with `normalizeIdentityEmail()` for adapter-independent identity lookup, `typeId` FK to ProfileType, plus a `metadata` `@oneToMany('ProfileMetadata')` relationship for controlled per-profile values.\n- **ProfileAsset**: dedicated owned-asset join in `profile_assets` with `relationship` and `sortOrder`.\n- **ProfileRelationship**: bidirectional — creating one auto-creates reciprocal inverse. `contextProfileId` for tertiary relationships. `ProfileRelationshipTerm` tracks start/end dates.\n- **ProfileMetafield**: controlled vocabulary with `validationSchema`. **ProfileMetadata**: per-profile values linked to metafields.\n- **AuditLog**: action, resourceType/Id, `source` (web/cli/ci/webhook/mcp), `onBehalfOfId` for CI pass-through identity. `allowSuperAdminBypass: true`.\n\n## Auth Methods\n\n| Model | Pattern |\n|-------|---------|\n| NostrIdentity | Encrypted keypair (AES-256-GCM). Requires `SERVER_MASTER_SECRET` env var for decryption. NIP-05 address generation. |\n| OidcIdentity | Multiple issuers (Keycloak/Google/GitHub). Lookup by `issuer + subject` pair. Transactional provisioning derives the readonly nullable unique `identityKey` and backfills legacy rows. |\n| ApiKey | SHA-256 hashed. **Plaintext returned once only** on `generate()`. `keyPrefix` for identification. Scope-based with expiry. |\n| MagicLinkToken | One-time token with expiry for passwordless auth. |\n\n## Identity Resolution\n\nAuth helpers in `src/auth/` build profiles from external identity claims:\n\n- `resolveIdentity()` — top-level dispatcher that returns/creates a Profile from Nostr signatures, OIDC claims, magic link tokens, or API keys.\n- `createProfileFromOidc(claims, provider, options)` — creates `Profile` + `OidcIdentity` for first-time OIDC sign-in using a transaction-capable root database in `options.db`.\n- `ProfileCollection.findUniqueGlobalPersonByEmail(email)` — supported\n verified-identity lookup; fails closed on tenant-scoped, non-Person, or\n duplicate case-insensitive matches.\n- `ProfileCollection.requireCanonicalGlobalPerson(profileId, email?)` —\n validates an application-selected Profile against the same canonical global\n Person invariant.\n- `ProfileCollection.reserveCanonicalIdentityEmail(profileId, email?)` —\n validates and synchronizes the private unique\n `oidc_profile_email_reservations.email_key` used as the database arbiter for\n concurrent external-identity provisioning. Omitting `email` uses the\n Profile's stored address, moving or removing an existing reservation as the\n canonical Profile changes.\n- `createProfileFromNostr(email, nostrData)` — creates `Profile` + `NostrIdentity` for Nostr-authenticated users.\n\n## Key Methods\n\n- `Profile.getAssets()` / `addAsset()` / `removeAsset()` and the matching `ProfileCollection` wrappers — canonical owned asset helpers backed by `profile_assets`.\n- `Profile.addMetadata(metafieldSlug, value)` / `Profile.getMetadata()` — validates against metafield schema. `ProfileCollection.batchGetMetadata()` / `batchUpdateMetadata()` for bulk reads/writes.\n- `Profile.getRelationships({ direction: 'from'|'to'|'all' })` — direction matters.\n- `Profile.getRelationshipsFrom()` / `getRelationshipsTo()` — R10-generated `@oneToMany` accessors. ProfileRelationship has two FKs back to Profile, so each `@oneToMany` annotates its inverse explicitly (`{ foreignKey: 'fromProfileId' }` / `'toProfileId'`). Return raw `ProfileRelationship[]`; use `getRelationships()` for slug/direction filtering.\n- AI: `generateBio()` (uses `smrtProfiles.profile.generateBio` prompt via `@happyvertical/smrt-prompts`), `matches(criteria)` (delegates to `is()`).\n\n## Prompt Registry\n\n`generateBio()` is registered with `@happyvertical/smrt-prompts` so tenants can override template/model/params at runtime:\n\n```typescript\nimport { smrtProfilesGenerateBioPrompt } from '@happyvertical/smrt-profiles';\n// key: 'smrtProfiles.profile.generateBio'\n```\n\n## Gotchas\n\n- **SERVER_MASTER_SECRET required** for Nostr private key decryption — centralized key management\n- **API key never returned again**: `ApiKey.generate()` returns plaintext once; only `keyPrefix` visible later\n- **OIDC unique per issuer+subject**: same subject from different issuers =\n different identities. Both claims are opaque and case-sensitive; preserve\n exact whitespace after using trim only to reject blank values.\n- **OIDC identity mutations are trusted-only**: generated REST, MCP, and CLI\n surfaces are read-only. Link or update identities through transactional\n provisioning APIs so callers cannot rebind issuer/subject authority to an\n arbitrary Profile. Deprecated `OidcIdentity.findOrCreate()` preserves only\n transaction-safe exact-link reuse and refuses to create new links;\n `OidcIdentityCollection.linkToProfile()` and `Profile.linkOidcIdentity()`\n delegate to that same non-creating compatibility path.\n- **Apply schema migrations for identity race keys**: run `smrt db:status`,\n `smrt db:migrate`, then `smrt db:status` before deploying. Transactional OIDC\n provisioning populates `OidcIdentity.identityKey` and\n `oidc_profile_email_reservations`; legacy rows reserve an address only after\n safe validation. Stop/upgrade old Profile writers, then run the public,\n transactional and idempotent `backfillProfileEmailKeys(db)` from one deploy\n process. Verified-email provisioning and public email-key lookup require the\n standard `_smrt_backfills` readiness marker and fail closed immediately when\n it is absent. This keeps table scans in the explicit deploy step; runtime\n reads use the indexed key and validate only returned candidates.\n- **Public OIDC provisioning requires transactions**:\n `createProfileFromOidc()` owns a root transaction or uses a savepoint on an\n already-bound handle. Root adapters must expose `beginTransaction`;\n transaction-only handles are ambiguous and fail closed. Pass the root\n database for adapters without nested savepoints, including DuckDB;\n provisioning fails before durable writes if neither path is safe. Trusted\n framework packages use the private\n `@happyvertical/smrt-profiles/internal/oidc-provisioning` subpath instead of\n duplicating adapter probing, locking, or retry policy, and supply both exact\n issuer/subject and normalized-email lock keys in deterministic order. The\n coordinator additionally serializes every root-handle statement it owns per\n database URL on SQLite/DuckDB — shared `_smrt_backfills` initialization, the\n provisioning transaction, and the post-commit rebind — because those adapters\n multiplex one native connection and cannot overlap unrelated root\n transactions safely. **Never overlap two statements on one such handle,\n transaction-bound or not** — no `Promise.all` over reads, not even\n primary-key rebinds — because DuckDB fails the losing prepared statement or\n aborts the process outright. It retries\n bounded PostgreSQL deadlock/serialization failures. New OIDC Profiles use per-profile,\n non-semantic slugs so duplicate display names never invoke natural-key upsert.\n Caller-owned transactions never execute `_smrt_backfills` DDL; paths that\n perform canonical email lookup or reservation require the table to already\n exist or the caller must retry with the root database. Exact issuer/subject\n reuse skips the email-key readiness-marker lookup, but root coordination still\n initializes the shared tracker table. Caller-owned exact reuse does not\n consult the tracker and therefore does not require that table.\n- **Profile-only OIDC linking fails closed on existing email matches**:\n the typed canonical scenario contract is\n `src/testing/oidcProvisioningDecisionMatrix.ts`, executed by both Profiles\n and Users tests; keep public docs pointed at it instead of adding a second\n behavioral table.\n `createProfileFromOidc()` preserves exact issuer/subject reuse, including\n legacy tenant-scoped and non-Person links, but profiles cannot prove whether a\n User owns a same-email Profile. New identities therefore never attach to an\n existing email match through this helper; User/session provisioning still\n rejects unsafe linked Profiles before creating authentication state. Use\n `UserCollection.getOrCreateFromOidc()` for owner-aware verified-email reuse and\n its supported transaction-bound hooks. A pre-provisioned owned Person remains\n fail-closed unless the application explicitly uses `authorizeProfileOwner`\n to select both the canonical Profile and its existing approved sole owner;\n the users package revalidates those IDs, emails, and identity authority in\n the provisioning transaction.\n- **Email storage and identity matching differ**: `Profile.email` has an\n exact-value DB constraint. Identity boundaries query readonly indexed\n `Profile.emailKey`, derived by the shared TypeScript\n `normalizeIdentityEmail()` helper rather than adapter-specific SQL casing or\n trimming, and deliberately fail closed on duplicate normalized keys.\n- **Manifest objects must remain root-importable**: the non-API\n `OidcProfileEmailReservation` model and collection are public root exports\n because generated consumer registration imports every advertised manifest\n symbol from `@happyvertical/smrt-profiles`. Release-pack validation imports\n the actual generated register file from the packed tarball.\n- **Optional tenancy** on Profile; AuditLog allows super-admin bypass\n"
|
|
13948
|
+
"agentDoc": "# @happyvertical/smrt-profiles\n\nCentral identity system with multi-auth, relationships, controlled metadata, and audit logging.\n\n## Models\n\n- **Profile** (STI base → Bot, Organization, Person): email (optional; identity uniqueness is arbitrated by `oidc_profile_email_reservations.email_key`, not a DB constraint — legacy duplicates are tolerated and fail closed, #2359), readonly indexed `emailKey` derived with `normalizeIdentityEmail()` for adapter-independent identity lookup, `typeId` FK to ProfileType, plus a `metadata` `@oneToMany('ProfileMetadata')` relationship for controlled per-profile values.\n- **ProfileAsset**: dedicated owned-asset join in `profile_assets` with `relationship` and `sortOrder`.\n- **ProfileRelationship**: bidirectional — creating one auto-creates reciprocal inverse. `contextProfileId` for tertiary relationships. `ProfileRelationshipTerm` tracks start/end dates.\n- **ProfileMetafield**: controlled vocabulary with `validationSchema`. **ProfileMetadata**: per-profile values linked to metafields.\n- **AuditLog**: action, resourceType/Id, `source` (web/cli/ci/webhook/mcp), `onBehalfOfId` for CI pass-through identity. `allowSuperAdminBypass: true`.\n\n## Auth Methods\n\n| Model | Pattern |\n|-------|---------|\n| NostrIdentity | Encrypted keypair (AES-256-GCM). Requires `SERVER_MASTER_SECRET` env var for decryption. NIP-05 address generation. |\n| OidcIdentity | Multiple issuers (Keycloak/Google/GitHub). Lookup by `issuer + subject` pair. Transactional provisioning derives the readonly nullable unique `identityKey` and backfills legacy rows. |\n| ApiKey | SHA-256 hashed. **Plaintext returned once only** on `generate()`. `keyPrefix` for identification. Scope-based with expiry. |\n| MagicLinkToken | One-time token with expiry for passwordless auth. |\n\n## Identity Resolution\n\nAuth helpers in `src/auth/` build profiles from external identity claims:\n\n- `resolveIdentity()` — top-level dispatcher that returns/creates a Profile from Nostr signatures, OIDC claims, magic link tokens, or API keys.\n- `createProfileFromOidc(claims, provider, options)` — creates `Profile` + `OidcIdentity` for first-time OIDC sign-in using a transaction-capable root database in `options.db`.\n- `ProfileCollection.findUniqueGlobalPersonByEmail(email)` — supported\n verified-identity lookup; fails closed on tenant-scoped, non-Person, or\n duplicate case-insensitive matches.\n- `ProfileCollection.requireCanonicalGlobalPerson(profileId, email?)` —\n validates an application-selected Profile against the same canonical global\n Person invariant.\n- `ProfileCollection.reserveCanonicalIdentityEmail(profileId, email?)` —\n validates and synchronizes the private unique\n `oidc_profile_email_reservations.email_key` used as the database arbiter for\n concurrent external-identity provisioning. Omitting `email` uses the\n Profile's stored address, moving or removing an existing reservation as the\n canonical Profile changes.\n- `createProfileFromNostr(email, nostrData)` — creates `Profile` + `NostrIdentity` for Nostr-authenticated users.\n\n## Agent (bot) profiles (#2995)\n\nAn automated agent is an author, and every authoring seam in the framework\n(`ChatMessage.senderProfileId`, `ChatParticipant.profileId`, a persona's\n`actsAsProfileId`) is a `crossPackageRef` to `Profile` — a native uuid column on\nPostgreSQL/DuckDB. A slug-style agent id is therefore not a representable author\n(`22P02`). `src/agent-profile.ts` is the owning-package API that turns one into\na real Profile so no consumer mints bot profiles itself and no uuid column is\nweakened to text:\n\n- `resolveAgentProfile(profiles, { agentId, tenantId, name? })` /\n `resolveAgentProfileId(...)` — resolve, creating on first use, the `bot`\n Profile an agent authors as. Idempotent and tenant-bound: identity is keyed on\n `(tenantId, slug = agentId, context = AGENT_PROFILE_CONTEXT)`, matching the\n model's `(tenant_id, slug, context, _meta_type)` unique key, so an agent id\n resolves to a distinct profile per tenant and never collides with a\n person/organization sharing the slug. Classification is the `bot` ProfileType\n (`AGENT_PROFILE_TYPE_SLUG`), created on demand. Parallel first use converges\n on the natural key rather than on whoever's INSERT won (each attempt re-reads\n before and after writing), which matters because NULLs are distinct in the\n `(tenant_id, slug, context)` unique index, so an untenanted agent is not\n arbitrated by the database (smrt#2360). Pinned against real PostgreSQL by\n `src/__tests__/agent-profile-postgres.test.ts`\n (`pnpm --filter @happyvertical/smrt-profiles test:postgres`).\n\nFirst consumer: `@happyvertical/smrt-chat`'s `ChatService`.\n\n## Key Methods\n\n- `Profile.getAssets()` / `addAsset()` / `removeAsset()` and the matching `ProfileCollection` wrappers — canonical owned asset helpers backed by `profile_assets`.\n- `Profile.addMetadata(metafieldSlug, value)` / `Profile.getMetadata()` — validates against metafield schema. `ProfileCollection.batchGetMetadata()` / `batchUpdateMetadata()` for bulk reads/writes.\n- `Profile.getRelationships({ direction: 'from'|'to'|'all' })` — direction matters.\n- `Profile.getRelationshipsFrom()` / `getRelationshipsTo()` — R10-generated `@oneToMany` accessors. ProfileRelationship has two FKs back to Profile, so each `@oneToMany` annotates its inverse explicitly (`{ foreignKey: 'fromProfileId' }` / `'toProfileId'`). Return raw `ProfileRelationship[]`; use `getRelationships()` for slug/direction filtering.\n- AI: `generateBio()` (uses `smrtProfiles.profile.generateBio` prompt via `@happyvertical/smrt-prompts`), `matches(criteria)` (delegates to `is()`).\n\n## Prompt Registry\n\n`generateBio()` is registered with `@happyvertical/smrt-prompts` so tenants can override template/model/params at runtime:\n\n```typescript\nimport { smrtProfilesGenerateBioPrompt } from '@happyvertical/smrt-profiles';\n// key: 'smrtProfiles.profile.generateBio'\n```\n\n## Gotchas\n\n- **SERVER_MASTER_SECRET required** for Nostr private key decryption — centralized key management\n- **API key never returned again**: `ApiKey.generate()` returns plaintext once; only `keyPrefix` visible later\n- **OIDC unique per issuer+subject**: same subject from different issuers =\n different identities. Both claims are opaque and case-sensitive; preserve\n exact whitespace after using trim only to reject blank values.\n- **OIDC identity mutations are trusted-only**: generated REST, MCP, and CLI\n surfaces are read-only. Link or update identities through transactional\n provisioning APIs so callers cannot rebind issuer/subject authority to an\n arbitrary Profile. Deprecated `OidcIdentity.findOrCreate()` preserves only\n transaction-safe exact-link reuse and refuses to create new links;\n `OidcIdentityCollection.linkToProfile()` and `Profile.linkOidcIdentity()`\n delegate to that same non-creating compatibility path.\n- **Apply schema migrations for identity race keys**: run `smrt db:status`,\n `smrt db:migrate`, then `smrt db:status` before deploying. Transactional OIDC\n provisioning populates `OidcIdentity.identityKey` and\n `oidc_profile_email_reservations`; legacy rows reserve an address only after\n safe validation. Stop/upgrade old Profile writers, then run the public,\n transactional and idempotent `backfillProfileEmailKeys(db)` from one deploy\n process. Verified-email provisioning and public email-key lookup require the\n standard `_smrt_backfills` readiness marker and fail closed immediately when\n it is absent. This keeps table scans in the explicit deploy step; runtime\n reads use the indexed key and validate only returned candidates.\n- **Public OIDC provisioning requires transactions**:\n `createProfileFromOidc()` owns a root transaction or uses a savepoint on an\n already-bound handle. Root adapters must expose `beginTransaction`;\n transaction-only handles are ambiguous and fail closed. Pass the root\n database for adapters without nested savepoints, including DuckDB;\n provisioning fails before durable writes if neither path is safe. Trusted\n framework packages use the private\n `@happyvertical/smrt-profiles/internal/oidc-provisioning` subpath instead of\n duplicating adapter probing, locking, or retry policy, and supply both exact\n issuer/subject and normalized-email lock keys in deterministic order. The\n coordinator additionally serializes every root-handle statement it owns per\n database URL on SQLite/DuckDB — shared `_smrt_backfills` initialization, the\n provisioning transaction, and the post-commit rebind — because those adapters\n multiplex one native connection and cannot overlap unrelated root\n transactions safely. **Never overlap two statements on one such handle,\n transaction-bound or not** — no `Promise.all` over reads, not even\n primary-key rebinds — because DuckDB fails the losing prepared statement or\n aborts the process outright. It retries\n bounded PostgreSQL deadlock/serialization failures. New OIDC Profiles use per-profile,\n non-semantic slugs so duplicate display names never invoke natural-key upsert.\n Caller-owned transactions never execute `_smrt_backfills` DDL; paths that\n perform canonical email lookup or reservation require the table to already\n exist or the caller must retry with the root database. Exact issuer/subject\n reuse skips the email-key readiness-marker lookup, but root coordination still\n initializes the shared tracker table. Caller-owned exact reuse does not\n consult the tracker and therefore does not require that table.\n- **Profile-only OIDC linking fails closed on existing email matches**:\n the typed canonical scenario contract is\n `src/testing/oidcProvisioningDecisionMatrix.ts`, executed by both Profiles\n and Users tests; keep public docs pointed at it instead of adding a second\n behavioral table.\n `createProfileFromOidc()` preserves exact issuer/subject reuse, including\n legacy tenant-scoped and non-Person links, but profiles cannot prove whether a\n User owns a same-email Profile. New identities therefore never attach to an\n existing email match through this helper; User/session provisioning still\n rejects unsafe linked Profiles before creating authentication state. Use\n `UserCollection.getOrCreateFromOidc()` for owner-aware verified-email reuse and\n its supported transaction-bound hooks. A pre-provisioned owned Person remains\n fail-closed unless the application explicitly uses `authorizeProfileOwner`\n to select both the canonical Profile and its existing approved sole owner;\n the users package revalidates those IDs, emails, and identity authority in\n the provisioning transaction.\n- **Email storage and identity matching differ**: `Profile.email` has an\n exact-value DB constraint. Identity boundaries query readonly indexed\n `Profile.emailKey`, derived by the shared TypeScript\n `normalizeIdentityEmail()` helper rather than adapter-specific SQL casing or\n trimming, and deliberately fail closed on duplicate normalized keys.\n- **Manifest objects must remain root-importable**: the non-API\n `OidcProfileEmailReservation` model and collection are public root exports\n because generated consumer registration imports every advertised manifest\n symbol from `@happyvertical/smrt-profiles`. Release-pack validation imports\n the actual generated register file from the packed tarball.\n- **Optional tenancy** on Profile; AuditLog allows super-admin bypass\n"
|
|
13949
13949
|
}
|
package/dist/utils.js
CHANGED
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
//#region src/utils.ts
|
|
2
2
|
async function getProfileMetadata(profileId, options = {}) {
|
|
3
|
-
const { ProfileMetadataCollection } = await import("./chunks/ProfileMetadataCollection-
|
|
3
|
+
const { ProfileMetadataCollection } = await import("./chunks/ProfileMetadataCollection-DyVxb-Tn.js").then((n) => n.n);
|
|
4
4
|
return await (await ProfileMetadataCollection.create(options)).getMetadataObject(profileId);
|
|
5
5
|
}
|
|
6
6
|
async function setProfileMetadata(profileId, metafieldSlug, value, options = {}) {
|
|
7
|
-
const { ProfileCollection } = await import("./chunks/ProfileCollection-
|
|
7
|
+
const { ProfileCollection } = await import("./chunks/ProfileCollection-BAC5weFA.js").then((n) => n.r);
|
|
8
8
|
const profile = await (await ProfileCollection.create(options)).get({ id: profileId });
|
|
9
9
|
if (!profile) throw new Error(`Profile '${profileId}' not found`);
|
|
10
10
|
await profile.addMetadata(metafieldSlug, value);
|
|
11
11
|
}
|
|
12
12
|
async function findProfilesByMeta(metafieldSlug, value, options = {}) {
|
|
13
|
-
const { ProfileMetafieldCollection } = await import("./chunks/ProfileMetafieldCollection-
|
|
14
|
-
const { ProfileMetadataCollection } = await import("./chunks/ProfileMetadataCollection-
|
|
13
|
+
const { ProfileMetafieldCollection } = await import("./chunks/ProfileMetafieldCollection-KSYFXBxU.js").then((n) => n.n);
|
|
14
|
+
const { ProfileMetadataCollection } = await import("./chunks/ProfileMetadataCollection-DyVxb-Tn.js").then((n) => n.n);
|
|
15
15
|
const metafield = await (await ProfileMetafieldCollection.create(options)).getBySlug(metafieldSlug);
|
|
16
16
|
if (!metafield) throw new Error(`Metafield '${metafieldSlug}' not found`);
|
|
17
17
|
return await (await ProfileMetadataCollection.create(options)).findProfilesByMetadata(metafield.id, value);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@happyvertical/smrt-profiles",
|
|
3
|
-
"version": "0.51.
|
|
3
|
+
"version": "0.51.11",
|
|
4
4
|
"description": "Profile management system with relationships, metadata, and reciprocal associations for SMRT framework",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -30,10 +30,10 @@
|
|
|
30
30
|
"@happyvertical/ai": "^0.89.11",
|
|
31
31
|
"@happyvertical/files": "^0.89.11",
|
|
32
32
|
"@happyvertical/logger": "^0.89.11",
|
|
33
|
-
"@happyvertical/smrt-assets": "0.51.
|
|
34
|
-
"@happyvertical/smrt-core": "0.51.
|
|
35
|
-
"@happyvertical/smrt-prompts": "0.51.
|
|
36
|
-
"@happyvertical/smrt-tenancy": "0.51.
|
|
33
|
+
"@happyvertical/smrt-assets": "0.51.11",
|
|
34
|
+
"@happyvertical/smrt-core": "0.51.11",
|
|
35
|
+
"@happyvertical/smrt-prompts": "0.51.11",
|
|
36
|
+
"@happyvertical/smrt-tenancy": "0.51.11",
|
|
37
37
|
"@happyvertical/sql": "^0.89.11",
|
|
38
38
|
"@happyvertical/utils": "^0.89.11",
|
|
39
39
|
"@noble/curves": "^1.9.7",
|
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
},
|
|
42
42
|
"devDependencies": {
|
|
43
43
|
"@faker-js/faker": "^10.5.0",
|
|
44
|
-
"@happyvertical/smrt-vitest": "0.51.
|
|
44
|
+
"@happyvertical/smrt-vitest": "0.51.11",
|
|
45
45
|
"@types/node": "24.13.2",
|
|
46
46
|
"fast-glob": "3.3.3",
|
|
47
47
|
"typescript": "5.9.3",
|
|
@@ -73,6 +73,7 @@
|
|
|
73
73
|
"clean": "rm -rf dist",
|
|
74
74
|
"dev": "vite dev",
|
|
75
75
|
"test": "vitest run",
|
|
76
|
+
"test:postgres": "node ../../scripts/run-with-ci-postgres.mjs -- pnpm exec vitest run src/__tests__/agent-profile-postgres.test.ts",
|
|
76
77
|
"test:watch": "vitest",
|
|
77
78
|
"typecheck": "tsc --noEmit -p tsconfig.json"
|
|
78
79
|
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"ApiKey-BTC7nWG1.js","names":[],"sources":["../../src/models/ApiKey.ts"],"sourcesContent":["/**\n * ApiKey - API keys for CLI and CI authentication\n *\n * Provides secure API key management with hashing, scopes, and expiration.\n * Keys are stored as hashes - the plaintext is only returned on creation.\n */\n\nimport { createHash, randomBytes } from 'node:crypto';\nimport {\n field,\n foreignKey,\n SmrtObject,\n type SmrtObjectOptions,\n smrt,\n} from '@happyvertical/smrt-core';\nimport type { Profile } from './Profile';\n\nexport interface ApiKeyOptions extends SmrtObjectOptions {\n profileId?: string;\n name?: string;\n scopes?: string[];\n expiresAt?: Date | null;\n}\n\nexport interface GenerateKeyResult {\n key: string;\n apiKey: ApiKey;\n}\n\n@smrt({\n tableName: 'api_keys',\n api: { include: ['list', 'get'] },\n mcp: { include: ['list', 'get'] },\n // create/revoke are admin operations invoked in-process via the CLI; only\n // read-only list/get are exposed over HTTP.\n cli: { include: ['list', 'get', 'create', 'revoke'], skipApiCheck: true },\n})\nexport class ApiKey extends SmrtObject {\n /**\n * Link to the Profile (Person, Organization, Bot)\n */\n @foreignKey('Profile', { required: true })\n profileId?: string;\n\n /**\n * SHA-256 hash of the API key.\n *\n * Indexed: `verify()` looks keys up by this hash on every authenticated\n * request (#2364, epic #2382 finding A3).\n */\n @field({ indexed: true })\n keyHash: string = '';\n\n /**\n * First 8 characters of the key for identification (e.g., \"sk_live_a1b2...\")\n */\n keyPrefix: string = '';\n\n /**\n * Human-readable name for the key (e.g., \"CI Bot\", \"Local CLI\")\n */\n name: string = '';\n\n /**\n * Scopes/permissions for this key\n */\n scopes: string[] = [];\n\n /**\n * Last time this key was used\n */\n lastUsedAt: Date | null = null;\n\n /**\n * When this key expires (null = never)\n */\n expiresAt: Date | null = null;\n\n /**\n * When this key was revoked (null = active)\n */\n revokedAt: Date | null = null;\n\n constructor(options: ApiKeyOptions = {}) {\n super(options);\n if (options.profileId) this.profileId = options.profileId;\n if (options.name) this.name = options.name;\n if (options.scopes) this.scopes = options.scopes;\n if (options.expiresAt !== undefined) this.expiresAt = options.expiresAt;\n }\n\n /**\n * Get the linked Profile\n */\n async getProfile(): Promise<Profile | null> {\n return (await this.getRelated('profileId')) as Profile | null;\n }\n\n /**\n * Check if this key is valid (not expired, not revoked)\n */\n isValid(): boolean {\n if (this.revokedAt) return false;\n if (this.expiresAt && this.expiresAt < new Date()) return false;\n return true;\n }\n\n /**\n * Check if this key has a specific scope\n */\n hasScope(scope: string): boolean {\n return this.scopes.includes(scope) || this.scopes.includes('*');\n }\n\n /**\n * Revoke this key\n */\n async revoke(): Promise<void> {\n this.revokedAt = new Date();\n await this.save();\n }\n\n /**\n * Record usage of this key\n */\n async recordUsage(): Promise<void> {\n for (let attempt = 0; attempt < 4; attempt += 1) {\n if (this.isPersisted) await this.loadFromId();\n if (!this.isValid()) return;\n this.lastUsedAt = new Date();\n try {\n await this.save();\n return;\n } catch (error) {\n if (\n attempt === 3 ||\n !(\n error instanceof Error &&\n 'code' in error &&\n error.code === 'RUNTIME_REVISION_CONFLICT'\n )\n ) {\n throw error;\n }\n }\n }\n }\n\n /**\n * Hash an API key\n */\n static hashKey(key: string): string {\n return createHash('sha256').update(key).digest('hex');\n }\n\n /**\n * Generate a new API key for a profile\n */\n static async generate(\n profile: Profile,\n options: {\n name: string;\n scopes?: string[];\n expiresAt?: Date | null;\n db?: SmrtObjectOptions['db'];\n },\n ): Promise<GenerateKeyResult> {\n // Generate a random key: sk_live_<32 random bytes as hex>\n const randomPart = randomBytes(32).toString('hex');\n const key = `sk_live_${randomPart}`;\n const keyPrefix = key.substring(0, 16);\n const keyHash = ApiKey.hashKey(key);\n\n const apiKey = new ApiKey({\n db: options.db || profile.options?.db,\n profileId: profile.id as string,\n name: options.name,\n scopes: options.scopes || ['*'],\n expiresAt: options.expiresAt ?? null,\n });\n apiKey.keyHash = keyHash;\n apiKey.keyPrefix = keyPrefix;\n\n await apiKey.initialize();\n await apiKey.save();\n\n return { key, apiKey };\n }\n\n /**\n * Verify an API key and return the ApiKey record if valid\n */\n static async verify(\n key: string,\n options: SmrtObjectOptions = {},\n ): Promise<ApiKey | null> {\n const keyHash = ApiKey.hashKey(key);\n\n const { ApiKeyCollection } = await import(\n '../collections/ApiKeyCollection'\n );\n const collection = await ApiKeyCollection.create(options);\n\n const apiKey = await collection.findOne({\n where: { keyHash },\n });\n\n if (!apiKey) return null;\n if (!apiKey.isValid()) return null;\n\n // Record usage\n await apiKey.recordUsage();\n\n // A concurrent revocation may have been observed while recordUsage()\n // rehydrated after a revision conflict. Never authenticate that key.\n if (!apiKey.isValid()) return null;\n\n return apiKey;\n }\n}\n"],"mappings":"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"}
|