@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.
- package/AGENTS.md +69 -5
- package/README.md +101 -50
- package/dist/chunks/{ApiKey-BrNyaaZN.js → ApiKey-Br9NGBnr.js} +3 -3
- package/dist/chunks/{ApiKey-BrNyaaZN.js.map → ApiKey-Br9NGBnr.js.map} +1 -1
- package/dist/chunks/{ApiKeyCollection-DNhDkS7O.js → ApiKeyCollection-CrTjdMz8.js} +3 -3
- package/dist/chunks/{ApiKeyCollection-DNhDkS7O.js.map → ApiKeyCollection-CrTjdMz8.js.map} +1 -1
- package/dist/chunks/{AuditLogCollection-CnIYU0aK.js → AuditLogCollection-BBIEI6BD.js} +2 -2
- package/dist/chunks/{AuditLogCollection-CnIYU0aK.js.map → AuditLogCollection-BBIEI6BD.js.map} +1 -1
- package/dist/chunks/{NostrIdentityCollection-CG6HGf6c.js → NostrIdentity-DF_a_WZk.js} +6 -125
- package/dist/chunks/NostrIdentity-DF_a_WZk.js.map +1 -0
- package/dist/chunks/NostrIdentityCollection-BRYUgt47.js +124 -0
- package/dist/chunks/NostrIdentityCollection-BRYUgt47.js.map +1 -0
- package/dist/chunks/OidcIdentity-ChkiT6hg.js +111 -0
- package/dist/chunks/OidcIdentity-ChkiT6hg.js.map +1 -0
- package/dist/chunks/OidcIdentityCollection-Ce4yv8p0.js +76 -0
- package/dist/chunks/OidcIdentityCollection-Ce4yv8p0.js.map +1 -0
- package/dist/chunks/{ProfileAssetCollection-DzUwbtTq.js → ProfileAssetCollection-CtlvXbGy.js} +3 -3
- package/dist/chunks/{ProfileAssetCollection-DzUwbtTq.js.map → ProfileAssetCollection-CtlvXbGy.js.map} +1 -1
- package/dist/chunks/ProfileCollection-Bix9yAiy.js +1161 -0
- package/dist/chunks/ProfileCollection-Bix9yAiy.js.map +1 -0
- package/dist/chunks/{ProfileMetadataCollection-CGcO4Iw_.js → ProfileMetadataCollection-loKVv_pj.js} +2 -2
- package/dist/chunks/{ProfileMetadataCollection-CGcO4Iw_.js.map → ProfileMetadataCollection-loKVv_pj.js.map} +1 -1
- package/dist/chunks/{ProfileMetafieldCollection-C8TX9HvN.js → ProfileMetafieldCollection-CACMY1hl.js} +2 -2
- package/dist/chunks/{ProfileMetafieldCollection-C8TX9HvN.js.map → ProfileMetafieldCollection-CACMY1hl.js.map} +1 -1
- package/dist/chunks/{ProfileRelationshipCollection-JnskbZTd.js → ProfileRelationshipCollection-CzQKfvMy.js} +5 -5
- package/dist/chunks/{ProfileRelationshipCollection-JnskbZTd.js.map → ProfileRelationshipCollection-CzQKfvMy.js.map} +1 -1
- package/dist/chunks/{ProfileRelationshipTermCollection-UopP7sja.js → ProfileRelationshipTermCollection-DzmU48P6.js} +2 -2
- package/dist/chunks/{ProfileRelationshipTermCollection-UopP7sja.js.map → ProfileRelationshipTermCollection-DzmU48P6.js.map} +1 -1
- package/dist/chunks/{ProfileRelationshipType-OMBn5cWR.js → ProfileRelationshipType-BjoaCt-u.js} +2 -2
- package/dist/chunks/{ProfileRelationshipType-OMBn5cWR.js.map → ProfileRelationshipType-BjoaCt-u.js.map} +1 -1
- package/dist/chunks/{ProfileRelationshipTypeCollection-CQu08kxF.js → ProfileRelationshipTypeCollection-Bh4xJ4-I.js} +3 -3
- package/dist/chunks/{ProfileRelationshipTypeCollection-CQu08kxF.js.map → ProfileRelationshipTypeCollection-Bh4xJ4-I.js.map} +1 -1
- package/dist/chunks/oidcProvisioningPrimitives-ZuVcDPt-.js +138 -0
- package/dist/chunks/oidcProvisioningPrimitives-ZuVcDPt-.js.map +1 -0
- package/dist/chunks/resolveIdentity-_h_2sGqJ.js +260 -0
- package/dist/chunks/resolveIdentity-_h_2sGqJ.js.map +1 -0
- package/dist/index.d.ts +127 -15
- package/dist/index.js +30 -273
- package/dist/index.js.map +1 -1
- package/dist/internal/oidc-provisioning.d.ts +17 -0
- package/dist/internal/oidc-provisioning.js +13 -0
- package/dist/internal/oidc-provisioning.js.map +1 -0
- package/dist/manifest.json +431 -46
- package/dist/oidc-provisioning.d.ts +83 -0
- package/dist/smrt-knowledge.json +105 -43
- package/dist/types.d.ts +26 -2
- package/dist/utils.d.ts +26 -2
- package/dist/utils.js +4 -4
- package/package.json +10 -6
- package/dist/chunks/NostrIdentityCollection-CG6HGf6c.js.map +0 -1
- package/dist/chunks/OidcIdentityCollection-DiLneB7J.js +0 -158
- package/dist/chunks/OidcIdentityCollection-DiLneB7J.js.map +0 -1
- package/dist/chunks/ProfileCollection-BOMOloTL.js +0 -641
- 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. `
|
|
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 =
|
|
52
|
-
|
|
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
|
-
|
|
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
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
await
|
|
32
|
-
|
|
33
|
-
// OIDC
|
|
34
|
-
const
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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
|
|
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 {
|
|
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-
|
|
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-
|
|
145
|
+
//# sourceMappingURL=ApiKey-Br9NGBnr.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ApiKey-
|
|
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 {
|
|
2
|
-
import { t as ApiKey } from "./ApiKey-
|
|
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-
|
|
83
|
+
//# sourceMappingURL=ApiKeyCollection-CrTjdMz8.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ApiKeyCollection-
|
|
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 {
|
|
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-
|
|
191
|
+
//# sourceMappingURL=AuditLogCollection-BBIEI6BD.js.map
|
package/dist/chunks/{AuditLogCollection-CnIYU0aK.js.map → AuditLogCollection-BBIEI6BD.js.map}
RENAMED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AuditLogCollection-
|
|
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":";;;;;;;;;;;;AAoCO,IAAM,WAAN,cAAuB,WAAW;CAEvC,WAA0B;CAM1B;;;;CAKA,SAAiB;;;;CAKjB,eAAuB;;;;CAKvB,aAAqB;;;;CAKrB,SAAsB;;;;CAKtB,WAAoC,CAAC;CAMrC;CAEA,YAAY,UAA2B,CAAC,GAAG;EACzC,MAAM,OAAO;EACb,IAAI,QAAQ,WAAW,KAAK,YAAY,QAAQ;EAChD,IAAI,QAAQ,QAAQ,KAAK,SAAS,QAAQ;EAC1C,IAAI,QAAQ,cAAc,KAAK,eAAe,QAAQ;EACtD,IAAI,QAAQ,YAAY,KAAK,aAAa,QAAQ;EAClD,IAAI,QAAQ,QAAQ,KAAK,SAAS,QAAQ;EAC1C,IAAI,QAAQ,UAAU,KAAK,WAAW,QAAQ;EAC9C,IAAI,QAAQ,iBAAiB,KAAA,GAC3B,KAAK,eAAe,QAAQ;CAChC;;;;CAKA,MAAM,aAAsC;EAC1C,OAAQ,MAAM,KAAK,WAAW,WAAW;CAC3C;;;;CAKA,MAAM,gBAAyC;EAC7C,IAAI,CAAC,KAAK,cAAc,OAAO;EAC/B,OAAQ,MAAM,KAAK,WAAW,cAAc;CAC9C;;;;CAKA,MAAM,oBAA6C;EACjD,MAAM,aAAa,MAAM,KAAK,cAAc;EAC5C,IAAI,YAAY,OAAO;EACvB,OAAO,MAAM,KAAK,WAAW;CAC/B;;;;CAKA,aAAa,OACX,SAImB;EACnB,MAAM,MAAM,IAAI,SAAS;GACvB,GAAG;GACH,WAAW,QAAQ,QAAQ;GAC3B,cAAc,QAAQ,YAAY;EACpC,CAAC;EACD,MAAM,IAAI,WAAW;EACrB,MAAM,IAAI,KAAK;EACf,OAAO;CACT;AACF;AA7FE,gBAAA,CADC,SAAS,EAAE,UAAU,KAAK,CAAC,CAAA,GADjB,SAEX,WAAA,YAAA,CAAA;AAMA,gBAAA,CADC,WAAW,WAAW,EAAE,UAAU,KAAK,CAAC,CAAA,GAP9B,SAQX,WAAA,aAAA,CAAA;AA+BA,gBAAA,CADC,WAAW,WAAW,EAAE,UAAU,KAAK,CAAC,CAAA,GAtC9B,SAuCX,WAAA,gBAAA,CAAA;AAvCW,WAAN,gBAAA,CAPN,aAAa;CAAE,MAAM;CAAY,uBAAuB;AAAK,CAAC,GAC9D,KAAK;CACJ,WAAW;CACX,KAAK,EAAE,SAAS,CAAC,QAAQ,KAAK,EAAE;CAChC,KAAK,EAAE,SAAS,CAAC,QAAQ,KAAK,EAAE;CAChC,KAAK,EAAE,SAAS,CAAC,MAAM,EAAE;AAC3B,CAAC,CAAA,GACY,QAAA;;;;AC5BN,IAAM,qBAAN,cAAiC,eAAyB;CAC/D,OAAgB,aAAa;;;;CAK7B,MAAM,cAAc,WAAwC;EAC1D,OAAO,MAAM,KAAK,KAAK;GACrB,OAAO,EAAE,UAAU;GACnB,SAAS;EACX,CAAC;CACH;;;;CAKA,MAAM,eACJ,cACA,YACqB;EACrB,OAAO,MAAM,KAAK,KAAK;GACrB,OAAO;IAAE;IAAc;GAAW;GAClC,SAAS;EACX,CAAC;CACH;;;;CAKA,MAAM,aAAa,QAAqC;EACtD,OAAO,MAAM,KAAK,KAAK;GACrB,OAAO,EAAE,OAAO;GAChB,SAAS;EACX,CAAC;CACH;;;;CAKA,MAAM,aAAa,QAA0C;EAC3D,OAAO,MAAM,KAAK,KAAK;GACrB,OAAO,EAAE,OAAO;GAChB,SAAS;EACX,CAAC;CACH;;;;CAKA,MAAM,eAAe,WAAwC;EAC3D,OAAO,MAAM,KAAK,KAAK;GACrB,OAAO,EAAE,cAAc,UAAU;GACjC,SAAS;EACX,CAAC;CACH;;;;CAKA,MAAM,OAAO,SAQS;EACpB,MAAM,MAAM,IAAI,SAAS;GACvB,GAAG,KAAK;GACR,WAAW,QAAQ,QAAQ;GAC3B,QAAQ,QAAQ;GAChB,cAAc,QAAQ;GACtB,YAAY,QAAQ;GACpB,QAAQ,QAAQ,UAAU;GAC1B,UAAU,QAAQ,YAAY,CAAC;GAC/B,cAAc,QAAQ,YAAY;EACpC,CAAC;EACD,MAAM,IAAI,WAAW;EACrB,MAAM,IAAI,KAAK;EACf,OAAO;CACT;;;;CAKA,MAAM,kBACJ,WACA,QAAgB,IACK;EACrB,OAAO,MAAM,KAAK,KAAK;GACrB,OAAO,EAAE,UAAU;GACnB,SAAS;GACT;EACF,CAAC;CACH;;;;CAKA,MAAM,oBACJ,cACA,YACqB;EACrB,OAAO,MAAM,KAAK,KAAK;GACrB,OAAO;IAAE;IAAc;GAAW;GAClC,SAAS;EACX,CAAC;CACH;AACF"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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=
|
|
3148
|
+
//# sourceMappingURL=NostrIdentity-DF_a_WZk.js.map
|