@happyvertical/smrt-profiles 0.30.0

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 (42) hide show
  1. package/AGENTS.md +53 -0
  2. package/CLAUDE.md +1 -0
  3. package/LICENSE +7 -0
  4. package/README.md +176 -0
  5. package/dist/chunks/ApiKey-B2LKEaP8.js +143 -0
  6. package/dist/chunks/ApiKey-B2LKEaP8.js.map +1 -0
  7. package/dist/chunks/ApiKeyCollection-B6Op817e.js +91 -0
  8. package/dist/chunks/ApiKeyCollection-B6Op817e.js.map +1 -0
  9. package/dist/chunks/AuditLogCollection-BYqCj0uE.js +195 -0
  10. package/dist/chunks/AuditLogCollection-BYqCj0uE.js.map +1 -0
  11. package/dist/chunks/NostrIdentityCollection-DadQBHWy.js +3065 -0
  12. package/dist/chunks/NostrIdentityCollection-DadQBHWy.js.map +1 -0
  13. package/dist/chunks/ProfileAssetCollection-D_tk1kKG.js +122 -0
  14. package/dist/chunks/ProfileAssetCollection-D_tk1kKG.js.map +1 -0
  15. package/dist/chunks/ProfileCollection-DU6wUJTO.js +782 -0
  16. package/dist/chunks/ProfileCollection-DU6wUJTO.js.map +1 -0
  17. package/dist/chunks/ProfileMetadataCollection-DEhmljMY.js +120 -0
  18. package/dist/chunks/ProfileMetadataCollection-DEhmljMY.js.map +1 -0
  19. package/dist/chunks/ProfileMetafieldCollection-DMKhSHXX.js +184 -0
  20. package/dist/chunks/ProfileMetafieldCollection-DMKhSHXX.js.map +1 -0
  21. package/dist/chunks/ProfileRelationshipCollection-C0IM8UQR.js +177 -0
  22. package/dist/chunks/ProfileRelationshipCollection-C0IM8UQR.js.map +1 -0
  23. package/dist/chunks/ProfileRelationshipTermCollection-CXem_qT-.js +117 -0
  24. package/dist/chunks/ProfileRelationshipTermCollection-CXem_qT-.js.map +1 -0
  25. package/dist/chunks/ProfileRelationshipType-BXBLldea.js +103 -0
  26. package/dist/chunks/ProfileRelationshipType-BXBLldea.js.map +1 -0
  27. package/dist/chunks/ProfileRelationshipTypeCollection-CF8YvLTV.js +48 -0
  28. package/dist/chunks/ProfileRelationshipTypeCollection-CF8YvLTV.js.map +1 -0
  29. package/dist/chunks/index-jFtOWsAV.js +1014 -0
  30. package/dist/chunks/index-jFtOWsAV.js.map +1 -0
  31. package/dist/index.d.ts +1848 -0
  32. package/dist/index.js +70 -0
  33. package/dist/index.js.map +1 -0
  34. package/dist/manifest.json +11829 -0
  35. package/dist/smrt-knowledge.json +3846 -0
  36. package/dist/types.d.ts +41 -0
  37. package/dist/types.js +2 -0
  38. package/dist/types.js.map +1 -0
  39. package/dist/utils.d.ts +61 -0
  40. package/dist/utils.js +49 -0
  41. package/dist/utils.js.map +1 -0
  42. package/package.json +75 -0
package/AGENTS.md ADDED
@@ -0,0 +1,53 @@
1
+ # @happyvertical/smrt-profiles
2
+
3
+ Central identity system with multi-auth, relationships, controlled metadata, and audit logging.
4
+
5
+ ## Models
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.
8
+ - **ProfileAsset**: dedicated owned-asset join in `profile_assets` with `relationship` and `sortOrder`.
9
+ - **ProfileRelationship**: bidirectional — creating one auto-creates reciprocal inverse. `contextProfileId` for tertiary relationships. `ProfileRelationshipTerm` tracks start/end dates.
10
+ - **ProfileMetafield**: controlled vocabulary with `validationSchema`. **ProfileMetadata**: per-profile values linked to metafields.
11
+ - **AuditLog**: action, resourceType/Id, `source` (web/cli/ci/webhook/mcp), `onBehalfOfId` for CI pass-through identity. `allowSuperAdminBypass: true`.
12
+
13
+ ## Auth Methods
14
+
15
+ | Model | Pattern |
16
+ |-------|---------|
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. |
19
+ | ApiKey | SHA-256 hashed. **Plaintext returned once only** on `generate()`. `keyPrefix` for identification. Scope-based with expiry. |
20
+ | MagicLinkToken | One-time token with expiry for passwordless auth. |
21
+
22
+ ## Identity Resolution
23
+
24
+ Auth helpers in `src/auth/` build profiles from external identity claims:
25
+
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.
28
+ - `createProfileFromNostr(email, nostrData)` — creates `Profile` + `NostrIdentity` for Nostr-authenticated users.
29
+
30
+ ## Key Methods
31
+
32
+ - `Profile.getAssets()` / `addAsset()` / `removeAsset()` and the matching `ProfileCollection` wrappers — canonical owned asset helpers backed by `profile_assets`.
33
+ - `Profile.addMetadata(metafieldSlug, value)` / `Profile.getMetadata()` — validates against metafield schema. `ProfileCollection.batchGetMetadata()` / `batchUpdateMetadata()` for bulk reads/writes.
34
+ - `Profile.getRelationships({ direction: 'from'|'to'|'all' })` — direction matters.
35
+ - `Profile.getRelationshipsFrom()` / `getRelationshipsTo()` — R10-generated `@oneToMany` accessors. ProfileRelationship has two FKs back to Profile, so each `@oneToMany` annotates its inverse explicitly (`{ foreignKey: 'fromProfileId' }` / `'toProfileId'`). Return raw `ProfileRelationship[]`; use `getRelationships()` for slug/direction filtering.
36
+ - AI: `generateBio()` (uses `smrtProfiles.profile.generateBio` prompt via `@happyvertical/smrt-prompts`), `matches(criteria)` (delegates to `is()`).
37
+
38
+ ## Prompt Registry
39
+
40
+ `generateBio()` is registered with `@happyvertical/smrt-prompts` so tenants can override template/model/params at runtime:
41
+
42
+ ```typescript
43
+ import { smrtProfilesGenerateBioPrompt } from '@happyvertical/smrt-profiles';
44
+ // key: 'smrtProfiles.profile.generateBio'
45
+ ```
46
+
47
+ ## Gotchas
48
+
49
+ - **SERVER_MASTER_SECRET required** for Nostr private key decryption — centralized key management
50
+ - **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
53
+ - **Optional tenancy** on Profile; AuditLog allows super-admin bypass
package/CLAUDE.md ADDED
@@ -0,0 +1 @@
1
+ @AGENTS.md
package/LICENSE ADDED
@@ -0,0 +1,7 @@
1
+ Copyright <2025> <Happy Vertical Corporation>
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4
+
5
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6
+
7
+ THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,176 @@
1
+ # @happyvertical/smrt-profiles
2
+
3
+ Central identity system with multi-auth (Nostr/OIDC/API keys/magic links), relationships, controlled metadata, and audit logging.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ pnpm add @happyvertical/smrt-profiles
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ ```typescript
14
+ import {
15
+ Profile,
16
+ ProfileCollection,
17
+ OidcIdentity,
18
+ OidcIdentityCollection,
19
+ ApiKey,
20
+ ApiKeyCollection,
21
+ NostrIdentity,
22
+ generateNostrKeypair,
23
+ createProfileFromOidc,
24
+ } 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' });
62
+ ```
63
+
64
+ ### Owned assets
65
+
66
+ ```typescript
67
+ import { AssetCollection } from '@happyvertical/smrt-assets';
68
+
69
+ const profiles = await ProfileCollection.create();
70
+ const assets = await AssetCollection.create();
71
+
72
+ const headshot = await assets.create({
73
+ name: 'alice-headshot.jpg',
74
+ sourceUri: 'file:///tmp/alice-headshot.jpg',
75
+ mimeType: 'image/jpeg',
76
+ });
77
+
78
+ await profile.addAsset(headshot, 'avatar');
79
+ await profiles.addAsset(profile.id!, headshot, 'gallery', 1);
80
+
81
+ const avatarAssets = await profile.getAssets('avatar');
82
+ const galleryAssets = await profiles.getAssets(profile.id!, 'gallery');
83
+ ```
84
+
85
+ ## API
86
+
87
+ ### Models
88
+
89
+ | Export | Description |
90
+ |--------|------------|
91
+ | `Profile` | Core identity (STI base for Bot/Organization/Person) |
92
+ | `Bot` | STI subclass for automated agents |
93
+ | `Organization` | STI subclass for companies/groups |
94
+ | `Person` | STI subclass for individuals |
95
+ | `ProfileType` | Profile classification lookup table |
96
+ | `ProfileMetadata` | Per-profile metadata values |
97
+ | `ProfileMetafield` | Controlled vocabulary with validation schema |
98
+ | `ProfileRelationship` | Directional link between two profiles |
99
+ | `ProfileRelationshipType` | Relationship classification with reciprocal flag |
100
+ | `ProfileRelationshipTerm` | Time-bounded relationship periods |
101
+ | `ProfileAsset` | Dedicated owned-asset join stored in `profile_assets` with `relationship` and `sortOrder` |
102
+
103
+ ### Auth Models
104
+
105
+ | Export | Description |
106
+ |--------|------------|
107
+ | `OidcIdentity` | OIDC provider identity (issuer + subject) |
108
+ | `NostrIdentity` | Nostr keypair with AES-256-GCM encryption |
109
+ | `ApiKey` | SHA-256 hashed API key with scope and expiry |
110
+ | `MagicLinkToken` | One-time passwordless auth token |
111
+ | `AuditLog` | Action/resource audit trail with source tracking |
112
+
113
+ ### Collections
114
+
115
+ | Export | Description |
116
+ |--------|------------|
117
+ | `ProfileCollection` | CRUD and query for profiles |
118
+ | `ProfileAssetCollection` | Direct access to `profile_assets` rows plus asset helper wrappers |
119
+ | `ProfileTypeCollection` | Profile type management |
120
+ | `ProfileMetadataCollection` | Metadata value operations |
121
+ | `ProfileMetafieldCollection` | Metafield vocabulary management |
122
+ | `ProfileRelationshipCollection` | Relationship queries |
123
+ | `ProfileRelationshipTypeCollection` | Relationship type management |
124
+ | `ProfileRelationshipTermCollection` | Term period management |
125
+ | `ApiKeyCollection` | API key lookup and management |
126
+ | `AuditLogCollection` | Audit log queries |
127
+ | `MagicLinkTokenCollection` | Magic link token operations |
128
+ | `NostrIdentityCollection` | Nostr identity lookup (includes NIP-05) |
129
+ | `OidcIdentityCollection` | OIDC identity lookup |
130
+
131
+ `Profile` and `ProfileCollection` both expose `getAssets()`, `addAsset()`, and
132
+ `removeAsset()` helpers backed by `profile_assets`. Typical relationships are
133
+ `avatar`, `gallery`, and `attachment`.
134
+
135
+ ### Auth Functions
136
+
137
+ | Export | Description |
138
+ |--------|------------|
139
+ | `resolveIdentity` | Resolve profile from any auth method |
140
+ | `createProfileFromOidc` | Create profile + OIDC identity in one call |
141
+ | `createProfileFromNostr` | Create profile + Nostr identity in one call |
142
+ | `createAuthEvent` | Create a Nostr auth event |
143
+ | `verifyAuthEvent` | Verify a Nostr auth event signature |
144
+ | `createMagicLinkService` | Factory for magic link auth service |
145
+ | `createNip05Handler` | Factory for NIP-05 address handler |
146
+
147
+ ### Nostr Crypto
148
+
149
+ | Export | Description |
150
+ |--------|------------|
151
+ | `generateNostrKeypair` | Generate new Nostr keypair |
152
+ | `encryptPrivkey` / `decryptPrivkey` | AES-256-GCM key encryption |
153
+ | `deriveEncryptionKey` | Derive encryption key from master secret |
154
+ | `getPublicKey` | Derive pubkey from privkey |
155
+ | `signEvent` / `computeEventId` | Nostr event signing |
156
+ | `verifyNostrSignature` | Verify Nostr signature |
157
+ | `pubkeyToNpub` / `npubToPubkey` | Bech32 pubkey conversion |
158
+ | `privkeyToNsec` / `nsecToPrivkey` | Bech32 privkey conversion |
159
+ | `isValidPubkey` / `isValidPrivkey` | Key validation |
160
+ | `parseNip05Identifier` / `isValidNip05Identifier` | NIP-05 parsing |
161
+
162
+ ### Key Types
163
+
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`
165
+
166
+ ## Dependencies
167
+
168
+ - `@happyvertical/smrt-core` -- ORM base classes
169
+ - `@happyvertical/ai` -- AI client (SDK)
170
+ - `@happyvertical/sql` -- Database operations (SDK)
171
+ - `@happyvertical/files` -- Filesystem utilities (SDK)
172
+ - `@happyvertical/logger` -- Structured logging (SDK)
173
+ - `@happyvertical/utils` -- Shared utilities (SDK)
174
+ - `@noble/curves` -- Nostr cryptography
175
+ - `bech32` -- Bech32 encoding for Nostr keys
176
+ - Peer: `@happyvertical/smrt-tenancy`
@@ -0,0 +1,143 @@
1
+ import { createHash, randomBytes } from "node:crypto";
2
+ import { foreignKey, smrt, SmrtObject } from "@happyvertical/smrt-core";
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __decorateClass = (decorators, target, key, kind) => {
6
+ var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc(target, key) : target;
7
+ for (var i = decorators.length - 1, decorator; i >= 0; i--)
8
+ if (decorator = decorators[i])
9
+ result = (kind ? decorator(target, key, result) : decorator(result)) || result;
10
+ if (kind && result) __defProp(target, key, result);
11
+ return result;
12
+ };
13
+ let ApiKey = class extends SmrtObject {
14
+ profileId;
15
+ /**
16
+ * SHA-256 hash of the API key
17
+ */
18
+ keyHash = "";
19
+ /**
20
+ * First 8 characters of the key for identification (e.g., "sk_live_a1b2...")
21
+ */
22
+ keyPrefix = "";
23
+ /**
24
+ * Human-readable name for the key (e.g., "CI Bot", "Local CLI")
25
+ */
26
+ name = "";
27
+ /**
28
+ * Scopes/permissions for this key
29
+ */
30
+ scopes = [];
31
+ /**
32
+ * Last time this key was used
33
+ */
34
+ lastUsedAt = null;
35
+ /**
36
+ * When this key expires (null = never)
37
+ */
38
+ expiresAt = null;
39
+ /**
40
+ * When this key was revoked (null = active)
41
+ */
42
+ revokedAt = null;
43
+ constructor(options = {}) {
44
+ super(options);
45
+ if (options.profileId) this.profileId = options.profileId;
46
+ if (options.name) this.name = options.name;
47
+ if (options.scopes) this.scopes = options.scopes;
48
+ if (options.expiresAt !== void 0) this.expiresAt = options.expiresAt;
49
+ }
50
+ /**
51
+ * Get the linked Profile
52
+ */
53
+ async getProfile() {
54
+ return await this.getRelated("profileId");
55
+ }
56
+ /**
57
+ * Check if this key is valid (not expired, not revoked)
58
+ */
59
+ isValid() {
60
+ if (this.revokedAt) return false;
61
+ if (this.expiresAt && this.expiresAt < /* @__PURE__ */ new Date()) return false;
62
+ return true;
63
+ }
64
+ /**
65
+ * Check if this key has a specific scope
66
+ */
67
+ hasScope(scope) {
68
+ return this.scopes.includes(scope) || this.scopes.includes("*");
69
+ }
70
+ /**
71
+ * Revoke this key
72
+ */
73
+ async revoke() {
74
+ this.revokedAt = /* @__PURE__ */ new Date();
75
+ await this.save();
76
+ }
77
+ /**
78
+ * Record usage of this key
79
+ */
80
+ async recordUsage() {
81
+ this.lastUsedAt = /* @__PURE__ */ new Date();
82
+ await this.save();
83
+ }
84
+ /**
85
+ * Hash an API key
86
+ */
87
+ static hashKey(key) {
88
+ return createHash("sha256").update(key).digest("hex");
89
+ }
90
+ /**
91
+ * Generate a new API key for a profile
92
+ */
93
+ static async generate(profile, options) {
94
+ const randomPart = randomBytes(32).toString("hex");
95
+ const key = `sk_live_${randomPart}`;
96
+ const keyPrefix = key.substring(0, 16);
97
+ const keyHash = ApiKey.hashKey(key);
98
+ const apiKey = new ApiKey({
99
+ db: options.db || profile.options?.db,
100
+ profileId: profile.id,
101
+ name: options.name,
102
+ scopes: options.scopes || ["*"],
103
+ expiresAt: options.expiresAt ?? null
104
+ });
105
+ apiKey.keyHash = keyHash;
106
+ apiKey.keyPrefix = keyPrefix;
107
+ await apiKey.initialize();
108
+ await apiKey.save();
109
+ return { key, apiKey };
110
+ }
111
+ /**
112
+ * Verify an API key and return the ApiKey record if valid
113
+ */
114
+ static async verify(key, options = {}) {
115
+ const keyHash = ApiKey.hashKey(key);
116
+ const { ApiKeyCollection } = await import("./ApiKeyCollection-B6Op817e.js");
117
+ const collection = await ApiKeyCollection.create(options);
118
+ const apiKey = await collection.findOne({
119
+ where: { keyHash }
120
+ });
121
+ if (!apiKey) return null;
122
+ if (!apiKey.isValid()) return null;
123
+ await apiKey.recordUsage();
124
+ return apiKey;
125
+ }
126
+ };
127
+ __decorateClass([
128
+ foreignKey("Profile", { required: true })
129
+ ], ApiKey.prototype, "profileId", 2);
130
+ ApiKey = __decorateClass([
131
+ smrt({
132
+ tableName: "api_keys",
133
+ api: { include: ["list", "get"] },
134
+ mcp: { include: ["list", "get"] },
135
+ // create/revoke are admin operations invoked in-process via the CLI; only
136
+ // read-only list/get are exposed over HTTP.
137
+ cli: { include: ["list", "get", "create", "revoke"], skipApiCheck: true }
138
+ })
139
+ ], ApiKey);
140
+ export {
141
+ ApiKey
142
+ };
143
+ //# sourceMappingURL=ApiKey-B2LKEaP8.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ApiKey-B2LKEaP8.js","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 as any).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"],"names":[],"mappings":"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"}
@@ -0,0 +1,91 @@
1
+ import { SmrtCollection } from "@happyvertical/smrt-core";
2
+ import { ApiKey } from "./ApiKey-B2LKEaP8.js";
3
+ class ApiKeyCollection extends SmrtCollection {
4
+ static _itemClass = ApiKey;
5
+ /**
6
+ * Find all keys for a profile
7
+ */
8
+ async findByProfile(profileId) {
9
+ return await this.list({
10
+ where: { profileId }
11
+ });
12
+ }
13
+ /**
14
+ * Find active (non-revoked, non-expired) keys for a profile
15
+ */
16
+ async findActiveByProfile(profileId) {
17
+ const keys = await this.findByProfile(profileId);
18
+ return keys.filter((key) => key.isValid());
19
+ }
20
+ /**
21
+ * Find key by hash
22
+ */
23
+ async findByHash(keyHash) {
24
+ return await this.findOne({
25
+ where: { keyHash }
26
+ });
27
+ }
28
+ /**
29
+ * Verify an API key string and return the record if valid
30
+ */
31
+ async verify(key) {
32
+ const keyHash = ApiKey.hashKey(key);
33
+ const apiKey = await this.findByHash(keyHash);
34
+ if (!apiKey) return null;
35
+ if (!apiKey.isValid()) return null;
36
+ await apiKey.recordUsage();
37
+ return apiKey;
38
+ }
39
+ /**
40
+ * Generate a new key for a profile
41
+ */
42
+ async generateForProfile(profile, options) {
43
+ return await ApiKey.generate(profile, {
44
+ ...options,
45
+ db: this.options.db
46
+ });
47
+ }
48
+ /**
49
+ * Revoke all keys for a profile
50
+ */
51
+ async revokeAllForProfile(profileId) {
52
+ const keys = await this.findActiveByProfile(profileId);
53
+ for (const key of keys) {
54
+ await key.revoke();
55
+ }
56
+ return keys.length;
57
+ }
58
+ /**
59
+ * Revoke a specific key by prefix
60
+ */
61
+ async revokeByPrefix(keyPrefix) {
62
+ const key = await this.findOne({
63
+ where: { keyPrefix }
64
+ });
65
+ if (key && key.isValid()) {
66
+ await key.revoke();
67
+ return true;
68
+ }
69
+ return false;
70
+ }
71
+ /**
72
+ * Clean up expired keys (soft delete by setting revokedAt)
73
+ */
74
+ async cleanupExpired() {
75
+ const now = /* @__PURE__ */ new Date();
76
+ const allKeys = await this.list({});
77
+ let cleaned = 0;
78
+ for (const key of allKeys) {
79
+ if (key.expiresAt && key.expiresAt < now && !key.revokedAt) {
80
+ key.revokedAt = now;
81
+ await key.save();
82
+ cleaned++;
83
+ }
84
+ }
85
+ return cleaned;
86
+ }
87
+ }
88
+ export {
89
+ ApiKeyCollection
90
+ };
91
+ //# sourceMappingURL=ApiKeyCollection-B6Op817e.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ApiKeyCollection-B6Op817e.js","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 && 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"],"names":[],"mappings":";;AAQO,MAAM,yBAAyB,eAAuB;AAAA,EAC3D,OAAgB,aAAa;AAAA;AAAA;AAAA;AAAA,EAK7B,MAAM,cAAc,WAAsC;AACxD,WAAO,MAAM,KAAK,KAAK;AAAA,MACrB,OAAO,EAAE,UAAA;AAAA,IAAU,CACpB;AAAA,EACH;AAAA;AAAA;AAAA;AAAA,EAKA,MAAM,oBAAoB,WAAsC;AAC9D,UAAM,OAAO,MAAM,KAAK,cAAc,SAAS;AAC/C,WAAO,KAAK,OAAO,CAAC,QAAQ,IAAI,SAAS;AAAA,EAC3C;AAAA;AAAA;AAAA;AAAA,EAKA,MAAM,WAAW,SAAyC;AACxD,WAAO,MAAM,KAAK,QAAQ;AAAA,MACxB,OAAO,EAAE,QAAA;AAAA,IAAQ,CAClB;AAAA,EACH;AAAA;AAAA;AAAA;AAAA,EAKA,MAAM,OAAO,KAAqC;AAChD,UAAM,UAAU,OAAO,QAAQ,GAAG;AAClC,UAAM,SAAS,MAAM,KAAK,WAAW,OAAO;AAE5C,QAAI,CAAC,OAAQ,QAAO;AACpB,QAAI,CAAC,OAAO,QAAA,EAAW,QAAO;AAG9B,UAAM,OAAO,YAAA;AAEb,WAAO;AAAA,EACT;AAAA;AAAA;AAAA;AAAA,EAKA,MAAM,mBACJ,SACA,SAK4B;AAC5B,WAAO,MAAM,OAAO,SAAS,SAAS;AAAA,MACpC,GAAG;AAAA,MACH,IAAI,KAAK,QAAQ;AAAA,IAAA,CAClB;AAAA,EACH;AAAA;AAAA;AAAA;AAAA,EAKA,MAAM,oBAAoB,WAAoC;AAC5D,UAAM,OAAO,MAAM,KAAK,oBAAoB,SAAS;AACrD,eAAW,OAAO,MAAM;AACtB,YAAM,IAAI,OAAA;AAAA,IACZ;AACA,WAAO,KAAK;AAAA,EACd;AAAA;AAAA;AAAA;AAAA,EAKA,MAAM,eAAe,WAAqC;AACxD,UAAM,MAAM,MAAM,KAAK,QAAQ;AAAA,MAC7B,OAAO,EAAE,UAAA;AAAA,IAAU,CACpB;AAED,QAAI,OAAO,IAAI,WAAW;AACxB,YAAM,IAAI,OAAA;AACV,aAAO;AAAA,IACT;AACA,WAAO;AAAA,EACT;AAAA;AAAA;AAAA;AAAA,EAKA,MAAM,iBAAkC;AACtC,UAAM,0BAAU,KAAA;AAChB,UAAM,UAAU,MAAM,KAAK,KAAK,CAAA,CAAE;AAClC,QAAI,UAAU;AAEd,eAAW,OAAO,SAAS;AACzB,UAAI,IAAI,aAAa,IAAI,YAAY,OAAO,CAAC,IAAI,WAAW;AAC1D,YAAI,YAAY;AAChB,cAAM,IAAI,KAAA;AACV;AAAA,MACF;AAAA,IACF;AAEA,WAAO;AAAA,EACT;AACF;"}