@happyvertical/smrt-profiles 0.39.13 → 0.39.15

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.
Files changed (54) hide show
  1. package/AGENTS.md +69 -5
  2. package/README.md +101 -50
  3. package/dist/chunks/{ApiKey-BrNyaaZN.js → ApiKey-Br9NGBnr.js} +3 -3
  4. package/dist/chunks/{ApiKey-BrNyaaZN.js.map → ApiKey-Br9NGBnr.js.map} +1 -1
  5. package/dist/chunks/{ApiKeyCollection-DNhDkS7O.js → ApiKeyCollection-CrTjdMz8.js} +3 -3
  6. package/dist/chunks/{ApiKeyCollection-DNhDkS7O.js.map → ApiKeyCollection-CrTjdMz8.js.map} +1 -1
  7. package/dist/chunks/{AuditLogCollection-CnIYU0aK.js → AuditLogCollection-BBIEI6BD.js} +2 -2
  8. package/dist/chunks/{AuditLogCollection-CnIYU0aK.js.map → AuditLogCollection-BBIEI6BD.js.map} +1 -1
  9. package/dist/chunks/{NostrIdentityCollection-CG6HGf6c.js → NostrIdentity-DF_a_WZk.js} +6 -125
  10. package/dist/chunks/NostrIdentity-DF_a_WZk.js.map +1 -0
  11. package/dist/chunks/NostrIdentityCollection-BRYUgt47.js +124 -0
  12. package/dist/chunks/NostrIdentityCollection-BRYUgt47.js.map +1 -0
  13. package/dist/chunks/OidcIdentity-ChkiT6hg.js +111 -0
  14. package/dist/chunks/OidcIdentity-ChkiT6hg.js.map +1 -0
  15. package/dist/chunks/OidcIdentityCollection-Ce4yv8p0.js +76 -0
  16. package/dist/chunks/OidcIdentityCollection-Ce4yv8p0.js.map +1 -0
  17. package/dist/chunks/{ProfileAssetCollection-DzUwbtTq.js → ProfileAssetCollection-CtlvXbGy.js} +3 -3
  18. package/dist/chunks/{ProfileAssetCollection-DzUwbtTq.js.map → ProfileAssetCollection-CtlvXbGy.js.map} +1 -1
  19. package/dist/chunks/ProfileCollection-Bix9yAiy.js +1161 -0
  20. package/dist/chunks/ProfileCollection-Bix9yAiy.js.map +1 -0
  21. package/dist/chunks/{ProfileMetadataCollection-CGcO4Iw_.js → ProfileMetadataCollection-loKVv_pj.js} +2 -2
  22. package/dist/chunks/{ProfileMetadataCollection-CGcO4Iw_.js.map → ProfileMetadataCollection-loKVv_pj.js.map} +1 -1
  23. package/dist/chunks/{ProfileMetafieldCollection-C8TX9HvN.js → ProfileMetafieldCollection-CACMY1hl.js} +2 -2
  24. package/dist/chunks/{ProfileMetafieldCollection-C8TX9HvN.js.map → ProfileMetafieldCollection-CACMY1hl.js.map} +1 -1
  25. package/dist/chunks/{ProfileRelationshipCollection-JnskbZTd.js → ProfileRelationshipCollection-CzQKfvMy.js} +5 -5
  26. package/dist/chunks/{ProfileRelationshipCollection-JnskbZTd.js.map → ProfileRelationshipCollection-CzQKfvMy.js.map} +1 -1
  27. package/dist/chunks/{ProfileRelationshipTermCollection-UopP7sja.js → ProfileRelationshipTermCollection-DzmU48P6.js} +2 -2
  28. package/dist/chunks/{ProfileRelationshipTermCollection-UopP7sja.js.map → ProfileRelationshipTermCollection-DzmU48P6.js.map} +1 -1
  29. package/dist/chunks/{ProfileRelationshipType-OMBn5cWR.js → ProfileRelationshipType-BjoaCt-u.js} +2 -2
  30. package/dist/chunks/{ProfileRelationshipType-OMBn5cWR.js.map → ProfileRelationshipType-BjoaCt-u.js.map} +1 -1
  31. package/dist/chunks/{ProfileRelationshipTypeCollection-CQu08kxF.js → ProfileRelationshipTypeCollection-Bh4xJ4-I.js} +3 -3
  32. package/dist/chunks/{ProfileRelationshipTypeCollection-CQu08kxF.js.map → ProfileRelationshipTypeCollection-Bh4xJ4-I.js.map} +1 -1
  33. package/dist/chunks/oidcProvisioningPrimitives-ZuVcDPt-.js +138 -0
  34. package/dist/chunks/oidcProvisioningPrimitives-ZuVcDPt-.js.map +1 -0
  35. package/dist/chunks/resolveIdentity-_h_2sGqJ.js +260 -0
  36. package/dist/chunks/resolveIdentity-_h_2sGqJ.js.map +1 -0
  37. package/dist/index.d.ts +127 -15
  38. package/dist/index.js +30 -273
  39. package/dist/index.js.map +1 -1
  40. package/dist/internal/oidc-provisioning.d.ts +17 -0
  41. package/dist/internal/oidc-provisioning.js +13 -0
  42. package/dist/internal/oidc-provisioning.js.map +1 -0
  43. package/dist/manifest.json +431 -46
  44. package/dist/oidc-provisioning.d.ts +83 -0
  45. package/dist/smrt-knowledge.json +105 -43
  46. package/dist/types.d.ts +26 -2
  47. package/dist/utils.d.ts +26 -2
  48. package/dist/utils.js +4 -4
  49. package/package.json +10 -6
  50. package/dist/chunks/NostrIdentityCollection-CG6HGf6c.js.map +0 -1
  51. package/dist/chunks/OidcIdentityCollection-DiLneB7J.js +0 -158
  52. package/dist/chunks/OidcIdentityCollection-DiLneB7J.js.map +0 -1
  53. package/dist/chunks/ProfileCollection-BOMOloTL.js +0 -641
  54. package/dist/chunks/ProfileCollection-BOMOloTL.js.map +0 -1
package/AGENTS.md CHANGED
@@ -4,7 +4,7 @@ Central identity system with multi-auth, relationships, controlled metadata, and
4
4
 
5
5
  ## Models
6
6
 
7
- - **Profile** (STI base → Bot, Organization, Person): email (globally unique), `typeId` FK to ProfileType, plus a `metadata` `@oneToMany('ProfileMetadata')` relationship for controlled per-profile values.
7
+ - **Profile** (STI base → Bot, Organization, Person): email (globally unique exact value), 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.
8
8
  - **ProfileAsset**: dedicated owned-asset join in `profile_assets` with `relationship` and `sortOrder`.
9
9
  - **ProfileRelationship**: bidirectional — creating one auto-creates reciprocal inverse. `contextProfileId` for tertiary relationships. `ProfileRelationshipTerm` tracks start/end dates.
10
10
  - **ProfileMetafield**: controlled vocabulary with `validationSchema`. **ProfileMetadata**: per-profile values linked to metafields.
@@ -15,7 +15,7 @@ Central identity system with multi-auth, relationships, controlled metadata, and
15
15
  | Model | Pattern |
16
16
  |-------|---------|
17
17
  | NostrIdentity | Encrypted keypair (AES-256-GCM). Requires `SERVER_MASTER_SECRET` env var for decryption. NIP-05 address generation. |
18
- | OidcIdentity | Multiple issuers (Keycloak/Google/GitHub). Lookup by `issuer + subject` pair. `findOrCreate()` for first login. |
18
+ | OidcIdentity | Multiple issuers (Keycloak/Google/GitHub). Lookup by `issuer + subject` pair. Transactional provisioning derives the readonly nullable unique `identityKey` and backfills legacy rows. |
19
19
  | ApiKey | SHA-256 hashed. **Plaintext returned once only** on `generate()`. `keyPrefix` for identification. Scope-based with expiry. |
20
20
  | MagicLinkToken | One-time token with expiry for passwordless auth. |
21
21
 
@@ -24,7 +24,19 @@ Central identity system with multi-auth, relationships, controlled metadata, and
24
24
  Auth helpers in `src/auth/` build profiles from external identity claims:
25
25
 
26
26
  - `resolveIdentity()` — top-level dispatcher that returns/creates a Profile from Nostr signatures, OIDC claims, magic link tokens, or API keys.
27
- - `createProfileFromOidc(claims, provider)` — creates `Profile` + `OidcIdentity` for first-time OIDC sign-in.
27
+ - `createProfileFromOidc(claims, provider, options)` — creates `Profile` + `OidcIdentity` for first-time OIDC sign-in using a transaction-capable root database in `options.db`.
28
+ - `ProfileCollection.findUniqueGlobalPersonByEmail(email)` — supported
29
+ verified-identity lookup; fails closed on tenant-scoped, non-Person, or
30
+ duplicate case-insensitive matches.
31
+ - `ProfileCollection.requireCanonicalGlobalPerson(profileId, email?)` —
32
+ validates an application-selected Profile against the same canonical global
33
+ Person invariant.
34
+ - `ProfileCollection.reserveCanonicalIdentityEmail(profileId, email?)` —
35
+ validates and synchronizes the private unique
36
+ `oidc_profile_email_reservations.email_key` used as the database arbiter for
37
+ concurrent external-identity provisioning. Omitting `email` uses the
38
+ Profile's stored address, moving or removing an existing reservation as the
39
+ canonical Profile changes.
28
40
  - `createProfileFromNostr(email, nostrData)` — creates `Profile` + `NostrIdentity` for Nostr-authenticated users.
29
41
 
30
42
  ## Key Methods
@@ -48,6 +60,58 @@ import { smrtProfilesGenerateBioPrompt } from '@happyvertical/smrt-profiles';
48
60
 
49
61
  - **SERVER_MASTER_SECRET required** for Nostr private key decryption — centralized key management
50
62
  - **API key never returned again**: `ApiKey.generate()` returns plaintext once; only `keyPrefix` visible later
51
- - **OIDC unique per issuer+subject**: same subject from different issuers = different identities
52
- - **Email unique across all profiles**: DB-level unique constraint, not per-tenant
63
+ - **OIDC unique per issuer+subject**: same subject from different issuers =
64
+ different identities. Both claims are opaque and case-sensitive; preserve
65
+ exact whitespace after using trim only to reject blank values.
66
+ - **OIDC identity mutations are trusted-only**: generated REST, MCP, and CLI
67
+ surfaces are read-only. Link or update identities through transactional
68
+ provisioning APIs so callers cannot rebind issuer/subject authority to an
69
+ arbitrary Profile. Deprecated `OidcIdentity.findOrCreate()` preserves only
70
+ transaction-safe exact-link reuse and refuses to create new links;
71
+ `OidcIdentityCollection.linkToProfile()` and `Profile.linkOidcIdentity()`
72
+ delegate to that same non-creating compatibility path.
73
+ - **Apply schema migrations for identity race keys**: run `smrt db:status`,
74
+ `smrt db:migrate`, then `smrt db:status` before deploying. Transactional OIDC
75
+ provisioning populates `OidcIdentity.identityKey` and
76
+ `oidc_profile_email_reservations`; legacy rows reserve an address only after
77
+ safe validation. Stop/upgrade old Profile writers, then run the public,
78
+ transactional and idempotent `backfillProfileEmailKeys(db)` from one deploy
79
+ process. Verified-email provisioning and public email-key lookup require the
80
+ standard `_smrt_backfills` readiness marker and fail closed immediately when
81
+ it is absent. This keeps table scans in the explicit deploy step; runtime
82
+ reads use the indexed key and validate only returned candidates.
83
+ - **Public OIDC provisioning requires transactions**:
84
+ `createProfileFromOidc()` owns a root transaction or uses a savepoint on an
85
+ already-bound handle. Root adapters must expose `beginTransaction`;
86
+ transaction-only handles are ambiguous and fail closed. Pass the root
87
+ database for adapters without nested savepoints, including DuckDB;
88
+ provisioning fails before durable writes if neither path is safe. Trusted
89
+ framework packages use the private
90
+ `@happyvertical/smrt-profiles/internal/oidc-provisioning` subpath instead of
91
+ duplicating adapter probing, locking, or retry policy, and supply both exact
92
+ issuer/subject and normalized-email lock keys in deterministic order. The
93
+ coordinator additionally serializes all SQLite/DuckDB provisioning
94
+ transactions per database URL because those adapters cannot overlap
95
+ unrelated root transactions safely and retries bounded PostgreSQL
96
+ deadlock/serialization failures. New OIDC Profiles use per-profile,
97
+ non-semantic slugs so duplicate display names never invoke natural-key upsert.
98
+ Caller-owned transactions never execute `_smrt_backfills` DDL; paths that
99
+ perform canonical email lookup or reservation require the table to already
100
+ exist or the caller must retry with the root database. Exact issuer/subject
101
+ reuse skips the email-key readiness-marker lookup, but root coordination still
102
+ initializes the shared tracker table. Caller-owned exact reuse does not
103
+ consult the tracker and therefore does not require that table.
104
+ - **Profile-only OIDC linking fails closed on existing email matches**:
105
+ `createProfileFromOidc()` preserves exact issuer/subject reuse, including
106
+ legacy tenant-scoped and non-Person links, but profiles cannot prove whether a
107
+ User owns a same-email Profile. New identities therefore never attach to an
108
+ existing email match through this helper; User/session provisioning still
109
+ rejects unsafe linked Profiles before creating authentication state. Use
110
+ `UserCollection.getOrCreateFromOidc()` for owner-aware verified-email reuse and
111
+ its supported transaction-bound resolver hook.
112
+ - **Email storage and identity matching differ**: `Profile.email` has an
113
+ exact-value DB constraint. Identity boundaries query readonly indexed
114
+ `Profile.emailKey`, derived by the shared TypeScript
115
+ `normalizeIdentityEmail()` helper rather than adapter-specific SQL casing or
116
+ trimming, and deliberately fail closed on duplicate normalized keys.
53
117
  - **Optional tenancy** on Profile; AuditLog allows super-admin bypass
package/README.md CHANGED
@@ -5,69 +5,46 @@ Central identity system with multi-auth (Nostr/OIDC/API keys/magic links), relat
5
5
  ## Installation
6
6
 
7
7
  ```bash
8
- pnpm add @happyvertical/smrt-profiles
8
+ pnpm add @happyvertical/smrt-profiles @happyvertical/sql
9
9
  ```
10
10
 
11
11
  ## Usage
12
12
 
13
13
  ```typescript
14
14
  import {
15
- Profile,
16
- ProfileCollection,
17
- OidcIdentity,
18
- OidcIdentityCollection,
19
- ApiKey,
20
- ApiKeyCollection,
21
- NostrIdentity,
22
- generateNostrKeypair,
15
+ backfillProfileEmailKeys,
23
16
  createProfileFromOidc,
24
17
  } from '@happyvertical/smrt-profiles';
25
-
26
- // Create a profile
27
- const profile = new Profile({
28
- name: 'Alice Johnson',
29
- email: 'alice@example.com',
30
- });
31
- await profile.save();
32
-
33
- // OIDC identity (Keycloak/Google/GitHub)
34
- const oidcProfile = await createProfileFromOidc({
35
- issuer: 'https://accounts.google.com',
36
- subject: 'abc123',
37
- email: 'alice@example.com',
38
- name: 'Alice Johnson',
39
- });
40
-
41
- // Nostr identity (encrypted keypair, requires SERVER_MASTER_SECRET)
42
- const keypair = generateNostrKeypair();
43
- const nostr = new NostrIdentity({
44
- profileId: profile.id,
45
- pubkey: keypair.pubkey,
46
- });
47
- await nostr.save();
48
-
49
- // API key (plaintext returned once only)
50
- const { key, apiKey } = await ApiKey.generate({
51
- profileId: profile.id,
52
- scope: 'read:profiles',
53
- expiresAt: new Date('2025-12-31'),
54
- });
55
- // key = plaintext (store now), apiKey.keyPrefix = visible identifier
56
-
57
- // Relationships (auto-creates reciprocal inverse)
58
- const bob = new Profile({ name: 'Bob Smith', email: 'bob@example.com' });
59
- await bob.save();
60
- await profile.addRelationship(bob, 'friend');
61
- const friends = await profile.getRelationships({ direction: 'from' });
18
+ import { getDatabase } from '@happyvertical/sql';
19
+
20
+ // Connect after applying schema migrations.
21
+ const persistence = { type: 'sqlite' as const, url: 'file:profiles.db' };
22
+ // Mark migrated Profile email keys ready before enabling OIDC provisioning.
23
+ const db = await getDatabase(persistence);
24
+ await backfillProfileEmailKeys(db);
25
+
26
+ // Provision a new OIDC Profile or reuse its exact issuer/subject link.
27
+ const { profile, oidcIdentity, created } = await createProfileFromOidc(
28
+ {
29
+ iss: 'https://accounts.google.com',
30
+ sub: 'abc123',
31
+ email: 'olivia@example.com',
32
+ email_verified: true,
33
+ name: 'Olivia Smith',
34
+ },
35
+ 'google',
36
+ { db },
37
+ );
62
38
  ```
63
39
 
64
40
  ### Owned assets
65
41
 
66
42
  ```typescript
67
43
  import { AssetCollection } from '@happyvertical/smrt-assets';
44
+ import { ProfileCollection } from '@happyvertical/smrt-profiles';
68
45
 
69
- const profiles = await ProfileCollection.create();
70
- const assets = await AssetCollection.create();
46
+ const profiles = await ProfileCollection.create({ db });
47
+ const assets = await AssetCollection.create({ db });
71
48
 
72
49
  const headshot = await assets.create({
73
50
  name: 'alice-headshot.jpg',
@@ -128,6 +105,77 @@ const galleryAssets = await profiles.getAssets(profile.id!, 'gallery');
128
105
  | `NostrIdentityCollection` | Nostr identity lookup (includes NIP-05) |
129
106
  | `OidcIdentityCollection` | OIDC identity lookup |
130
107
 
108
+ `ProfileCollection.findUniqueGlobalPersonByEmail(email)` is the supported
109
+ verified-identity lookup. It reads across tenant scopes and returns a Profile
110
+ only when the case-insensitive email has exactly one match and that row is a
111
+ global `Person`; tenant-scoped, non-Person, and duplicate matches throw a
112
+ `CanonicalPersonProfileError`. Matching uses readonly, indexed
113
+ `Profile.emailKey`, derived with the exported TypeScript
114
+ `normalizeIdentityEmail()` helper so Unicode casing and whitespace behave the
115
+ same on every database adapter. Use
116
+ `requireCanonicalGlobalPerson(profileId, email?)` to validate an
117
+ application-selected Profile against the same invariant. When `email` is
118
+ omitted, the helper validates uniqueness using the Profile's current stored
119
+ email. `reserveCanonicalIdentityEmail(profileId, email)` additionally claims
120
+ the normalized address in the private
121
+ `oidc_profile_email_reservations` table. Omit `email` to synchronize the
122
+ reservation from the Profile's current stored email; changing the address moves
123
+ the reservation, and clearing it removes the reservation. Legacy simple
124
+ `Person` STI discriminators remain valid and are handled by core's normal
125
+ upgrade path.
126
+
127
+ `OidcIdentity.identityKey` stores a nullable, unique issuer/subject natural key
128
+ for transaction-safe first-login races. The model derives it from issuer and
129
+ subject on every save, so callers cannot desynchronize it. New links populate
130
+ it and legacy rows backfill it when reused. Issuer and subject are opaque,
131
+ case-sensitive OIDC identifiers: surrounding whitespace is preserved and is
132
+ part of the key; trimming is used only to reject an all-whitespace claim. The
133
+ generated REST, MCP, and CLI surfaces are read-only because identity mutation
134
+ is an authentication authority change; trusted callers link identities through
135
+ the transactional provisioning APIs. The legacy
136
+ `OidcIdentity.findOrCreate()` method is deprecated: it transactionally reuses
137
+ one exact safe issuer/subject link for compatibility but refuses to create a
138
+ new authentication link. Use `createProfileFromOidc()` or the users package's
139
+ owner-aware provisioning API for creation. The deprecated
140
+ `OidcIdentityCollection.linkToProfile()` and `Profile.linkOidcIdentity()`
141
+ helpers delegate to the same exact-reuse-only path and cannot plant or rebind a
142
+ link.
143
+ Existing installations must stop or upgrade legacy Profile writers, run
144
+ `smrt db:status`, `smrt db:migrate`, and then `smrt db:status` before deploying
145
+ this version to add the identity keys and private email-reservation table. Then
146
+ run `backfillProfileEmailKeys(db)` once from a single deploy process before
147
+ enabling verified-email provisioning. The transaction-safe backfill is
148
+ idempotent; canonical email lookup and reservation fail closed with
149
+ `email_key_backfill_required` while the standard `_smrt_backfills` readiness
150
+ marker is absent. Exact issuer/subject reuse does not depend on email-key
151
+ readiness because it does not perform email-based linking.
152
+ The identity-boundary helpers use the readiness guard and indexed key. The
153
+ general-purpose `ProfileCollection.findByEmail()` retains its compatible legacy
154
+ lookup behavior and is not suitable for identity linking. Only the explicit
155
+ deploy-time backfill scans the Profile table and records readiness; guarded
156
+ runtime identity lookups use the indexed key and validate returned candidates.
157
+ `createProfileFromOidc()` requires a transaction-capable database. Pass the
158
+ root database, which must expose `beginTransaction`, and SMRT owns the
159
+ transaction; an already transaction-bound handle is supported through a
160
+ savepoint. Paths that perform canonical email lookup or reservation require
161
+ `_smrt_backfills` to exist first. Provisioning never attempts tracker DDL on
162
+ a caller-owned transaction; for those paths, if the table is absent, pass the
163
+ root database so SMRT can initialize it outside the transaction. Exact
164
+ issuer/subject reuse skips the email-key readiness-marker lookup, but root
165
+ coordination still initializes the shared tracker table. Caller-owned exact
166
+ reuse does not consult the tracker and therefore does not require that table.
167
+ A handle exposing only `transaction()` is ambiguous and fails closed. For
168
+ adapters without nested savepoint support, including DuckDB, pass
169
+ the root database rather than calling the helper inside an outer transaction.
170
+ Provisioning fails before durable writes when neither safe path is available.
171
+ The Profile-only helper preserves exact issuer/subject reuse, including legacy
172
+ links to tenant-scoped or non-Person Profiles, but never attaches a new identity
173
+ to an existing email match because this package cannot prove whether a User owns
174
+ that Profile. User/session provisioning still rejects those unsafe linked
175
+ Profiles before creating authentication state. Use
176
+ `UserCollection.getOrCreateFromOidc()` from `@happyvertical/smrt-users` for
177
+ owner-aware verified-email reuse and the supported pre-provision resolver hook.
178
+
131
179
  `Profile` and `ProfileCollection` both expose `getAssets()`, `addAsset()`, and
132
180
  `removeAsset()` helpers backed by `profile_assets`. Typical relationships are
133
181
  `avatar`, `gallery`, and `attachment`.
@@ -137,7 +185,10 @@ const galleryAssets = await profiles.getAssets(profile.id!, 'gallery');
137
185
  | Export | Description |
138
186
  |--------|------------|
139
187
  | `resolveIdentity` | Resolve profile from any auth method |
140
- | `createProfileFromOidc` | Create profile + OIDC identity in one call |
188
+ | `createProfileFromOidc` | Create a Profile + OIDC identity or reuse an exact issuer/subject link; existing email matches fail closed |
189
+ | `normalizeIdentityEmail` | Adapter-independent Unicode/whitespace email canonicalizer used by identity keys |
190
+ | `backfillProfileEmailKeys` | Transactionally populate normalized keys for migrated Profiles |
191
+ | `PROFILE_EMAIL_KEY_BACKFILL_NAME` | Durable readiness-marker name recorded by the Profile email-key backfill |
141
192
  | `createProfileFromNostr` | Create profile + Nostr identity in one call |
142
193
  | `createAuthEvent` | Create a Nostr auth event |
143
194
  | `verifyAuthEvent` | Verify a Nostr auth event signature |
@@ -161,7 +212,7 @@ const galleryAssets = await profiles.getAssets(profile.id!, 'gallery');
161
212
 
162
213
  ### Key Types
163
214
 
164
- `ProfileOptions`, `ProfileTypeOptions`, `ProfileMetadataOptions`, `ProfileMetafieldOptions`, `ProfileRelationshipOptions`, `ProfileRelationshipTypeOptions`, `ProfileRelationshipTermOptions`, `OidcIdentityOptions`, `NostrIdentityOptions`, `ApiKeyOptions`, `GenerateKeyResult`, `MagicLinkTokenOptions`, `GenerateTokenResult`, `AuditLogOptions`, `AuditSource`, `AuthContext`, `ResolveIdentityResult`, `InitiateResult`, `VerifyResult`, `MagicLinkConfig`, `MagicLinkService`, `Nip05HandlerConfig`, `Nip05HandlerResult`, `Nip05Request`, `Nip05Response`, `NostrEvent`, `NostrKeypair`, `EncryptedKey`, `ValidationSchema`, `ValidatorFunction`, `ReciprocalHandler`
215
+ `ProfileOptions`, `ProfileTypeOptions`, `ProfileMetadataOptions`, `ProfileMetafieldOptions`, `ProfileRelationshipOptions`, `ProfileRelationshipTypeOptions`, `ProfileRelationshipTermOptions`, `OidcIdentityOptions`, `CanonicalPersonProfileErrorCode`, `NostrIdentityOptions`, `ApiKeyOptions`, `GenerateKeyResult`, `MagicLinkTokenOptions`, `GenerateTokenResult`, `AuditLogOptions`, `AuditSource`, `AuthContext`, `ResolveIdentityResult`, `InitiateResult`, `VerifyResult`, `MagicLinkConfig`, `MagicLinkService`, `Nip05HandlerConfig`, `Nip05HandlerResult`, `Nip05Request`, `Nip05Response`, `NostrEvent`, `NostrKeypair`, `EncryptedKey`, `ValidationSchema`, `ValidatorFunction`, `ReciprocalHandler`
165
216
 
166
217
  ## Dependencies
167
218
 
@@ -1,4 +1,4 @@
1
- import { x as __exportAll } from "./NostrIdentityCollection-CG6HGf6c.js";
1
+ import { y as __exportAll } from "./NostrIdentity-DF_a_WZk.js";
2
2
  import { SmrtObject, foreignKey, smrt } from "@happyvertical/smrt-core";
3
3
  import { createHash, randomBytes } from "node:crypto";
4
4
  //#region src/models/ApiKey.ts
@@ -116,7 +116,7 @@ var ApiKey = class extends SmrtObject {
116
116
  */
117
117
  static async verify(key, options = {}) {
118
118
  const keyHash = ApiKey.hashKey(key);
119
- const { ApiKeyCollection } = await import("./ApiKeyCollection-DNhDkS7O.js").then((n) => n.n);
119
+ const { ApiKeyCollection } = await import("./ApiKeyCollection-CrTjdMz8.js").then((n) => n.n);
120
120
  const apiKey = await (await ApiKeyCollection.create(options)).findOne({ where: { keyHash } });
121
121
  if (!apiKey) return null;
122
122
  if (!apiKey.isValid()) return null;
@@ -142,4 +142,4 @@ ApiKey = __decorateClass([smrt({
142
142
  //#endregion
143
143
  export { ApiKey_exports as n, ApiKey as t };
144
144
 
145
- //# sourceMappingURL=ApiKey-BrNyaaZN.js.map
145
+ //# sourceMappingURL=ApiKey-Br9NGBnr.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"ApiKey-BrNyaaZN.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 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 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 this.lastUsedAt = new Date();\n await this.save();\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 return apiKey;\n }\n}\n"],"mappings":";;;;;;;;;;;;;AAoCO,IAAM,SAAN,cAAqB,WAAW;CAKrC;;;;CAKA,UAAkB;;;;CAKlB,YAAoB;;;;CAKpB,OAAe;;;;CAKf,SAAmB,CAAC;;;;CAKpB,aAA0B;;;;CAK1B,YAAyB;;;;CAKzB,YAAyB;CAEzB,YAAY,UAAyB,CAAC,GAAG;EACvC,MAAM,OAAO;EACb,IAAI,QAAQ,WAAW,KAAK,YAAY,QAAQ;EAChD,IAAI,QAAQ,MAAM,KAAK,OAAO,QAAQ;EACtC,IAAI,QAAQ,QAAQ,KAAK,SAAS,QAAQ;EAC1C,IAAI,QAAQ,cAAc,KAAA,GAAW,KAAK,YAAY,QAAQ;CAChE;;;;CAKA,MAAM,aAAsC;EAC1C,OAAQ,MAAM,KAAK,WAAW,WAAW;CAC3C;;;;CAKA,UAAmB;EACjB,IAAI,KAAK,WAAW,OAAO;EAC3B,IAAI,KAAK,aAAa,KAAK,4BAAY,IAAI,KAAK,GAAG,OAAO;EAC1D,OAAO;CACT;;;;CAKA,SAAS,OAAwB;EAC/B,OAAO,KAAK,OAAO,SAAS,KAAK,KAAK,KAAK,OAAO,SAAS,GAAG;CAChE;;;;CAKA,MAAM,SAAwB;EAC5B,KAAK,4BAAY,IAAI,KAAK;EAC1B,MAAM,KAAK,KAAK;CAClB;;;;CAKA,MAAM,cAA6B;EACjC,KAAK,6BAAa,IAAI,KAAK;EAC3B,MAAM,KAAK,KAAK;CAClB;;;;CAKA,OAAO,QAAQ,KAAqB;EAClC,OAAO,WAAW,QAAQ,CAAA,CAAE,OAAO,GAAG,CAAA,CAAE,OAAO,KAAK;CACtD;;;;CAKA,aAAa,SACX,SACA,SAM4B;EAG5B,MAAM,MAAM,WADO,YAAY,EAAE,CAAA,CAAE,SAAS,KACrB;EACvB,MAAM,YAAY,IAAI,UAAU,GAAG,EAAE;EACrC,MAAM,UAAU,OAAO,QAAQ,GAAG;EAElC,MAAM,SAAS,IAAI,OAAO;GACxB,IAAI,QAAQ,MAAM,QAAQ,SAAS;GACnC,WAAW,QAAQ;GACnB,MAAM,QAAQ;GACd,QAAQ,QAAQ,UAAU,CAAC,GAAG;GAC9B,WAAW,QAAQ,aAAa;EAClC,CAAC;EACD,OAAO,UAAU;EACjB,OAAO,YAAY;EAEnB,MAAM,OAAO,WAAW;EACxB,MAAM,OAAO,KAAK;EAElB,OAAO;GAAE;GAAK;EAAO;CACvB;;;;CAKA,aAAa,OACX,KACA,UAA6B,CAAC,GACN;EACxB,MAAM,UAAU,OAAO,QAAQ,GAAG;EAElC,MAAM,EAAE,qBAAqB,MAAM,OACjC,iCAAA,CAAA,MAAA,MAAA,EAAA,CAAA;EAIF,MAAM,SAAS,OAAM,MAFI,iBAAiB,OAAO,OAAO,EAAA,CAExB,QAAQ,EACtC,OAAO,EAAE,QAAQ,EACnB,CAAC;EAED,IAAI,CAAC,QAAQ,OAAO;EACpB,IAAI,CAAC,OAAO,QAAQ,GAAG,OAAO;EAG9B,MAAM,OAAO,YAAY;EAEzB,OAAO;CACT;AACF;AAvJE,gBAAA,CADC,WAAW,WAAW,EAAE,UAAU,KAAK,CAAC,CAAA,GAJ9B,OAKX,WAAA,aAAA,CAAA;AALW,SAAN,gBAAA,CARN,KAAK;CACJ,WAAW;CACX,KAAK,EAAE,SAAS,CAAC,QAAQ,KAAK,EAAE;CAChC,KAAK,EAAE,SAAS,CAAC,QAAQ,KAAK,EAAE;CAGhC,KAAK;EAAE,SAAS;GAAC;GAAQ;GAAO;GAAU;EAAQ;EAAG,cAAc;CAAK;AAC1E,CAAC,CAAA,GACY,MAAA"}
1
+ {"version":3,"file":"ApiKey-Br9NGBnr.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 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 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 this.lastUsedAt = new Date();\n await this.save();\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 return apiKey;\n }\n}\n"],"mappings":";;;;;;;;;;;;;AAoCO,IAAM,SAAN,cAAqB,WAAW;CAKrC;;;;CAKA,UAAkB;;;;CAKlB,YAAoB;;;;CAKpB,OAAe;;;;CAKf,SAAmB,CAAC;;;;CAKpB,aAA0B;;;;CAK1B,YAAyB;;;;CAKzB,YAAyB;CAEzB,YAAY,UAAyB,CAAC,GAAG;EACvC,MAAM,OAAO;EACb,IAAI,QAAQ,WAAW,KAAK,YAAY,QAAQ;EAChD,IAAI,QAAQ,MAAM,KAAK,OAAO,QAAQ;EACtC,IAAI,QAAQ,QAAQ,KAAK,SAAS,QAAQ;EAC1C,IAAI,QAAQ,cAAc,KAAA,GAAW,KAAK,YAAY,QAAQ;CAChE;;;;CAKA,MAAM,aAAsC;EAC1C,OAAQ,MAAM,KAAK,WAAW,WAAW;CAC3C;;;;CAKA,UAAmB;EACjB,IAAI,KAAK,WAAW,OAAO;EAC3B,IAAI,KAAK,aAAa,KAAK,4BAAY,IAAI,KAAK,GAAG,OAAO;EAC1D,OAAO;CACT;;;;CAKA,SAAS,OAAwB;EAC/B,OAAO,KAAK,OAAO,SAAS,KAAK,KAAK,KAAK,OAAO,SAAS,GAAG;CAChE;;;;CAKA,MAAM,SAAwB;EAC5B,KAAK,4BAAY,IAAI,KAAK;EAC1B,MAAM,KAAK,KAAK;CAClB;;;;CAKA,MAAM,cAA6B;EACjC,KAAK,6BAAa,IAAI,KAAK;EAC3B,MAAM,KAAK,KAAK;CAClB;;;;CAKA,OAAO,QAAQ,KAAqB;EAClC,OAAO,WAAW,QAAQ,CAAA,CAAE,OAAO,GAAG,CAAA,CAAE,OAAO,KAAK;CACtD;;;;CAKA,aAAa,SACX,SACA,SAM4B;EAG5B,MAAM,MAAM,WADO,YAAY,EAAE,CAAA,CAAE,SAAS,KACrB;EACvB,MAAM,YAAY,IAAI,UAAU,GAAG,EAAE;EACrC,MAAM,UAAU,OAAO,QAAQ,GAAG;EAElC,MAAM,SAAS,IAAI,OAAO;GACxB,IAAI,QAAQ,MAAM,QAAQ,SAAS;GACnC,WAAW,QAAQ;GACnB,MAAM,QAAQ;GACd,QAAQ,QAAQ,UAAU,CAAC,GAAG;GAC9B,WAAW,QAAQ,aAAa;EAClC,CAAC;EACD,OAAO,UAAU;EACjB,OAAO,YAAY;EAEnB,MAAM,OAAO,WAAW;EACxB,MAAM,OAAO,KAAK;EAElB,OAAO;GAAE;GAAK;EAAO;CACvB;;;;CAKA,aAAa,OACX,KACA,UAA6B,CAAC,GACN;EACxB,MAAM,UAAU,OAAO,QAAQ,GAAG;EAElC,MAAM,EAAE,qBAAqB,MAAM,OACjC,iCAAA,CAAA,MAAA,MAAA,EAAA,CAAA;EAIF,MAAM,SAAS,OAAM,MAFI,iBAAiB,OAAO,OAAO,EAAA,CAExB,QAAQ,EACtC,OAAO,EAAE,QAAQ,EACnB,CAAC;EAED,IAAI,CAAC,QAAQ,OAAO;EACpB,IAAI,CAAC,OAAO,QAAQ,GAAG,OAAO;EAG9B,MAAM,OAAO,YAAY;EAEzB,OAAO;CACT;AACF;AAvJE,gBAAA,CADC,WAAW,WAAW,EAAE,UAAU,KAAK,CAAC,CAAA,GAJ9B,OAKX,WAAA,aAAA,CAAA;AALW,SAAN,gBAAA,CARN,KAAK;CACJ,WAAW;CACX,KAAK,EAAE,SAAS,CAAC,QAAQ,KAAK,EAAE;CAChC,KAAK,EAAE,SAAS,CAAC,QAAQ,KAAK,EAAE;CAGhC,KAAK;EAAE,SAAS;GAAC;GAAQ;GAAO;GAAU;EAAQ;EAAG,cAAc;CAAK;AAC1E,CAAC,CAAA,GACY,MAAA"}
@@ -1,5 +1,5 @@
1
- import { x as __exportAll } from "./NostrIdentityCollection-CG6HGf6c.js";
2
- import { t as ApiKey } from "./ApiKey-BrNyaaZN.js";
1
+ import { y as __exportAll } from "./NostrIdentity-DF_a_WZk.js";
2
+ import { t as ApiKey } from "./ApiKey-Br9NGBnr.js";
3
3
  import { SmrtCollection } from "@happyvertical/smrt-core";
4
4
  //#region src/collections/ApiKeyCollection.ts
5
5
  var ApiKeyCollection_exports = /* @__PURE__ */ __exportAll({ ApiKeyCollection: () => ApiKeyCollection });
@@ -80,4 +80,4 @@ var ApiKeyCollection = class extends SmrtCollection {
80
80
  //#endregion
81
81
  export { ApiKeyCollection_exports as n, ApiKeyCollection as t };
82
82
 
83
- //# sourceMappingURL=ApiKeyCollection-DNhDkS7O.js.map
83
+ //# sourceMappingURL=ApiKeyCollection-CrTjdMz8.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"ApiKeyCollection-DNhDkS7O.js","names":[],"sources":["../../src/collections/ApiKeyCollection.ts"],"sourcesContent":["/**\n * ApiKeyCollection - Collection for managing API keys\n */\n\nimport { SmrtCollection } from '@happyvertical/smrt-core';\nimport { ApiKey, type GenerateKeyResult } from '../models/ApiKey';\nimport type { Profile } from '../models/Profile';\n\nexport class ApiKeyCollection extends SmrtCollection<ApiKey> {\n static readonly _itemClass = ApiKey;\n\n /**\n * Find all keys for a profile\n */\n async findByProfile(profileId: string): Promise<ApiKey[]> {\n return await this.list({\n where: { profileId },\n });\n }\n\n /**\n * Find active (non-revoked, non-expired) keys for a profile\n */\n async findActiveByProfile(profileId: string): Promise<ApiKey[]> {\n const keys = await this.findByProfile(profileId);\n return keys.filter((key) => key.isValid());\n }\n\n /**\n * Find key by hash\n */\n async findByHash(keyHash: string): Promise<ApiKey | null> {\n return await this.findOne({\n where: { keyHash },\n });\n }\n\n /**\n * Verify an API key string and return the record if valid\n */\n async verify(key: string): Promise<ApiKey | null> {\n const keyHash = ApiKey.hashKey(key);\n const apiKey = await this.findByHash(keyHash);\n\n if (!apiKey) return null;\n if (!apiKey.isValid()) return null;\n\n // Record usage\n await apiKey.recordUsage();\n\n return apiKey;\n }\n\n /**\n * Generate a new key for a profile\n */\n async generateForProfile(\n profile: Profile,\n options: {\n name: string;\n scopes?: string[];\n expiresAt?: Date | null;\n },\n ): Promise<GenerateKeyResult> {\n return await ApiKey.generate(profile, {\n ...options,\n db: this.options.db,\n });\n }\n\n /**\n * Revoke all keys for a profile\n */\n async revokeAllForProfile(profileId: string): Promise<number> {\n const keys = await this.findActiveByProfile(profileId);\n for (const key of keys) {\n await key.revoke();\n }\n return keys.length;\n }\n\n /**\n * Revoke a specific key by prefix\n */\n async revokeByPrefix(keyPrefix: string): Promise<boolean> {\n const key = await this.findOne({\n where: { keyPrefix },\n });\n\n if (key?.isValid()) {\n await key.revoke();\n return true;\n }\n return false;\n }\n\n /**\n * Clean up expired keys (soft delete by setting revokedAt)\n */\n async cleanupExpired(): Promise<number> {\n const now = new Date();\n const allKeys = await this.list({});\n let cleaned = 0;\n\n for (const key of allKeys) {\n if (key.expiresAt && key.expiresAt < now && !key.revokedAt) {\n key.revokedAt = now;\n await key.save();\n cleaned++;\n }\n }\n\n return cleaned;\n }\n}\n"],"mappings":";;;;;AAQO,IAAM,mBAAN,cAA+B,eAAuB;CAC3D,OAAgB,aAAa;;;;CAK7B,MAAM,cAAc,WAAsC;EACxD,OAAO,MAAM,KAAK,KAAK,EACrB,OAAO,EAAE,UAAU,EACrB,CAAC;CACH;;;;CAKA,MAAM,oBAAoB,WAAsC;EAE9D,QAAO,MADY,KAAK,cAAc,SAAS,EAAA,CACnC,QAAQ,QAAQ,IAAI,QAAQ,CAAC;CAC3C;;;;CAKA,MAAM,WAAW,SAAyC;EACxD,OAAO,MAAM,KAAK,QAAQ,EACxB,OAAO,EAAE,QAAQ,EACnB,CAAC;CACH;;;;CAKA,MAAM,OAAO,KAAqC;EAChD,MAAM,UAAU,OAAO,QAAQ,GAAG;EAClC,MAAM,SAAS,MAAM,KAAK,WAAW,OAAO;EAE5C,IAAI,CAAC,QAAQ,OAAO;EACpB,IAAI,CAAC,OAAO,QAAQ,GAAG,OAAO;EAG9B,MAAM,OAAO,YAAY;EAEzB,OAAO;CACT;;;;CAKA,MAAM,mBACJ,SACA,SAK4B;EAC5B,OAAO,MAAM,OAAO,SAAS,SAAS;GACpC,GAAG;GACH,IAAI,KAAK,QAAQ;EACnB,CAAC;CACH;;;;CAKA,MAAM,oBAAoB,WAAoC;EAC5D,MAAM,OAAO,MAAM,KAAK,oBAAoB,SAAS;EACrD,KAAA,MAAW,OAAO,MAChB,MAAM,IAAI,OAAO;EAEnB,OAAO,KAAK;CACd;;;;CAKA,MAAM,eAAe,WAAqC;EACxD,MAAM,MAAM,MAAM,KAAK,QAAQ,EAC7B,OAAO,EAAE,UAAU,EACrB,CAAC;EAED,IAAI,KAAK,QAAQ,GAAG;GAClB,MAAM,IAAI,OAAO;GACjB,OAAO;EACT;EACA,OAAO;CACT;;;;CAKA,MAAM,iBAAkC;EACtC,MAAM,sBAAM,IAAI,KAAK;EACrB,MAAM,UAAU,MAAM,KAAK,KAAK,CAAC,CAAC;EAClC,IAAI,UAAU;EAEd,KAAA,MAAW,OAAO,SAChB,IAAI,IAAI,aAAa,IAAI,YAAY,OAAO,CAAC,IAAI,WAAW;GAC1D,IAAI,YAAY;GAChB,MAAM,IAAI,KAAK;GACf;EACF;EAGF,OAAO;CACT;AACF"}
1
+ {"version":3,"file":"ApiKeyCollection-CrTjdMz8.js","names":[],"sources":["../../src/collections/ApiKeyCollection.ts"],"sourcesContent":["/**\n * ApiKeyCollection - Collection for managing API keys\n */\n\nimport { SmrtCollection } from '@happyvertical/smrt-core';\nimport { ApiKey, type GenerateKeyResult } from '../models/ApiKey';\nimport type { Profile } from '../models/Profile';\n\nexport class ApiKeyCollection extends SmrtCollection<ApiKey> {\n static readonly _itemClass = ApiKey;\n\n /**\n * Find all keys for a profile\n */\n async findByProfile(profileId: string): Promise<ApiKey[]> {\n return await this.list({\n where: { profileId },\n });\n }\n\n /**\n * Find active (non-revoked, non-expired) keys for a profile\n */\n async findActiveByProfile(profileId: string): Promise<ApiKey[]> {\n const keys = await this.findByProfile(profileId);\n return keys.filter((key) => key.isValid());\n }\n\n /**\n * Find key by hash\n */\n async findByHash(keyHash: string): Promise<ApiKey | null> {\n return await this.findOne({\n where: { keyHash },\n });\n }\n\n /**\n * Verify an API key string and return the record if valid\n */\n async verify(key: string): Promise<ApiKey | null> {\n const keyHash = ApiKey.hashKey(key);\n const apiKey = await this.findByHash(keyHash);\n\n if (!apiKey) return null;\n if (!apiKey.isValid()) return null;\n\n // Record usage\n await apiKey.recordUsage();\n\n return apiKey;\n }\n\n /**\n * Generate a new key for a profile\n */\n async generateForProfile(\n profile: Profile,\n options: {\n name: string;\n scopes?: string[];\n expiresAt?: Date | null;\n },\n ): Promise<GenerateKeyResult> {\n return await ApiKey.generate(profile, {\n ...options,\n db: this.options.db,\n });\n }\n\n /**\n * Revoke all keys for a profile\n */\n async revokeAllForProfile(profileId: string): Promise<number> {\n const keys = await this.findActiveByProfile(profileId);\n for (const key of keys) {\n await key.revoke();\n }\n return keys.length;\n }\n\n /**\n * Revoke a specific key by prefix\n */\n async revokeByPrefix(keyPrefix: string): Promise<boolean> {\n const key = await this.findOne({\n where: { keyPrefix },\n });\n\n if (key?.isValid()) {\n await key.revoke();\n return true;\n }\n return false;\n }\n\n /**\n * Clean up expired keys (soft delete by setting revokedAt)\n */\n async cleanupExpired(): Promise<number> {\n const now = new Date();\n const allKeys = await this.list({});\n let cleaned = 0;\n\n for (const key of allKeys) {\n if (key.expiresAt && key.expiresAt < now && !key.revokedAt) {\n key.revokedAt = now;\n await key.save();\n cleaned++;\n }\n }\n\n return cleaned;\n }\n}\n"],"mappings":";;;;;AAQO,IAAM,mBAAN,cAA+B,eAAuB;CAC3D,OAAgB,aAAa;;;;CAK7B,MAAM,cAAc,WAAsC;EACxD,OAAO,MAAM,KAAK,KAAK,EACrB,OAAO,EAAE,UAAU,EACrB,CAAC;CACH;;;;CAKA,MAAM,oBAAoB,WAAsC;EAE9D,QAAO,MADY,KAAK,cAAc,SAAS,EAAA,CACnC,QAAQ,QAAQ,IAAI,QAAQ,CAAC;CAC3C;;;;CAKA,MAAM,WAAW,SAAyC;EACxD,OAAO,MAAM,KAAK,QAAQ,EACxB,OAAO,EAAE,QAAQ,EACnB,CAAC;CACH;;;;CAKA,MAAM,OAAO,KAAqC;EAChD,MAAM,UAAU,OAAO,QAAQ,GAAG;EAClC,MAAM,SAAS,MAAM,KAAK,WAAW,OAAO;EAE5C,IAAI,CAAC,QAAQ,OAAO;EACpB,IAAI,CAAC,OAAO,QAAQ,GAAG,OAAO;EAG9B,MAAM,OAAO,YAAY;EAEzB,OAAO;CACT;;;;CAKA,MAAM,mBACJ,SACA,SAK4B;EAC5B,OAAO,MAAM,OAAO,SAAS,SAAS;GACpC,GAAG;GACH,IAAI,KAAK,QAAQ;EACnB,CAAC;CACH;;;;CAKA,MAAM,oBAAoB,WAAoC;EAC5D,MAAM,OAAO,MAAM,KAAK,oBAAoB,SAAS;EACrD,KAAA,MAAW,OAAO,MAChB,MAAM,IAAI,OAAO;EAEnB,OAAO,KAAK;CACd;;;;CAKA,MAAM,eAAe,WAAqC;EACxD,MAAM,MAAM,MAAM,KAAK,QAAQ,EAC7B,OAAO,EAAE,UAAU,EACrB,CAAC;EAED,IAAI,KAAK,QAAQ,GAAG;GAClB,MAAM,IAAI,OAAO;GACjB,OAAO;EACT;EACA,OAAO;CACT;;;;CAKA,MAAM,iBAAkC;EACtC,MAAM,sBAAM,IAAI,KAAK;EACrB,MAAM,UAAU,MAAM,KAAK,KAAK,CAAC,CAAC;EAClC,IAAI,UAAU;EAEd,KAAA,MAAW,OAAO,SAChB,IAAI,IAAI,aAAa,IAAI,YAAY,OAAO,CAAC,IAAI,WAAW;GAC1D,IAAI,YAAY;GAChB,MAAM,IAAI,KAAK;GACf;EACF;EAGF,OAAO;CACT;AACF"}
@@ -1,4 +1,4 @@
1
- import { x as __exportAll } from "./NostrIdentityCollection-CG6HGf6c.js";
1
+ import { y as __exportAll } from "./NostrIdentity-DF_a_WZk.js";
2
2
  import { SmrtCollection, SmrtObject, foreignKey, smrt } from "@happyvertical/smrt-core";
3
3
  import { TenantScoped, tenantId } from "@happyvertical/smrt-tenancy";
4
4
  //#region src/models/AuditLog.ts
@@ -188,4 +188,4 @@ var AuditLogCollection = class extends SmrtCollection {
188
188
  //#endregion
189
189
  export { AuditLogCollection_exports as n, AuditLog as r, AuditLogCollection as t };
190
190
 
191
- //# sourceMappingURL=AuditLogCollection-CnIYU0aK.js.map
191
+ //# sourceMappingURL=AuditLogCollection-BBIEI6BD.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"AuditLogCollection-CnIYU0aK.js","names":[],"sources":["../../src/models/AuditLog.ts","../../src/collections/AuditLogCollection.ts"],"sourcesContent":["/**\n * AuditLog - Tracks all actions for full audit trail\n *\n * Records who did what, when, and how (web, CLI, CI, webhook, MCP).\n * Supports pass-through identity for CI workflows.\n */\n\nimport {\n foreignKey,\n SmrtObject,\n type SmrtObjectOptions,\n smrt,\n} from '@happyvertical/smrt-core';\nimport { TenantScoped, tenantId } from '@happyvertical/smrt-tenancy';\nimport type { Profile } from './Profile';\n\nexport type AuditSource = 'web' | 'cli' | 'ci' | 'webhook' | 'mcp';\n\nexport interface AuditLogOptions extends SmrtObjectOptions {\n profileId?: string;\n action?: string;\n resourceType?: string;\n resourceId?: string;\n source?: AuditSource;\n metadata?: Record<string, unknown>;\n onBehalfOfId?: string | null;\n tenantId?: string | null;\n}\n\n@TenantScoped({ mode: 'optional', allowSuperAdminBypass: true })\n@smrt({\n tableName: 'audit_logs',\n api: { include: ['list', 'get'] },\n mcp: { include: ['list', 'get'] },\n cli: { include: ['list'] },\n})\nexport class AuditLog extends SmrtObject {\n @tenantId({ nullable: true })\n tenantId: string | null = null;\n\n /**\n * The profile who performed the action\n */\n @foreignKey('Profile', { required: true })\n profileId?: string;\n\n /**\n * Action performed (e.g., 'issue.update', 'feedback.incorporate')\n */\n action: string = '';\n\n /**\n * Type of resource affected (e.g., 'Issue', 'Repository')\n */\n resourceType: string = '';\n\n /**\n * ID of the affected resource\n */\n resourceId: string = '';\n\n /**\n * Source of the action\n */\n source: AuditSource = 'web';\n\n /**\n * Additional context (CI run ID, request ID, etc.)\n */\n metadata: Record<string, unknown> = {};\n\n /**\n * For pass-through identity in CI - the actual person who triggered\n */\n @foreignKey('Profile', { nullable: true })\n onBehalfOfId?: string | null;\n\n constructor(options: AuditLogOptions = {}) {\n super(options);\n if (options.profileId) this.profileId = options.profileId;\n if (options.action) this.action = options.action;\n if (options.resourceType) this.resourceType = options.resourceType;\n if (options.resourceId) this.resourceId = options.resourceId;\n if (options.source) this.source = options.source;\n if (options.metadata) this.metadata = options.metadata;\n if (options.onBehalfOfId !== undefined)\n this.onBehalfOfId = options.onBehalfOfId;\n }\n\n /**\n * Get the profile who performed the action\n */\n async getProfile(): Promise<Profile | null> {\n return (await this.getRelated('profileId')) as Profile | null;\n }\n\n /**\n * Get the profile on whose behalf the action was performed (if any)\n */\n async getOnBehalfOf(): Promise<Profile | null> {\n if (!this.onBehalfOfId) return null;\n return (await this.getRelated('onBehalfOfId')) as Profile | null;\n }\n\n /**\n * Get the effective actor (onBehalfOf if set, otherwise profile)\n */\n async getEffectiveActor(): Promise<Profile | null> {\n const onBehalfOf = await this.getOnBehalfOf();\n if (onBehalfOf) return onBehalfOf;\n return await this.getProfile();\n }\n\n /**\n * Create an audit log entry\n */\n static async record(\n options: AuditLogOptions & {\n profile: Profile;\n onBehalfOf?: Profile | null;\n },\n ): Promise<AuditLog> {\n const log = new AuditLog({\n ...options,\n profileId: options.profile.id as string,\n onBehalfOfId: options.onBehalfOf?.id as string | undefined,\n });\n await log.initialize();\n await log.save();\n return log;\n }\n}\n","/**\n * AuditLogCollection - Collection for querying audit logs\n */\n\nimport { SmrtCollection } from '@happyvertical/smrt-core';\nimport { AuditLog, type AuditSource } from '../models/AuditLog';\nimport type { Profile } from '../models/Profile';\n\nexport class AuditLogCollection extends SmrtCollection<AuditLog> {\n static readonly _itemClass = AuditLog;\n\n /**\n * Find logs for a profile (actions they performed)\n */\n async findByProfile(profileId: string): Promise<AuditLog[]> {\n return await this.list({\n where: { profileId },\n orderBy: 'createdAt DESC',\n });\n }\n\n /**\n * Find logs for a resource\n */\n async findByResource(\n resourceType: string,\n resourceId: string,\n ): Promise<AuditLog[]> {\n return await this.list({\n where: { resourceType, resourceId },\n orderBy: 'createdAt DESC',\n });\n }\n\n /**\n * Find logs by action type\n */\n async findByAction(action: string): Promise<AuditLog[]> {\n return await this.list({\n where: { action },\n orderBy: 'createdAt DESC',\n });\n }\n\n /**\n * Find logs by source\n */\n async findBySource(source: AuditSource): Promise<AuditLog[]> {\n return await this.list({\n where: { source },\n orderBy: 'createdAt DESC',\n });\n }\n\n /**\n * Find logs where actions were performed on behalf of someone\n */\n async findOnBehalfOf(profileId: string): Promise<AuditLog[]> {\n return await this.list({\n where: { onBehalfOfId: profileId },\n orderBy: 'createdAt DESC',\n });\n }\n\n /**\n * Record a new audit log entry\n */\n async record(options: {\n profile: Profile;\n action: string;\n resourceType: string;\n resourceId: string;\n source?: AuditSource;\n metadata?: Record<string, unknown>;\n onBehalfOf?: Profile | null;\n }): Promise<AuditLog> {\n const log = new AuditLog({\n ...this.options,\n profileId: options.profile.id as string,\n action: options.action,\n resourceType: options.resourceType,\n resourceId: options.resourceId,\n source: options.source || 'web',\n metadata: options.metadata || {},\n onBehalfOfId: options.onBehalfOf?.id as string | undefined,\n });\n await log.initialize();\n await log.save();\n return log;\n }\n\n /**\n * Get recent activity for a profile\n */\n async getRecentActivity(\n profileId: string,\n limit: number = 50,\n ): Promise<AuditLog[]> {\n return await this.list({\n where: { profileId },\n orderBy: 'createdAt DESC',\n limit,\n });\n }\n\n /**\n * Get activity timeline for a resource\n */\n async getResourceTimeline(\n resourceType: string,\n resourceId: string,\n ): Promise<AuditLog[]> {\n return await this.list({\n where: { resourceType, resourceId },\n orderBy: 'createdAt ASC',\n });\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"AuditLogCollection-BBIEI6BD.js","names":[],"sources":["../../src/models/AuditLog.ts","../../src/collections/AuditLogCollection.ts"],"sourcesContent":["/**\n * AuditLog - Tracks all actions for full audit trail\n *\n * Records who did what, when, and how (web, CLI, CI, webhook, MCP).\n * Supports pass-through identity for CI workflows.\n */\n\nimport {\n foreignKey,\n SmrtObject,\n type SmrtObjectOptions,\n smrt,\n} from '@happyvertical/smrt-core';\nimport { TenantScoped, tenantId } from '@happyvertical/smrt-tenancy';\nimport type { Profile } from './Profile';\n\nexport type AuditSource = 'web' | 'cli' | 'ci' | 'webhook' | 'mcp';\n\nexport interface AuditLogOptions extends SmrtObjectOptions {\n profileId?: string;\n action?: string;\n resourceType?: string;\n resourceId?: string;\n source?: AuditSource;\n metadata?: Record<string, unknown>;\n onBehalfOfId?: string | null;\n tenantId?: string | null;\n}\n\n@TenantScoped({ mode: 'optional', allowSuperAdminBypass: true })\n@smrt({\n tableName: 'audit_logs',\n api: { include: ['list', 'get'] },\n mcp: { include: ['list', 'get'] },\n cli: { include: ['list'] },\n})\nexport class AuditLog extends SmrtObject {\n @tenantId({ nullable: true })\n tenantId: string | null = null;\n\n /**\n * The profile who performed the action\n */\n @foreignKey('Profile', { required: true })\n profileId?: string;\n\n /**\n * Action performed (e.g., 'issue.update', 'feedback.incorporate')\n */\n action: string = '';\n\n /**\n * Type of resource affected (e.g., 'Issue', 'Repository')\n */\n resourceType: string = '';\n\n /**\n * ID of the affected resource\n */\n resourceId: string = '';\n\n /**\n * Source of the action\n */\n source: AuditSource = 'web';\n\n /**\n * Additional context (CI run ID, request ID, etc.)\n */\n metadata: Record<string, unknown> = {};\n\n /**\n * For pass-through identity in CI - the actual person who triggered\n */\n @foreignKey('Profile', { nullable: true })\n onBehalfOfId?: string | null;\n\n constructor(options: AuditLogOptions = {}) {\n super(options);\n if (options.profileId) this.profileId = options.profileId;\n if (options.action) this.action = options.action;\n if (options.resourceType) this.resourceType = options.resourceType;\n if (options.resourceId) this.resourceId = options.resourceId;\n if (options.source) this.source = options.source;\n if (options.metadata) this.metadata = options.metadata;\n if (options.onBehalfOfId !== undefined)\n this.onBehalfOfId = options.onBehalfOfId;\n }\n\n /**\n * Get the profile who performed the action\n */\n async getProfile(): Promise<Profile | null> {\n return (await this.getRelated('profileId')) as Profile | null;\n }\n\n /**\n * Get the profile on whose behalf the action was performed (if any)\n */\n async getOnBehalfOf(): Promise<Profile | null> {\n if (!this.onBehalfOfId) return null;\n return (await this.getRelated('onBehalfOfId')) as Profile | null;\n }\n\n /**\n * Get the effective actor (onBehalfOf if set, otherwise profile)\n */\n async getEffectiveActor(): Promise<Profile | null> {\n const onBehalfOf = await this.getOnBehalfOf();\n if (onBehalfOf) return onBehalfOf;\n return await this.getProfile();\n }\n\n /**\n * Create an audit log entry\n */\n static async record(\n options: AuditLogOptions & {\n profile: Profile;\n onBehalfOf?: Profile | null;\n },\n ): Promise<AuditLog> {\n const log = new AuditLog({\n ...options,\n profileId: options.profile.id as string,\n onBehalfOfId: options.onBehalfOf?.id as string | undefined,\n });\n await log.initialize();\n await log.save();\n return log;\n }\n}\n","/**\n * AuditLogCollection - Collection for querying audit logs\n */\n\nimport { SmrtCollection } from '@happyvertical/smrt-core';\nimport { AuditLog, type AuditSource } from '../models/AuditLog';\nimport type { Profile } from '../models/Profile';\n\nexport class AuditLogCollection extends SmrtCollection<AuditLog> {\n static readonly _itemClass = AuditLog;\n\n /**\n * Find logs for a profile (actions they performed)\n */\n async findByProfile(profileId: string): Promise<AuditLog[]> {\n return await this.list({\n where: { profileId },\n orderBy: 'createdAt DESC',\n });\n }\n\n /**\n * Find logs for a resource\n */\n async findByResource(\n resourceType: string,\n resourceId: string,\n ): Promise<AuditLog[]> {\n return await this.list({\n where: { resourceType, resourceId },\n orderBy: 'createdAt DESC',\n });\n }\n\n /**\n * Find logs by action type\n */\n async findByAction(action: string): Promise<AuditLog[]> {\n return await this.list({\n where: { action },\n orderBy: 'createdAt DESC',\n });\n }\n\n /**\n * Find logs by source\n */\n async findBySource(source: AuditSource): Promise<AuditLog[]> {\n return await this.list({\n where: { source },\n orderBy: 'createdAt DESC',\n });\n }\n\n /**\n * Find logs where actions were performed on behalf of someone\n */\n async findOnBehalfOf(profileId: string): Promise<AuditLog[]> {\n return await this.list({\n where: { onBehalfOfId: profileId },\n orderBy: 'createdAt DESC',\n });\n }\n\n /**\n * Record a new audit log entry\n */\n async record(options: {\n profile: Profile;\n action: string;\n resourceType: string;\n resourceId: string;\n source?: AuditSource;\n metadata?: Record<string, unknown>;\n onBehalfOf?: Profile | null;\n }): Promise<AuditLog> {\n const log = new AuditLog({\n ...this.options,\n profileId: options.profile.id as string,\n action: options.action,\n resourceType: options.resourceType,\n resourceId: options.resourceId,\n source: options.source || 'web',\n metadata: options.metadata || {},\n onBehalfOfId: options.onBehalfOf?.id as string | undefined,\n });\n await log.initialize();\n await log.save();\n return log;\n }\n\n /**\n * Get recent activity for a profile\n */\n async getRecentActivity(\n profileId: string,\n limit: number = 50,\n ): Promise<AuditLog[]> {\n return await this.list({\n where: { profileId },\n orderBy: 'createdAt DESC',\n limit,\n });\n }\n\n /**\n * Get activity timeline for a resource\n */\n async getResourceTimeline(\n resourceType: string,\n resourceId: string,\n ): Promise<AuditLog[]> {\n return await this.list({\n where: { resourceType, resourceId },\n orderBy: 'createdAt ASC',\n });\n }\n}\n"],"mappings":"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"}
@@ -1,4 +1,4 @@
1
- import { SmrtCollection, SmrtObject, foreignKey, smrt } from "@happyvertical/smrt-core";
1
+ import { SmrtObject, foreignKey, smrt } from "@happyvertical/smrt-core";
2
2
  import { createCipheriv, createDecipheriv, createHash, hkdfSync, randomBytes } from "node:crypto";
3
3
  //#region \0rolldown/runtime.js
4
4
  var __defProp$1 = Object.defineProperty;
@@ -3064,21 +3064,21 @@ var NostrIdentity = class extends SmrtObject {
3064
3064
  * Find identity by public key
3065
3065
  */
3066
3066
  static async findByPubkey(pubkey, options = {}) {
3067
- const { NostrIdentityCollection } = await Promise.resolve().then(() => NostrIdentityCollection_exports);
3067
+ const { NostrIdentityCollection } = await import("./NostrIdentityCollection-BRYUgt47.js").then((n) => n.n);
3068
3068
  return await (await NostrIdentityCollection.create(options)).findOne({ where: { pubkey } });
3069
3069
  }
3070
3070
  /**
3071
3071
  * Find identity by email
3072
3072
  */
3073
3073
  static async findByEmail(email, options = {}) {
3074
- const { NostrIdentityCollection } = await Promise.resolve().then(() => NostrIdentityCollection_exports);
3074
+ const { NostrIdentityCollection } = await import("./NostrIdentityCollection-BRYUgt47.js").then((n) => n.n);
3075
3075
  return await (await NostrIdentityCollection.create(options)).findOne({ where: { email: email.toLowerCase() } });
3076
3076
  }
3077
3077
  /**
3078
3078
  * Find identity by NIP-05 username
3079
3079
  */
3080
3080
  static async findByNip05(username, options = {}) {
3081
- const { NostrIdentityCollection } = await Promise.resolve().then(() => NostrIdentityCollection_exports);
3081
+ const { NostrIdentityCollection } = await import("./NostrIdentityCollection-BRYUgt47.js").then((n) => n.n);
3082
3082
  return await (await NostrIdentityCollection.create(options)).findOne({ where: { nip05Username: username.toLowerCase() } });
3083
3083
  }
3084
3084
  /**
@@ -3143,125 +3143,6 @@ NostrIdentity = __decorateClass([smrt({
3143
3143
  cli: { include: ["list", "get"] }
3144
3144
  })], NostrIdentity);
3145
3145
  //#endregion
3146
- //#region src/collections/NostrIdentityCollection.ts
3147
- var NostrIdentityCollection_exports = /* @__PURE__ */ __exportAll({ NostrIdentityCollection: () => NostrIdentityCollection });
3148
- var NostrIdentityCollection = class extends SmrtCollection {
3149
- static _itemClass = NostrIdentity;
3150
- /**
3151
- * Find identities for a profile
3152
- */
3153
- async findByProfile(profileId) {
3154
- return await this.list({ where: { profileId } });
3155
- }
3156
- /**
3157
- * Find identity by public key
3158
- */
3159
- async findByPubkey(pubkey) {
3160
- return await this.findOne({ where: { pubkey } });
3161
- }
3162
- /**
3163
- * Find identity by email
3164
- */
3165
- async findByEmail(email) {
3166
- return await this.findOne({ where: { email: email.toLowerCase() } });
3167
- }
3168
- /**
3169
- * Find identity by NIP-05 username
3170
- */
3171
- async findByNip05(username) {
3172
- return await this.findOne({ where: { nip05Username: username.toLowerCase() } });
3173
- }
3174
- /**
3175
- * Link a new Nostr identity to a profile with a new keypair
3176
- * @param profile - The profile to link to
3177
- * @param email - Email address for this identity
3178
- * @param masterSecret - Server master secret for encryption
3179
- * @param nip05Username - Optional NIP-05 username (defaults to email local part)
3180
- */
3181
- async createForProfile(profile, email, masterSecret, nip05Username) {
3182
- const normalizedEmail = email.toLowerCase();
3183
- if (await this.findByEmail(normalizedEmail)) throw new Error(`Nostr identity already exists for email: ${email}`);
3184
- const keypair = generateNostrKeypair();
3185
- const encrypted = encryptPrivkey(keypair.privkey, masterSecret);
3186
- const username = nip05Username?.toLowerCase() || normalizedEmail.split("@")[0];
3187
- const identity = new NostrIdentity({
3188
- ...this.options,
3189
- profileId: profile.id,
3190
- pubkey: keypair.pubkey,
3191
- encryptedPrivkey: encrypted.ciphertext,
3192
- encryptionIv: encrypted.iv,
3193
- encryptionTag: encrypted.tag,
3194
- email: normalizedEmail,
3195
- nip05Username: username
3196
- });
3197
- await identity.initialize();
3198
- await identity.save();
3199
- return identity;
3200
- }
3201
- /**
3202
- * Link an existing Nostr identity (with encrypted keypair) to a profile
3203
- */
3204
- async linkToProfile(profile, nostrData) {
3205
- if (await this.findByPubkey(nostrData.pubkey)) throw new Error("Nostr identity with this public key already exists");
3206
- if (await this.findByEmail(nostrData.email)) throw new Error("Nostr identity with this email already exists");
3207
- const identity = new NostrIdentity({
3208
- ...this.options,
3209
- profileId: profile.id,
3210
- pubkey: nostrData.pubkey,
3211
- encryptedPrivkey: nostrData.encryptedPrivkey,
3212
- encryptionIv: nostrData.encryptionIv,
3213
- encryptionTag: nostrData.encryptionTag,
3214
- email: nostrData.email.toLowerCase(),
3215
- nip05Username: nostrData.nip05Username?.toLowerCase() || nostrData.email.toLowerCase().split("@")[0]
3216
- });
3217
- await identity.initialize();
3218
- await identity.save();
3219
- return identity;
3220
- }
3221
- /**
3222
- * Unlink a Nostr identity from a profile
3223
- */
3224
- async unlinkFromProfile(profileId, pubkey) {
3225
- const identity = await this.findOne({ where: {
3226
- profileId,
3227
- pubkey
3228
- } });
3229
- if (identity) {
3230
- await identity.delete();
3231
- return true;
3232
- }
3233
- return false;
3234
- }
3235
- /**
3236
- * Get NIP-05 response data for the /.well-known/nostr.json endpoint
3237
- * @param username - Optional username filter, returns all if not provided
3238
- */
3239
- async getNip05Response(username) {
3240
- const where = username ? { nip05Username: username.toLowerCase() } : {};
3241
- const identities = await this.list({ where });
3242
- const names = {};
3243
- for (const identity of identities) if (identity.nip05Username && identity.pubkey) names[identity.nip05Username] = identity.pubkey;
3244
- return { names };
3245
- }
3246
- /**
3247
- * Check if a NIP-05 username is available
3248
- */
3249
- async isNip05Available(username) {
3250
- return await this.findByNip05(username) === null;
3251
- }
3252
- /**
3253
- * Update NIP-05 username for an identity
3254
- */
3255
- async updateNip05Username(identity, newUsername) {
3256
- const normalizedUsername = newUsername.toLowerCase();
3257
- const existing = await this.findByNip05(normalizedUsername);
3258
- if (existing && existing.id !== identity.id) throw new Error(`NIP-05 username "${newUsername}" is already taken`);
3259
- identity.nip05Username = normalizedUsername;
3260
- await identity.save();
3261
- return identity;
3262
- }
3263
- };
3264
- //#endregion
3265
- export { pubkeyToNpub as _, computeEventId as a, verifyNostrSignature as b, deriveEncryptionKey as c, getPublicKey as d, isValidPrivkey as f, privkeyToNsec as g, nsecToPrivkey as h, NostrIdentity_exports as i, encryptPrivkey as l, npubToPubkey as m, NostrIdentityCollection_exports as n, createAuthEvent as o, isValidPubkey as p, NostrIdentity as r, decryptPrivkey as s, NostrIdentityCollection as t, generateNostrKeypair as u, signEvent as v, __exportAll as x, verifyAuthEvent as y };
3146
+ export { verifyAuthEvent as _, decryptPrivkey as a, generateNostrKeypair as c, isValidPubkey as d, npubToPubkey as f, signEvent as g, pubkeyToNpub as h, createAuthEvent as i, getPublicKey as l, privkeyToNsec as m, NostrIdentity_exports as n, deriveEncryptionKey as o, nsecToPrivkey as p, computeEventId as r, encryptPrivkey as s, NostrIdentity as t, isValidPrivkey as u, verifyNostrSignature as v, __exportAll as y };
3266
3147
 
3267
- //# sourceMappingURL=NostrIdentityCollection-CG6HGf6c.js.map
3148
+ //# sourceMappingURL=NostrIdentity-DF_a_WZk.js.map