alepha 0.20.4 → 0.20.6
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/dist/api/audits/index.d.ts +391 -359
- package/dist/api/audits/index.d.ts.map +1 -1
- package/dist/api/audits/index.js +23 -1
- package/dist/api/audits/index.js.map +1 -1
- package/dist/api/files/index.d.ts +18 -0
- package/dist/api/files/index.d.ts.map +1 -1
- package/dist/api/files/index.js +51 -0
- package/dist/api/files/index.js.map +1 -1
- package/dist/api/jobs/index.browser.js +33 -14
- package/dist/api/jobs/index.browser.js.map +1 -1
- package/dist/api/jobs/index.d.ts +452 -155
- package/dist/api/jobs/index.d.ts.map +1 -1
- package/dist/api/jobs/index.js +474 -159
- package/dist/api/jobs/index.js.map +1 -1
- package/dist/api/keys/index.d.ts +32 -4
- package/dist/api/keys/index.d.ts.map +1 -1
- package/dist/api/keys/index.js +53 -0
- package/dist/api/keys/index.js.map +1 -1
- package/dist/api/notifications/index.d.ts +29 -1
- package/dist/api/notifications/index.d.ts.map +1 -1
- package/dist/api/notifications/index.js +55 -13
- package/dist/api/notifications/index.js.map +1 -1
- package/dist/api/organizations/index.js.map +1 -1
- package/dist/api/parameters/index.d.ts +15 -0
- package/dist/api/parameters/index.d.ts.map +1 -1
- package/dist/api/parameters/index.js +37 -0
- package/dist/api/parameters/index.js.map +1 -1
- package/dist/api/payments/index.js.map +1 -1
- package/dist/api/users/index.d.ts +150 -9
- package/dist/api/users/index.d.ts.map +1 -1
- package/dist/api/users/index.js +237 -28
- package/dist/api/users/index.js.map +1 -1
- package/dist/api/verifications/index.d.ts +3 -3
- package/dist/api/verifications/index.js.map +1 -1
- package/dist/batch/index.js.map +1 -1
- package/dist/bin/index.js +0 -0
- package/dist/bucket/index.d.ts +18 -0
- package/dist/bucket/index.d.ts.map +1 -1
- package/dist/bucket/index.js +47 -0
- package/dist/bucket/index.js.map +1 -1
- package/dist/bucket/index.workerd.js +24 -0
- package/dist/bucket/index.workerd.js.map +1 -1
- package/dist/cache/core/index.d.ts +20 -3
- package/dist/cache/core/index.d.ts.map +1 -1
- package/dist/cache/core/index.js.map +1 -1
- package/dist/cache/core/index.workerd.js.map +1 -1
- package/dist/cache/database/index.d.ts +155 -0
- package/dist/cache/database/index.d.ts.map +1 -0
- package/dist/cache/database/index.js +266 -0
- package/dist/cache/database/index.js.map +1 -0
- package/dist/cache/redis/index.js.map +1 -1
- package/dist/captcha/index.js.map +1 -1
- package/dist/cli/config/index.js.map +1 -1
- package/dist/cli/core/index.d.ts +35 -5
- package/dist/cli/core/index.d.ts.map +1 -1
- package/dist/cli/core/index.js +85 -6
- package/dist/cli/core/index.js.map +1 -1
- package/dist/cli/devtools/index.js.map +1 -1
- package/dist/cli/platform/index.js +1 -1
- package/dist/cli/platform/index.js.map +1 -1
- package/dist/cli/vendor/index.js.map +1 -1
- package/dist/command/index.js.map +1 -1
- package/dist/core/index.browser.js.map +1 -1
- package/dist/core/index.js.map +1 -1
- package/dist/core/index.native.js.map +1 -1
- package/dist/core/index.workerd.js.map +1 -1
- package/dist/crypto/index.browser.js.map +1 -1
- package/dist/crypto/index.js.map +1 -1
- package/dist/datetime/index.js.map +1 -1
- package/dist/email/brevo/index.js.map +1 -1
- package/dist/email/core/index.js.map +1 -1
- package/dist/email/core/index.workerd.js.map +1 -1
- package/dist/email/smtp/index.js.map +1 -1
- package/dist/fake/index.js.map +1 -1
- package/dist/lock/core/index.js.map +1 -1
- package/dist/lock/redis/index.js.map +1 -1
- package/dist/logger/index.js.map +1 -1
- package/dist/mcp/index.js.map +1 -1
- package/dist/orm/core/index.browser.js.map +1 -1
- package/dist/orm/core/index.bun.js.map +1 -1
- package/dist/orm/core/index.js.map +1 -1
- package/dist/orm/postgres/index.bun.js.map +1 -1
- package/dist/orm/postgres/index.js.map +1 -1
- package/dist/queue/core/index.js.map +1 -1
- package/dist/queue/core/index.workerd.js.map +1 -1
- package/dist/queue/redis/index.js.map +1 -1
- package/dist/react/auth/index.browser.js.map +1 -1
- package/dist/react/auth/index.js.map +1 -1
- package/dist/react/core/index.js.map +1 -1
- package/dist/react/form/index.js +2 -0
- package/dist/react/form/index.js.map +1 -1
- package/dist/react/head/index.browser.js.map +1 -1
- package/dist/react/head/index.js.map +1 -1
- package/dist/react/i18n/index.js.map +1 -1
- package/dist/react/intro/index.js.map +1 -1
- package/dist/react/router/index.browser.js.map +1 -1
- package/dist/react/router/index.js.map +1 -1
- package/dist/react/testing/index.js.map +1 -1
- package/dist/react/ui/index.js.map +1 -1
- package/dist/react/websocket/index.js.map +1 -1
- package/dist/redis/index.bun.js.map +1 -1
- package/dist/redis/index.js.map +1 -1
- package/dist/retry/index.js.map +1 -1
- package/dist/router/index.js.map +1 -1
- package/dist/scheduler/index.d.ts +22 -0
- package/dist/scheduler/index.d.ts.map +1 -1
- package/dist/scheduler/index.js +12 -0
- package/dist/scheduler/index.js.map +1 -1
- package/dist/scheduler/index.workerd.js +12 -0
- package/dist/scheduler/index.workerd.js.map +1 -1
- package/dist/security/index.browser.js.map +1 -1
- package/dist/security/index.js.map +1 -1
- package/dist/server/auth/index.js.map +1 -1
- package/dist/server/cookies/index.browser.js.map +1 -1
- package/dist/server/cookies/index.js.map +1 -1
- package/dist/server/core/index.browser.js.map +1 -1
- package/dist/server/core/index.js.map +1 -1
- package/dist/server/cors/index.js.map +1 -1
- package/dist/server/etag/index.js.map +1 -1
- package/dist/server/health/index.js.map +1 -1
- package/dist/server/links/index.browser.js.map +1 -1
- package/dist/server/links/index.js.map +1 -1
- package/dist/server/metrics/index.js.map +1 -1
- package/dist/server/proxy/index.js.map +1 -1
- package/dist/server/rate-limit/index.js.map +1 -1
- package/dist/server/static/index.js.map +1 -1
- package/dist/server/swagger/index.js.map +1 -1
- package/dist/sms/index.js.map +1 -1
- package/dist/system/index.browser.js.map +1 -1
- package/dist/system/index.js.map +1 -1
- package/dist/system/index.workerd.js.map +1 -1
- package/dist/topic/core/index.js.map +1 -1
- package/dist/topic/redis/index.js.map +1 -1
- package/dist/websocket/index.browser.js +4 -0
- package/dist/websocket/index.browser.js.map +1 -1
- package/dist/websocket/index.js +10 -0
- package/dist/websocket/index.js.map +1 -1
- package/package.json +282 -272
- package/src/api/audits/controllers/AdminAuditController.ts +29 -0
- package/src/api/files/controllers/FileController.ts +24 -0
- package/src/api/files/services/FileService.ts +41 -0
- package/src/api/jobs/__tests__/$job.spec.ts +427 -2
- package/src/api/jobs/entities/jobExecutionEntity.ts +3 -3
- package/src/api/jobs/index.ts +47 -10
- package/src/api/jobs/primitives/$job.ts +22 -9
- package/src/api/jobs/providers/DirectJobDispatcher.ts +71 -0
- package/src/api/jobs/providers/JobDispatcher.ts +49 -0
- package/src/api/jobs/providers/JobProvider.ts +365 -142
- package/src/api/jobs/providers/JobQueueProvider.ts +43 -18
- package/src/api/jobs/schemas/jobConfigAtom.ts +4 -3
- package/src/api/jobs/schemas/jobExecutionResourceSchema.ts +11 -0
- package/src/api/jobs/schemas/jobRegistrationSchema.ts +4 -2
- package/src/api/jobs/services/JobService.ts +21 -11
- package/src/api/keys/controllers/AdminApiKeyController.ts +23 -0
- package/src/api/keys/services/ApiKeyService.ts +42 -0
- package/src/api/notifications/__tests__/AlephaApiNotifications.spec.ts +63 -0
- package/src/api/notifications/controllers/AdminNotificationController.ts +48 -1
- package/src/api/notifications/index.ts +13 -3
- package/src/api/notifications/jobs/NotificationJobs.ts +0 -6
- package/src/api/parameters/controllers/AdminParameterController.ts +26 -0
- package/src/api/parameters/services/ParameterProvider.ts +18 -0
- package/src/api/users/__tests__/Registration-emailMode.spec.ts +203 -0
- package/src/api/users/__tests__/UsernameSlugger.spec.ts +138 -0
- package/src/api/users/atoms/realmAuthSettingsAtom.ts +41 -3
- package/src/api/users/controllers/AdminSessionController.ts +29 -0
- package/src/api/users/controllers/AdminUserController.ts +32 -0
- package/src/api/users/index.ts +3 -0
- package/src/api/users/services/CredentialService.ts +5 -0
- package/src/api/users/services/RegistrationService.ts +49 -1
- package/src/api/users/services/SessionCrudService.ts +16 -0
- package/src/api/users/services/SessionService.ts +17 -59
- package/src/api/users/services/UsernameSlugger.ts +195 -0
- package/src/bucket/primitives/$bucket.ts +21 -0
- package/src/bucket/providers/CloudflareR2Provider.ts +15 -0
- package/src/bucket/providers/FileStorageProvider.ts +9 -0
- package/src/bucket/providers/LocalFileStorageProvider.ts +14 -0
- package/src/bucket/providers/MemoryFileStorageProvider.ts +9 -0
- package/src/bucket/providers/NodeS3BucketProvider.ts +35 -0
- package/src/cache/core/primitives/$cache.ts +20 -3
- package/src/cache/database/__tests__/DatabaseCacheProvider.behavior.spec.ts +203 -0
- package/src/cache/database/__tests__/DatabaseCacheProvider.spec.ts +110 -0
- package/src/cache/database/entities/cacheEntries.ts +55 -0
- package/src/cache/database/index.ts +36 -0
- package/src/cache/database/providers/DatabaseCacheProvider.ts +348 -0
- package/src/cli/core/services/ProjectScaffolder.ts +0 -2
- package/src/cli/core/tasks/BuildCloudflareTask.ts +17 -3
- package/src/cli/core/tasks/BuildSitemapTask.ts +7 -0
- package/src/cli/core/tasks/BuildVercelTask.ts +82 -3
- package/src/cli/platform/__tests__/detectResources.spec.ts +96 -0
- package/src/cli/platform/commands/platform.ts +7 -1
- package/src/scheduler/index.ts +14 -0
- package/src/scheduler/providers/CronProvider.ts +13 -0
package/dist/api/keys/index.js
CHANGED
|
@@ -66,8 +66,21 @@ var ApiKeyService = class {
|
|
|
66
66
|
repo = $repository(apiKeyEntity);
|
|
67
67
|
/**
|
|
68
68
|
* Cache validated API keys for 15 minutes.
|
|
69
|
+
*
|
|
70
|
+
* Pinned to per-isolate memory:
|
|
71
|
+
* - The cache replaces a single indexed SELECT on `api_keys`. Routing it
|
|
72
|
+
* through a distributed K/V (KV/Redis) buys little — the SELECT is
|
|
73
|
+
* already cheap — and pinning to DB would actively trade one SQL read
|
|
74
|
+
* for another.
|
|
75
|
+
* - Cold-start gives a fresh DB read on every new isolate, which is
|
|
76
|
+
* *better* for revocation visibility than a distributed cache that
|
|
77
|
+
* keeps serving stale entries until its own TTL.
|
|
78
|
+
* - Avoids provisioning KV/Redis just for this one cache. Users who need
|
|
79
|
+
* cross-isolate sharing for high-throughput API auth can override
|
|
80
|
+
* globally via `alepha.with({ provide: CacheProvider, use: ... })`.
|
|
69
81
|
*/
|
|
70
82
|
validationCache = $cache({
|
|
83
|
+
provider: "memory",
|
|
71
84
|
name: "api:keys:validation",
|
|
72
85
|
ttl: [15, "minutes"]
|
|
73
86
|
});
|
|
@@ -171,6 +184,26 @@ var ApiKeyService = class {
|
|
|
171
184
|
});
|
|
172
185
|
}
|
|
173
186
|
/**
|
|
187
|
+
* Revoke many API keys in one repository call (admin only). Already-revoked
|
|
188
|
+
* keys are silently skipped. Returns the ids that were actually revoked.
|
|
189
|
+
*/
|
|
190
|
+
async revokeManyByAdmin(ids) {
|
|
191
|
+
if (ids.length === 0) return [];
|
|
192
|
+
const toRevoke = (await this.repo.findMany({
|
|
193
|
+
where: { id: { inArray: ids } },
|
|
194
|
+
columns: [
|
|
195
|
+
"id",
|
|
196
|
+
"tokenHash",
|
|
197
|
+
"revokedAt"
|
|
198
|
+
]
|
|
199
|
+
})).filter((k) => !k.revokedAt);
|
|
200
|
+
if (toRevoke.length === 0) return [];
|
|
201
|
+
await Promise.all(toRevoke.map((k) => this.validationCache.invalidate(k.tokenHash)));
|
|
202
|
+
await this.repo.updateMany({ id: { inArray: toRevoke.map((k) => k.id) } }, { revokedAt: this.dateTimeProvider.now().toISOString() });
|
|
203
|
+
this.log.info("API keys revoked by admin", { count: toRevoke.length });
|
|
204
|
+
return toRevoke.map((k) => k.id);
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
174
207
|
* Revoke an API key. Only the owner can revoke their own keys.
|
|
175
208
|
*/
|
|
176
209
|
async revoke(id, userId) {
|
|
@@ -289,6 +322,26 @@ var AdminApiKeyController = class {
|
|
|
289
322
|
};
|
|
290
323
|
}
|
|
291
324
|
});
|
|
325
|
+
/**
|
|
326
|
+
* Revoke many API keys in one request.
|
|
327
|
+
*/
|
|
328
|
+
revokeApiKeys = $action({
|
|
329
|
+
method: "POST",
|
|
330
|
+
path: `${this.url}/revoke`,
|
|
331
|
+
group: this.group,
|
|
332
|
+
use: [$secure({ permissions: ["admin:api-key:delete"] })],
|
|
333
|
+
description: "Revoke many API keys",
|
|
334
|
+
schema: {
|
|
335
|
+
body: t.object({ ids: t.array(t.uuid(), {
|
|
336
|
+
minItems: 1,
|
|
337
|
+
maxItems: 1e3
|
|
338
|
+
}) }),
|
|
339
|
+
response: t.object({ revoked: t.array(t.uuid()) })
|
|
340
|
+
},
|
|
341
|
+
handler: async ({ body }) => {
|
|
342
|
+
return { revoked: await this.apiKeyService.revokeManyByAdmin(body.ids) };
|
|
343
|
+
}
|
|
344
|
+
});
|
|
292
345
|
};
|
|
293
346
|
//#endregion
|
|
294
347
|
//#region ../../src/api/keys/schemas/createApiKeyBodySchema.ts
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":[],"sources":["../../../src/api/keys/schemas/adminApiKeyQuerySchema.ts","../../../src/api/keys/schemas/adminApiKeyResourceSchema.ts","../../../src/api/keys/entities/apiKeyEntity.ts","../../../src/api/keys/services/ApiKeyService.ts","../../../src/api/keys/controllers/AdminApiKeyController.ts","../../../src/api/keys/schemas/createApiKeyBodySchema.ts","../../../src/api/keys/schemas/createApiKeyResponseSchema.ts","../../../src/api/keys/schemas/listApiKeyResponseSchema.ts","../../../src/api/keys/schemas/revokeApiKeyParamsSchema.ts","../../../src/api/keys/schemas/revokeApiKeyResponseSchema.ts","../../../src/api/keys/controllers/ApiKeyController.ts","../../../src/api/keys/index.ts"],"sourcesContent":["import { t } from \"alepha\";\nimport { pageQuerySchema } from \"alepha/orm\";\n\nexport const adminApiKeyQuerySchema = t.extend(pageQuerySchema, {\n userId: t.optional(t.uuid()),\n includeRevoked: t.optional(t.boolean()),\n});\n","import { t } from \"alepha\";\n\nexport const adminApiKeyResourceSchema = t.object({\n id: t.uuid(),\n userId: t.uuid(),\n name: t.string(),\n description: t.optional(t.string()),\n tokenPrefix: t.string(),\n tokenSuffix: t.string(),\n roles: t.array(t.string()),\n createdAt: t.datetime(),\n lastUsedAt: t.optional(t.datetime()),\n lastUsedIp: t.optional(t.string()),\n expiresAt: t.optional(t.datetime()),\n revokedAt: t.optional(t.datetime()),\n usageCount: t.integer(),\n});\n","import { type Static, t } from \"alepha\";\nimport { $entity, db } from \"alepha/orm\";\n\nexport const apiKeyEntity = $entity({\n name: \"api_keys\",\n schema: t.object({\n id: db.primaryKey(t.uuid()),\n createdAt: db.createdAt(),\n updatedAt: db.updatedAt(),\n\n // Owner\n userId: t.uuid(),\n\n // Key metadata\n name: t.text({ maxLength: 100 }),\n description: t.optional(t.text({ maxLength: 500 })),\n\n // Token (hashed) - internal, not user input\n tokenHash: t.string({ maxLength: 256 }),\n tokenPrefix: t.string({ maxLength: 10 }),\n tokenSuffix: t.string({ maxLength: 8 }),\n\n // Roles (snapshot from user at creation)\n roles: db.default(t.array(t.string()), []),\n\n // Tracking\n lastUsedAt: t.optional(t.datetime()),\n lastUsedIp: t.optional(t.string({ maxLength: 45 })),\n usageCount: db.default(t.integer(), 0),\n\n // Lifecycle\n expiresAt: t.optional(t.datetime()),\n revokedAt: t.optional(t.datetime()),\n }),\n indexes: [\n { columns: [\"userId\", \"name\"], unique: true },\n { columns: [\"tokenHash\"], unique: true },\n ],\n});\n\nexport type ApiKeyEntity = Static<typeof apiKeyEntity.schema>;\n","import { createHash, randomBytes } from \"node:crypto\";\nimport { $inject, Alepha } from \"alepha\";\nimport { $cache } from \"alepha/cache\";\nimport { DateTimeProvider } from \"alepha/datetime\";\nimport { $logger } from \"alepha/logger\";\nimport { $repository, sql } from \"alepha/orm\";\nimport type { IssuerResolver, UserInfo } from \"alepha/security\";\nimport { ForbiddenError, type ServerRequest } from \"alepha/server\";\nimport { type ApiKeyEntity, apiKeyEntity } from \"../entities/apiKeyEntity.ts\";\n\nexport class ApiKeyService {\n protected readonly alepha = $inject(Alepha);\n protected readonly dateTimeProvider = $inject(DateTimeProvider);\n protected readonly log = $logger();\n protected readonly repo = $repository(apiKeyEntity);\n\n /**\n * Cache validated API keys for 15 minutes.\n */\n protected readonly validationCache = $cache<ApiKeyEntity | null, [string]>({\n name: \"api:keys:validation\",\n ttl: [15, \"minutes\"],\n });\n\n // -------------------------------------------------------------------------\n // Resolver\n // -------------------------------------------------------------------------\n\n /**\n * Create an issuer resolver for API key authentication.\n * Lower priority means it runs before JWT resolver.\n *\n * @param options.priority - Priority of this resolver (default: 50, JWT is 100)\n * @param options.prefix - API key prefix to match in Bearer header (default: \"ak\")\n */\n public createResolver(\n options: { priority?: number; prefix?: string } = {},\n ): IssuerResolver {\n const { priority = 50, prefix = \"ak\" } = options;\n const prefixPattern = `${prefix}_`;\n\n return {\n priority,\n onRequest: async (req: ServerRequest) => {\n // Try query param first\n const url = typeof req.url === \"string\" ? new URL(req.url) : req.url;\n let token = url.searchParams.get(\"api_key\");\n\n // Try Bearer header - only if token starts with expected prefix\n if (!token) {\n const auth = req.headers.authorization;\n if (auth?.startsWith(\"Bearer \")) {\n const bearerToken = auth.slice(7);\n if (bearerToken.startsWith(prefixPattern)) {\n token = bearerToken;\n }\n }\n }\n\n if (!token) {\n return null;\n }\n\n return this.validate(token);\n },\n };\n }\n\n // -------------------------------------------------------------------------\n // CRUD\n // -------------------------------------------------------------------------\n\n /**\n * Create a new API key for a user.\n * Returns both the API key entity and the plain token (which is only available once).\n */\n public async create(options: {\n userId: string;\n name: string;\n roles: string[];\n description?: string;\n expiresAt?: Date;\n prefix?: string;\n }): Promise<{ apiKey: ApiKeyEntity; token: string }> {\n const prefix = options.prefix ?? \"ak\";\n const random = randomBytes(24).toString(\"base64url\");\n const token = `${prefix}_${random}`;\n const hash = this.hashToken(token);\n const suffix = token.slice(-8);\n\n const apiKey = await this.repo.create({\n userId: options.userId,\n name: options.name,\n description: options.description,\n tokenHash: hash,\n tokenPrefix: prefix,\n tokenSuffix: suffix,\n roles: options.roles,\n expiresAt: options.expiresAt?.toISOString(),\n });\n\n this.log.info(\"API key created\", {\n apiKeyId: apiKey.id,\n userId: options.userId,\n name: options.name,\n });\n\n return { apiKey, token };\n }\n\n /**\n * List all non-revoked API keys for a user.\n */\n public async list(userId: string): Promise<ApiKeyEntity[]> {\n return this.repo.findMany({\n where: {\n userId: { eq: userId },\n revokedAt: { isNull: true },\n },\n orderBy: { column: \"createdAt\", direction: \"desc\" },\n });\n }\n\n // -------------------------------------------------------------------------\n // Admin Operations\n // -------------------------------------------------------------------------\n\n /**\n * Find all API keys with optional filtering (admin only).\n */\n public async findAll(query: {\n userId?: string;\n includeRevoked?: boolean;\n page?: number;\n size?: number;\n sort?: string;\n }) {\n query.sort ??= \"-createdAt\";\n\n const where = this.repo.createQueryWhere();\n\n if (query.userId) {\n where.userId = { eq: query.userId };\n }\n\n if (!query.includeRevoked) {\n where.revokedAt = { isNull: true };\n }\n\n return this.repo.paginate(query, { where }, { count: true });\n }\n\n /**\n * Get an API key by ID (admin only).\n */\n public async getById(id: string): Promise<ApiKeyEntity> {\n return await this.repo.getById(id);\n }\n\n /**\n * Revoke any API key (admin only).\n */\n public async revokeByAdmin(id: string): Promise<void> {\n const apiKey = await this.repo.getById(id);\n\n if (apiKey.revokedAt) {\n return; // Already revoked\n }\n\n // Invalidate cache\n await this.validationCache.invalidate(apiKey.tokenHash);\n\n await this.repo.updateById(id, {\n revokedAt: this.dateTimeProvider.now().toISOString(),\n });\n\n this.log.info(\"API key revoked by admin\", {\n apiKeyId: id,\n userId: apiKey.userId,\n });\n }\n\n // -------------------------------------------------------------------------\n // User Operations\n // -------------------------------------------------------------------------\n\n /**\n * Revoke an API key. Only the owner can revoke their own keys.\n */\n public async revoke(id: string, userId: string): Promise<void> {\n const apiKey = await this.repo.getById(id);\n\n if (apiKey.userId !== userId) {\n throw new ForbiddenError(\"Not your API key\");\n }\n\n await this.validationCache.invalidate(apiKey.tokenHash);\n\n await this.repo.updateById(id, {\n revokedAt: this.dateTimeProvider.now().toISOString(),\n });\n\n this.log.info(\"API key revoked\", {\n apiKeyId: id,\n userId,\n });\n }\n\n // -------------------------------------------------------------------------\n // Validation\n // -------------------------------------------------------------------------\n\n /**\n * Validate an API key token and return user info if valid.\n */\n public async validate(token: string): Promise<UserInfo | null> {\n // Quick check for API key format\n if (!token.includes(\"_\")) {\n return null;\n }\n\n const hash = this.hashToken(token);\n\n // Try cache first\n let apiKey = await this.validationCache.get(hash);\n\n // If not in cache, look up in database\n if (apiKey === undefined) {\n apiKey =\n (await this.repo.findOne({\n where: { tokenHash: { eq: hash } },\n })) ?? null;\n\n // Store in cache (even if null, to prevent repeated lookups)\n await this.validationCache.set(hash, apiKey);\n }\n\n if (!apiKey) {\n return null;\n }\n\n // Check revocation\n if (apiKey.revokedAt) {\n return null;\n }\n\n // Check expiration\n if (\n apiKey.expiresAt &&\n this.dateTimeProvider.now().isAfter(apiKey.expiresAt)\n ) {\n return null;\n }\n\n // Update usage stats (fire and forget)\n this.updateUsage(apiKey.id).catch((error) => {\n this.log.warn(\"Failed to update API key usage\", { error });\n });\n\n return {\n id: apiKey.userId,\n roles: apiKey.roles,\n };\n }\n\n /**\n * Update usage statistics for an API key.\n */\n protected async updateUsage(id: string): Promise<void> {\n const request = this.alepha.store.get(\"alepha.http.request\");\n\n await this.repo.updateById(id, {\n lastUsedAt: this.dateTimeProvider.now().toISOString(),\n lastUsedIp: request?.ip,\n usageCount: sql`${this.repo.table.usageCount} + 1`,\n });\n }\n\n /**\n * Hash a token using SHA-256.\n */\n protected hashToken(token: string): string {\n return createHash(\"sha256\").update(token).digest(\"hex\");\n }\n}\n","import { $inject, t } from \"alepha\";\nimport { $secure } from \"alepha/security\";\nimport { $action, okSchema } from \"alepha/server\";\nimport { adminApiKeyQuerySchema } from \"../schemas/adminApiKeyQuerySchema.ts\";\nimport { adminApiKeyResourceSchema } from \"../schemas/adminApiKeyResourceSchema.ts\";\nimport { ApiKeyService } from \"../services/ApiKeyService.ts\";\n\n/**\n * REST API controller for admin API key management.\n * Admins can list, view, and revoke any API key.\n */\nexport class AdminApiKeyController {\n protected readonly url = \"/admin/api-keys\";\n protected readonly group = \"admin:api-keys\";\n protected readonly apiKeyService = $inject(ApiKeyService);\n\n /**\n * Find all API keys with optional filtering.\n */\n public readonly findApiKeys = $action({\n path: this.url,\n group: this.group,\n use: [$secure({ permissions: [\"admin:api-key:read\"] })],\n description: \"Find API keys with pagination and filtering\",\n schema: {\n query: adminApiKeyQuerySchema,\n response: t.page(adminApiKeyResourceSchema),\n },\n handler: ({ query }) => {\n const { userId, includeRevoked, ...pagination } = query;\n return this.apiKeyService.findAll({\n userId,\n includeRevoked,\n ...pagination,\n });\n },\n });\n\n /**\n * Get an API key by ID.\n */\n public readonly getApiKey = $action({\n path: `${this.url}/:id`,\n group: this.group,\n use: [$secure({ permissions: [\"admin:api-key:read\"] })],\n description: \"Get an API key by ID\",\n schema: {\n params: t.object({\n id: t.uuid(),\n }),\n response: adminApiKeyResourceSchema,\n },\n handler: ({ params }) => this.apiKeyService.getById(params.id),\n });\n\n /**\n * Revoke any API key.\n */\n public readonly revokeApiKey = $action({\n method: \"DELETE\",\n path: `${this.url}/:id`,\n group: this.group,\n use: [$secure({ permissions: [\"admin:api-key:delete\"] })],\n description: \"Revoke an API key\",\n schema: {\n params: t.object({\n id: t.uuid(),\n }),\n response: okSchema,\n },\n handler: async ({ params }) => {\n await this.apiKeyService.revokeByAdmin(params.id);\n return { ok: true, id: params.id };\n },\n });\n}\n","import { t } from \"alepha\";\n\nexport const createApiKeyBodySchema = t.object({\n name: t.text({ minLength: 1, maxLength: 100 }),\n description: t.optional(t.text({ maxLength: 500 })),\n expiresAt: t.optional(t.datetime()),\n});\n","import { t } from \"alepha\";\n\nexport const createApiKeyResponseSchema = t.object({\n id: t.uuid(),\n name: t.string(),\n token: t.string(),\n tokenSuffix: t.string(),\n roles: t.array(t.string()),\n createdAt: t.datetime(),\n expiresAt: t.optional(t.datetime()),\n});\n","import { t } from \"alepha\";\n\nexport const listApiKeyItemSchema = t.object({\n id: t.uuid(),\n name: t.string(),\n tokenPrefix: t.string(),\n tokenSuffix: t.string(),\n roles: t.array(t.string()),\n createdAt: t.datetime(),\n lastUsedAt: t.optional(t.datetime()),\n expiresAt: t.optional(t.datetime()),\n usageCount: t.integer(),\n});\n\nexport const listApiKeyResponseSchema = t.array(listApiKeyItemSchema);\n","import { t } from \"alepha\";\n\nexport const revokeApiKeyParamsSchema = t.object({\n id: t.uuid(),\n});\n","import { t } from \"alepha\";\n\nexport const revokeApiKeyResponseSchema = t.object({\n ok: t.boolean(),\n});\n","import { $inject } from \"alepha\";\nimport { $secure } from \"alepha/security\";\nimport { $action } from \"alepha/server\";\nimport { createApiKeyBodySchema } from \"../schemas/createApiKeyBodySchema.ts\";\nimport { createApiKeyResponseSchema } from \"../schemas/createApiKeyResponseSchema.ts\";\nimport { listApiKeyResponseSchema } from \"../schemas/listApiKeyResponseSchema.ts\";\nimport { revokeApiKeyParamsSchema } from \"../schemas/revokeApiKeyParamsSchema.ts\";\nimport { revokeApiKeyResponseSchema } from \"../schemas/revokeApiKeyResponseSchema.ts\";\nimport { ApiKeyService } from \"../services/ApiKeyService.ts\";\n\n/**\n * REST API controller for user's own API key management.\n * Users can create, list, and revoke their own API keys.\n */\nexport class ApiKeyController {\n protected readonly url = \"/api-keys\";\n protected readonly group = \"api-keys\";\n protected readonly apiKeyService = $inject(ApiKeyService);\n\n /**\n * Create a new API key for the authenticated user.\n * The token is only returned once upon creation.\n */\n public readonly createApiKey = $action({\n method: \"POST\",\n path: this.url,\n group: this.group,\n description: \"Create a new API key\",\n use: [$secure({ permissions: [\"api-key:create\"] })],\n schema: {\n body: createApiKeyBodySchema,\n response: createApiKeyResponseSchema,\n },\n handler: async (request) => {\n const { apiKey, token } = await this.apiKeyService.create({\n userId: request.user.id,\n name: request.body.name,\n description: request.body.description,\n roles: request.user.roles ?? [],\n expiresAt: request.body.expiresAt\n ? new Date(request.body.expiresAt)\n : undefined,\n });\n\n return {\n id: apiKey.id,\n name: apiKey.name,\n token,\n tokenSuffix: apiKey.tokenSuffix,\n roles: apiKey.roles,\n createdAt: apiKey.createdAt,\n expiresAt: apiKey.expiresAt,\n };\n },\n });\n\n /**\n * List all active API keys for the authenticated user.\n * Does not return the actual tokens.\n */\n public readonly listApiKeys = $action({\n path: this.url,\n group: this.group,\n description: \"List your API keys\",\n use: [$secure({ permissions: [\"api-key:read\"] })],\n schema: {\n response: listApiKeyResponseSchema,\n },\n handler: async (request) => {\n const apiKeys = await this.apiKeyService.list(request.user.id);\n\n return apiKeys.map((apiKey) => ({\n id: apiKey.id,\n name: apiKey.name,\n tokenPrefix: apiKey.tokenPrefix,\n tokenSuffix: apiKey.tokenSuffix,\n roles: apiKey.roles,\n createdAt: apiKey.createdAt,\n lastUsedAt: apiKey.lastUsedAt,\n expiresAt: apiKey.expiresAt,\n usageCount: apiKey.usageCount,\n }));\n },\n });\n\n /**\n * Revoke an API key. Only the owner can revoke their own keys.\n */\n public readonly revokeMyApiKey = $action({\n method: \"DELETE\",\n path: `${this.url}/:id`,\n group: this.group,\n description: \"Revoke an API key\",\n use: [$secure({ permissions: [\"api-key:delete\"] })],\n schema: {\n params: revokeApiKeyParamsSchema,\n response: revokeApiKeyResponseSchema,\n },\n handler: async (request) => {\n await this.apiKeyService.revoke(request.params.id, request.user.id);\n return { ok: true };\n },\n });\n}\n","import { $module } from \"alepha\";\nimport { AdminApiKeyController } from \"./controllers/AdminApiKeyController.ts\";\nimport { ApiKeyController } from \"./controllers/ApiKeyController.ts\";\nimport { ApiKeyService } from \"./services/ApiKeyService.ts\";\n\nexport * from \"./controllers/AdminApiKeyController.ts\";\nexport * from \"./controllers/ApiKeyController.ts\";\nexport * from \"./entities/apiKeyEntity.ts\";\nexport * from \"./schemas/adminApiKeyQuerySchema.ts\";\nexport * from \"./schemas/adminApiKeyResourceSchema.ts\";\nexport * from \"./schemas/createApiKeyBodySchema.ts\";\nexport * from \"./schemas/createApiKeyResponseSchema.ts\";\nexport * from \"./schemas/listApiKeyResponseSchema.ts\";\nexport * from \"./schemas/revokeApiKeyParamsSchema.ts\";\nexport * from \"./schemas/revokeApiKeyResponseSchema.ts\";\nexport * from \"./services/ApiKeyService.ts\";\n\n/**\n * API key management module for programmatic access.\n *\n * **Features:**\n * - Create API keys with role snapshots\n * - List and revoke API keys\n * - 15-minute validation caching\n * - Query param (?api_key=) and Bearer header support\n *\n * **Integration:**\n * To enable API key authentication for an issuer, register the resolver:\n *\n * ```ts\n * class MyApp {\n * apiKeyService = $inject(ApiKeyService);\n * issuer = $issuer({\n * secret: env.APP_SECRET,\n * resolvers: [this.apiKeyService.createResolver()],\n * });\n * }\n * ```\n *\n * @module alepha.api.keys\n */\nexport const AlephaApiKeys = $module({\n name: \"alepha.api.keys\",\n services: [ApiKeyService, ApiKeyController, AdminApiKeyController],\n});\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../../../src/api/keys/schemas/adminApiKeyQuerySchema.ts","../../../src/api/keys/schemas/adminApiKeyResourceSchema.ts","../../../src/api/keys/entities/apiKeyEntity.ts","../../../src/api/keys/services/ApiKeyService.ts","../../../src/api/keys/controllers/AdminApiKeyController.ts","../../../src/api/keys/schemas/createApiKeyBodySchema.ts","../../../src/api/keys/schemas/createApiKeyResponseSchema.ts","../../../src/api/keys/schemas/listApiKeyResponseSchema.ts","../../../src/api/keys/schemas/revokeApiKeyParamsSchema.ts","../../../src/api/keys/schemas/revokeApiKeyResponseSchema.ts","../../../src/api/keys/controllers/ApiKeyController.ts","../../../src/api/keys/index.ts"],"sourcesContent":["import { t } from \"alepha\";\nimport { pageQuerySchema } from \"alepha/orm\";\n\nexport const adminApiKeyQuerySchema = t.extend(pageQuerySchema, {\n userId: t.optional(t.uuid()),\n includeRevoked: t.optional(t.boolean()),\n});\n","import { t } from \"alepha\";\n\nexport const adminApiKeyResourceSchema = t.object({\n id: t.uuid(),\n userId: t.uuid(),\n name: t.string(),\n description: t.optional(t.string()),\n tokenPrefix: t.string(),\n tokenSuffix: t.string(),\n roles: t.array(t.string()),\n createdAt: t.datetime(),\n lastUsedAt: t.optional(t.datetime()),\n lastUsedIp: t.optional(t.string()),\n expiresAt: t.optional(t.datetime()),\n revokedAt: t.optional(t.datetime()),\n usageCount: t.integer(),\n});\n","import { type Static, t } from \"alepha\";\nimport { $entity, db } from \"alepha/orm\";\n\nexport const apiKeyEntity = $entity({\n name: \"api_keys\",\n schema: t.object({\n id: db.primaryKey(t.uuid()),\n createdAt: db.createdAt(),\n updatedAt: db.updatedAt(),\n\n // Owner\n userId: t.uuid(),\n\n // Key metadata\n name: t.text({ maxLength: 100 }),\n description: t.optional(t.text({ maxLength: 500 })),\n\n // Token (hashed) - internal, not user input\n tokenHash: t.string({ maxLength: 256 }),\n tokenPrefix: t.string({ maxLength: 10 }),\n tokenSuffix: t.string({ maxLength: 8 }),\n\n // Roles (snapshot from user at creation)\n roles: db.default(t.array(t.string()), []),\n\n // Tracking\n lastUsedAt: t.optional(t.datetime()),\n lastUsedIp: t.optional(t.string({ maxLength: 45 })),\n usageCount: db.default(t.integer(), 0),\n\n // Lifecycle\n expiresAt: t.optional(t.datetime()),\n revokedAt: t.optional(t.datetime()),\n }),\n indexes: [\n { columns: [\"userId\", \"name\"], unique: true },\n { columns: [\"tokenHash\"], unique: true },\n ],\n});\n\nexport type ApiKeyEntity = Static<typeof apiKeyEntity.schema>;\n","import { createHash, randomBytes } from \"node:crypto\";\nimport { $inject, Alepha } from \"alepha\";\nimport { $cache } from \"alepha/cache\";\nimport { DateTimeProvider } from \"alepha/datetime\";\nimport { $logger } from \"alepha/logger\";\nimport { $repository, sql } from \"alepha/orm\";\nimport type { IssuerResolver, UserInfo } from \"alepha/security\";\nimport { ForbiddenError, type ServerRequest } from \"alepha/server\";\nimport { type ApiKeyEntity, apiKeyEntity } from \"../entities/apiKeyEntity.ts\";\n\nexport class ApiKeyService {\n protected readonly alepha = $inject(Alepha);\n protected readonly dateTimeProvider = $inject(DateTimeProvider);\n protected readonly log = $logger();\n protected readonly repo = $repository(apiKeyEntity);\n\n /**\n * Cache validated API keys for 15 minutes.\n *\n * Pinned to per-isolate memory:\n * - The cache replaces a single indexed SELECT on `api_keys`. Routing it\n * through a distributed K/V (KV/Redis) buys little — the SELECT is\n * already cheap — and pinning to DB would actively trade one SQL read\n * for another.\n * - Cold-start gives a fresh DB read on every new isolate, which is\n * *better* for revocation visibility than a distributed cache that\n * keeps serving stale entries until its own TTL.\n * - Avoids provisioning KV/Redis just for this one cache. Users who need\n * cross-isolate sharing for high-throughput API auth can override\n * globally via `alepha.with({ provide: CacheProvider, use: ... })`.\n */\n protected readonly validationCache = $cache<ApiKeyEntity | null, [string]>({\n provider: \"memory\",\n name: \"api:keys:validation\",\n ttl: [15, \"minutes\"],\n });\n\n // -------------------------------------------------------------------------\n // Resolver\n // -------------------------------------------------------------------------\n\n /**\n * Create an issuer resolver for API key authentication.\n * Lower priority means it runs before JWT resolver.\n *\n * @param options.priority - Priority of this resolver (default: 50, JWT is 100)\n * @param options.prefix - API key prefix to match in Bearer header (default: \"ak\")\n */\n public createResolver(\n options: { priority?: number; prefix?: string } = {},\n ): IssuerResolver {\n const { priority = 50, prefix = \"ak\" } = options;\n const prefixPattern = `${prefix}_`;\n\n return {\n priority,\n onRequest: async (req: ServerRequest) => {\n // Try query param first\n const url = typeof req.url === \"string\" ? new URL(req.url) : req.url;\n let token = url.searchParams.get(\"api_key\");\n\n // Try Bearer header - only if token starts with expected prefix\n if (!token) {\n const auth = req.headers.authorization;\n if (auth?.startsWith(\"Bearer \")) {\n const bearerToken = auth.slice(7);\n if (bearerToken.startsWith(prefixPattern)) {\n token = bearerToken;\n }\n }\n }\n\n if (!token) {\n return null;\n }\n\n return this.validate(token);\n },\n };\n }\n\n // -------------------------------------------------------------------------\n // CRUD\n // -------------------------------------------------------------------------\n\n /**\n * Create a new API key for a user.\n * Returns both the API key entity and the plain token (which is only available once).\n */\n public async create(options: {\n userId: string;\n name: string;\n roles: string[];\n description?: string;\n expiresAt?: Date;\n prefix?: string;\n }): Promise<{ apiKey: ApiKeyEntity; token: string }> {\n const prefix = options.prefix ?? \"ak\";\n const random = randomBytes(24).toString(\"base64url\");\n const token = `${prefix}_${random}`;\n const hash = this.hashToken(token);\n const suffix = token.slice(-8);\n\n const apiKey = await this.repo.create({\n userId: options.userId,\n name: options.name,\n description: options.description,\n tokenHash: hash,\n tokenPrefix: prefix,\n tokenSuffix: suffix,\n roles: options.roles,\n expiresAt: options.expiresAt?.toISOString(),\n });\n\n this.log.info(\"API key created\", {\n apiKeyId: apiKey.id,\n userId: options.userId,\n name: options.name,\n });\n\n return { apiKey, token };\n }\n\n /**\n * List all non-revoked API keys for a user.\n */\n public async list(userId: string): Promise<ApiKeyEntity[]> {\n return this.repo.findMany({\n where: {\n userId: { eq: userId },\n revokedAt: { isNull: true },\n },\n orderBy: { column: \"createdAt\", direction: \"desc\" },\n });\n }\n\n // -------------------------------------------------------------------------\n // Admin Operations\n // -------------------------------------------------------------------------\n\n /**\n * Find all API keys with optional filtering (admin only).\n */\n public async findAll(query: {\n userId?: string;\n includeRevoked?: boolean;\n page?: number;\n size?: number;\n sort?: string;\n }) {\n query.sort ??= \"-createdAt\";\n\n const where = this.repo.createQueryWhere();\n\n if (query.userId) {\n where.userId = { eq: query.userId };\n }\n\n if (!query.includeRevoked) {\n where.revokedAt = { isNull: true };\n }\n\n return this.repo.paginate(query, { where }, { count: true });\n }\n\n /**\n * Get an API key by ID (admin only).\n */\n public async getById(id: string): Promise<ApiKeyEntity> {\n return await this.repo.getById(id);\n }\n\n /**\n * Revoke any API key (admin only).\n */\n public async revokeByAdmin(id: string): Promise<void> {\n const apiKey = await this.repo.getById(id);\n\n if (apiKey.revokedAt) {\n return; // Already revoked\n }\n\n // Invalidate cache\n await this.validationCache.invalidate(apiKey.tokenHash);\n\n await this.repo.updateById(id, {\n revokedAt: this.dateTimeProvider.now().toISOString(),\n });\n\n this.log.info(\"API key revoked by admin\", {\n apiKeyId: id,\n userId: apiKey.userId,\n });\n }\n\n /**\n * Revoke many API keys in one repository call (admin only). Already-revoked\n * keys are silently skipped. Returns the ids that were actually revoked.\n */\n public async revokeManyByAdmin(ids: string[]): Promise<string[]> {\n if (ids.length === 0) return [];\n\n const keys = await this.repo.findMany({\n where: { id: { inArray: ids } },\n columns: [\"id\", \"tokenHash\", \"revokedAt\"],\n });\n const toRevoke = keys.filter((k) => !k.revokedAt);\n if (toRevoke.length === 0) return [];\n\n await Promise.all(\n toRevoke.map((k) => this.validationCache.invalidate(k.tokenHash)),\n );\n\n await this.repo.updateMany(\n { id: { inArray: toRevoke.map((k) => k.id) } },\n {\n revokedAt: this.dateTimeProvider.now().toISOString(),\n },\n );\n\n this.log.info(\"API keys revoked by admin\", { count: toRevoke.length });\n return toRevoke.map((k) => k.id);\n }\n\n // -------------------------------------------------------------------------\n // User Operations\n // -------------------------------------------------------------------------\n\n /**\n * Revoke an API key. Only the owner can revoke their own keys.\n */\n public async revoke(id: string, userId: string): Promise<void> {\n const apiKey = await this.repo.getById(id);\n\n if (apiKey.userId !== userId) {\n throw new ForbiddenError(\"Not your API key\");\n }\n\n await this.validationCache.invalidate(apiKey.tokenHash);\n\n await this.repo.updateById(id, {\n revokedAt: this.dateTimeProvider.now().toISOString(),\n });\n\n this.log.info(\"API key revoked\", {\n apiKeyId: id,\n userId,\n });\n }\n\n // -------------------------------------------------------------------------\n // Validation\n // -------------------------------------------------------------------------\n\n /**\n * Validate an API key token and return user info if valid.\n */\n public async validate(token: string): Promise<UserInfo | null> {\n // Quick check for API key format\n if (!token.includes(\"_\")) {\n return null;\n }\n\n const hash = this.hashToken(token);\n\n // Try cache first\n let apiKey = await this.validationCache.get(hash);\n\n // If not in cache, look up in database\n if (apiKey === undefined) {\n apiKey =\n (await this.repo.findOne({\n where: { tokenHash: { eq: hash } },\n })) ?? null;\n\n // Store in cache (even if null, to prevent repeated lookups)\n await this.validationCache.set(hash, apiKey);\n }\n\n if (!apiKey) {\n return null;\n }\n\n // Check revocation\n if (apiKey.revokedAt) {\n return null;\n }\n\n // Check expiration\n if (\n apiKey.expiresAt &&\n this.dateTimeProvider.now().isAfter(apiKey.expiresAt)\n ) {\n return null;\n }\n\n // Update usage stats (fire and forget)\n this.updateUsage(apiKey.id).catch((error) => {\n this.log.warn(\"Failed to update API key usage\", { error });\n });\n\n return {\n id: apiKey.userId,\n roles: apiKey.roles,\n };\n }\n\n /**\n * Update usage statistics for an API key.\n */\n protected async updateUsage(id: string): Promise<void> {\n const request = this.alepha.store.get(\"alepha.http.request\");\n\n await this.repo.updateById(id, {\n lastUsedAt: this.dateTimeProvider.now().toISOString(),\n lastUsedIp: request?.ip,\n usageCount: sql`${this.repo.table.usageCount} + 1`,\n });\n }\n\n /**\n * Hash a token using SHA-256.\n */\n protected hashToken(token: string): string {\n return createHash(\"sha256\").update(token).digest(\"hex\");\n }\n}\n","import { $inject, t } from \"alepha\";\nimport { $secure } from \"alepha/security\";\nimport { $action, okSchema } from \"alepha/server\";\nimport { adminApiKeyQuerySchema } from \"../schemas/adminApiKeyQuerySchema.ts\";\nimport { adminApiKeyResourceSchema } from \"../schemas/adminApiKeyResourceSchema.ts\";\nimport { ApiKeyService } from \"../services/ApiKeyService.ts\";\n\n/**\n * REST API controller for admin API key management.\n * Admins can list, view, and revoke any API key.\n */\nexport class AdminApiKeyController {\n protected readonly url = \"/admin/api-keys\";\n protected readonly group = \"admin:api-keys\";\n protected readonly apiKeyService = $inject(ApiKeyService);\n\n /**\n * Find all API keys with optional filtering.\n */\n public readonly findApiKeys = $action({\n path: this.url,\n group: this.group,\n use: [$secure({ permissions: [\"admin:api-key:read\"] })],\n description: \"Find API keys with pagination and filtering\",\n schema: {\n query: adminApiKeyQuerySchema,\n response: t.page(adminApiKeyResourceSchema),\n },\n handler: ({ query }) => {\n const { userId, includeRevoked, ...pagination } = query;\n return this.apiKeyService.findAll({\n userId,\n includeRevoked,\n ...pagination,\n });\n },\n });\n\n /**\n * Get an API key by ID.\n */\n public readonly getApiKey = $action({\n path: `${this.url}/:id`,\n group: this.group,\n use: [$secure({ permissions: [\"admin:api-key:read\"] })],\n description: \"Get an API key by ID\",\n schema: {\n params: t.object({\n id: t.uuid(),\n }),\n response: adminApiKeyResourceSchema,\n },\n handler: ({ params }) => this.apiKeyService.getById(params.id),\n });\n\n /**\n * Revoke any API key.\n */\n public readonly revokeApiKey = $action({\n method: \"DELETE\",\n path: `${this.url}/:id`,\n group: this.group,\n use: [$secure({ permissions: [\"admin:api-key:delete\"] })],\n description: \"Revoke an API key\",\n schema: {\n params: t.object({\n id: t.uuid(),\n }),\n response: okSchema,\n },\n handler: async ({ params }) => {\n await this.apiKeyService.revokeByAdmin(params.id);\n return { ok: true, id: params.id };\n },\n });\n\n /**\n * Revoke many API keys in one request.\n */\n public readonly revokeApiKeys = $action({\n method: \"POST\",\n path: `${this.url}/revoke`,\n group: this.group,\n use: [$secure({ permissions: [\"admin:api-key:delete\"] })],\n description: \"Revoke many API keys\",\n schema: {\n body: t.object({\n ids: t.array(t.uuid(), { minItems: 1, maxItems: 1000 }),\n }),\n response: t.object({\n revoked: t.array(t.uuid()),\n }),\n },\n handler: async ({ body }) => {\n const revoked = await this.apiKeyService.revokeManyByAdmin(body.ids);\n return { revoked };\n },\n });\n}\n","import { t } from \"alepha\";\n\nexport const createApiKeyBodySchema = t.object({\n name: t.text({ minLength: 1, maxLength: 100 }),\n description: t.optional(t.text({ maxLength: 500 })),\n expiresAt: t.optional(t.datetime()),\n});\n","import { t } from \"alepha\";\n\nexport const createApiKeyResponseSchema = t.object({\n id: t.uuid(),\n name: t.string(),\n token: t.string(),\n tokenSuffix: t.string(),\n roles: t.array(t.string()),\n createdAt: t.datetime(),\n expiresAt: t.optional(t.datetime()),\n});\n","import { t } from \"alepha\";\n\nexport const listApiKeyItemSchema = t.object({\n id: t.uuid(),\n name: t.string(),\n tokenPrefix: t.string(),\n tokenSuffix: t.string(),\n roles: t.array(t.string()),\n createdAt: t.datetime(),\n lastUsedAt: t.optional(t.datetime()),\n expiresAt: t.optional(t.datetime()),\n usageCount: t.integer(),\n});\n\nexport const listApiKeyResponseSchema = t.array(listApiKeyItemSchema);\n","import { t } from \"alepha\";\n\nexport const revokeApiKeyParamsSchema = t.object({\n id: t.uuid(),\n});\n","import { t } from \"alepha\";\n\nexport const revokeApiKeyResponseSchema = t.object({\n ok: t.boolean(),\n});\n","import { $inject } from \"alepha\";\nimport { $secure } from \"alepha/security\";\nimport { $action } from \"alepha/server\";\nimport { createApiKeyBodySchema } from \"../schemas/createApiKeyBodySchema.ts\";\nimport { createApiKeyResponseSchema } from \"../schemas/createApiKeyResponseSchema.ts\";\nimport { listApiKeyResponseSchema } from \"../schemas/listApiKeyResponseSchema.ts\";\nimport { revokeApiKeyParamsSchema } from \"../schemas/revokeApiKeyParamsSchema.ts\";\nimport { revokeApiKeyResponseSchema } from \"../schemas/revokeApiKeyResponseSchema.ts\";\nimport { ApiKeyService } from \"../services/ApiKeyService.ts\";\n\n/**\n * REST API controller for user's own API key management.\n * Users can create, list, and revoke their own API keys.\n */\nexport class ApiKeyController {\n protected readonly url = \"/api-keys\";\n protected readonly group = \"api-keys\";\n protected readonly apiKeyService = $inject(ApiKeyService);\n\n /**\n * Create a new API key for the authenticated user.\n * The token is only returned once upon creation.\n */\n public readonly createApiKey = $action({\n method: \"POST\",\n path: this.url,\n group: this.group,\n description: \"Create a new API key\",\n use: [$secure({ permissions: [\"api-key:create\"] })],\n schema: {\n body: createApiKeyBodySchema,\n response: createApiKeyResponseSchema,\n },\n handler: async (request) => {\n const { apiKey, token } = await this.apiKeyService.create({\n userId: request.user.id,\n name: request.body.name,\n description: request.body.description,\n roles: request.user.roles ?? [],\n expiresAt: request.body.expiresAt\n ? new Date(request.body.expiresAt)\n : undefined,\n });\n\n return {\n id: apiKey.id,\n name: apiKey.name,\n token,\n tokenSuffix: apiKey.tokenSuffix,\n roles: apiKey.roles,\n createdAt: apiKey.createdAt,\n expiresAt: apiKey.expiresAt,\n };\n },\n });\n\n /**\n * List all active API keys for the authenticated user.\n * Does not return the actual tokens.\n */\n public readonly listApiKeys = $action({\n path: this.url,\n group: this.group,\n description: \"List your API keys\",\n use: [$secure({ permissions: [\"api-key:read\"] })],\n schema: {\n response: listApiKeyResponseSchema,\n },\n handler: async (request) => {\n const apiKeys = await this.apiKeyService.list(request.user.id);\n\n return apiKeys.map((apiKey) => ({\n id: apiKey.id,\n name: apiKey.name,\n tokenPrefix: apiKey.tokenPrefix,\n tokenSuffix: apiKey.tokenSuffix,\n roles: apiKey.roles,\n createdAt: apiKey.createdAt,\n lastUsedAt: apiKey.lastUsedAt,\n expiresAt: apiKey.expiresAt,\n usageCount: apiKey.usageCount,\n }));\n },\n });\n\n /**\n * Revoke an API key. Only the owner can revoke their own keys.\n */\n public readonly revokeMyApiKey = $action({\n method: \"DELETE\",\n path: `${this.url}/:id`,\n group: this.group,\n description: \"Revoke an API key\",\n use: [$secure({ permissions: [\"api-key:delete\"] })],\n schema: {\n params: revokeApiKeyParamsSchema,\n response: revokeApiKeyResponseSchema,\n },\n handler: async (request) => {\n await this.apiKeyService.revoke(request.params.id, request.user.id);\n return { ok: true };\n },\n });\n}\n","import { $module } from \"alepha\";\nimport { AdminApiKeyController } from \"./controllers/AdminApiKeyController.ts\";\nimport { ApiKeyController } from \"./controllers/ApiKeyController.ts\";\nimport { ApiKeyService } from \"./services/ApiKeyService.ts\";\n\nexport * from \"./controllers/AdminApiKeyController.ts\";\nexport * from \"./controllers/ApiKeyController.ts\";\nexport * from \"./entities/apiKeyEntity.ts\";\nexport * from \"./schemas/adminApiKeyQuerySchema.ts\";\nexport * from \"./schemas/adminApiKeyResourceSchema.ts\";\nexport * from \"./schemas/createApiKeyBodySchema.ts\";\nexport * from \"./schemas/createApiKeyResponseSchema.ts\";\nexport * from \"./schemas/listApiKeyResponseSchema.ts\";\nexport * from \"./schemas/revokeApiKeyParamsSchema.ts\";\nexport * from \"./schemas/revokeApiKeyResponseSchema.ts\";\nexport * from \"./services/ApiKeyService.ts\";\n\n/**\n * API key management module for programmatic access.\n *\n * **Features:**\n * - Create API keys with role snapshots\n * - List and revoke API keys\n * - 15-minute validation caching\n * - Query param (?api_key=) and Bearer header support\n *\n * **Integration:**\n * To enable API key authentication for an issuer, register the resolver:\n *\n * ```ts\n * class MyApp {\n * apiKeyService = $inject(ApiKeyService);\n * issuer = $issuer({\n * secret: env.APP_SECRET,\n * resolvers: [this.apiKeyService.createResolver()],\n * });\n * }\n * ```\n *\n * @module alepha.api.keys\n */\nexport const AlephaApiKeys = $module({\n name: \"alepha.api.keys\",\n services: [ApiKeyService, ApiKeyController, AdminApiKeyController],\n});\n"],"mappings":"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"}
|
|
@@ -277,6 +277,24 @@ declare class AdminNotificationController {
|
|
|
277
277
|
}>>>;
|
|
278
278
|
}>;
|
|
279
279
|
}>;
|
|
280
|
+
readonly deleteNotification: _$alepha_server0.ActionPrimitiveFn<{
|
|
281
|
+
params: _$alepha.TObject<{
|
|
282
|
+
id: _$alepha.TString;
|
|
283
|
+
}>;
|
|
284
|
+
response: _$alepha.TObject<{
|
|
285
|
+
ok: _$alepha.TBoolean;
|
|
286
|
+
id: _$alepha.TOptional<_$alepha.TUnion<[_$alepha.TString, _$alepha.TInteger]>>;
|
|
287
|
+
count: _$alepha.TOptional<_$alepha.TNumber>;
|
|
288
|
+
}>;
|
|
289
|
+
}>;
|
|
290
|
+
readonly deleteNotifications: _$alepha_server0.ActionPrimitiveFn<{
|
|
291
|
+
body: _$alepha.TObject<{
|
|
292
|
+
ids: _$alepha.TArray<_$alepha.TString>;
|
|
293
|
+
}>;
|
|
294
|
+
response: _$alepha.TObject<{
|
|
295
|
+
deleted: _$alepha.TArray<_$alepha.TString>;
|
|
296
|
+
}>;
|
|
297
|
+
}>;
|
|
280
298
|
protected toResource(exec: Record<string, unknown>): {
|
|
281
299
|
id: unknown;
|
|
282
300
|
createdAt: unknown;
|
|
@@ -387,10 +405,20 @@ type NotificationResource = Static<typeof notificationResourceSchema>;
|
|
|
387
405
|
*
|
|
388
406
|
* **Features:**
|
|
389
407
|
* - Notification definitions (email/SMS templates)
|
|
390
|
-
* -
|
|
408
|
+
* - Delivery via `$job` with retry and audit trail (`record: "all"` + no ring buffer trim)
|
|
391
409
|
* - Runtime-editable retention window via `$parameter` — purge cron respects it live
|
|
392
410
|
* - Admin API for inspecting sent notifications
|
|
393
411
|
*
|
|
412
|
+
* **Delivery mode** is decided at runtime by the `$job` system:
|
|
413
|
+
* - If your app loads `AlephaApiJobsQueue` (and thus `AlephaQueue`), notifications
|
|
414
|
+
* go through the queue (best for high-volume systems).
|
|
415
|
+
* - Otherwise, notifications run in **direct** mode: pushed to the outbox table
|
|
416
|
+
* and processed in the same process right after the HTTP response is returned.
|
|
417
|
+
* The reconciliation sweep is the safety net for crashes / retries.
|
|
418
|
+
*
|
|
419
|
+
* Direct mode is the recommended default for small / cheap deployments
|
|
420
|
+
* (Cloudflare Workers, single-instance Node) — no queue infrastructure required.
|
|
421
|
+
*
|
|
394
422
|
* @module alepha.api.notifications
|
|
395
423
|
*/
|
|
396
424
|
declare const AlephaApiNotifications: _$alepha.Service<_$alepha.Module>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","names":[],"sources":["../../../src/api/notifications/primitives/$notification.ts","../../../src/api/notifications/schemas/notificationPayloadSchema.ts","../../../src/api/notifications/services/NotificationSenderService.ts","../../../src/api/notifications/jobs/NotificationJobs.ts","../../../src/api/notifications/controllers/AdminNotificationController.ts","../../../src/api/notifications/schemas/notificationContactPreferencesSchema.ts","../../../src/api/notifications/schemas/notificationContactSchema.ts","../../../src/api/notifications/schemas/notificationDetailResourceSchema.ts","../../../src/api/notifications/schemas/notificationQuerySchema.ts","../../../src/api/notifications/schemas/notificationResourceSchema.ts","../../../src/api/notifications/index.ts"],"mappings":";;;;;;;;;;;;;;;;;;;;;;AAuCA;;;;;;;;;;;;;;;;;;cAAa,aAAA;EAAA,WAA2B,OAAA,EAAO,OAAA,EACpC,4BAAA,CAA6B,CAAA,IAAE,qBAAA,CAAA,CAAA;EAAA;;UAKzB,4BAAA,WAAuC,OAAA,UAC9C,mBAAA,CAAoB,CAAA;EAC5B,IAAA;EACA,WAAA;EACA,QAAA;EACA,QAAA;EACA,SAAA;EACA,YAAA;IAAA,CAEG,IAAA,WAAe,mBAAA,CAAoB,CAAA;EAAA;EAEtC,MAAA,EAAQ,CAAA;AAAA;AAAA,cAKG,qBAAA,WAAgC,OAAA,UAAiB,SAAA,CAC5D,4BAAA,CAA6B,CAAA;EAAA,mBAEV,gBAAA,EAAgB,gBAAA;EAAA,IAExB,IAAA,CAAA;EAIE,IAAA,CAAK,OAAA,EAAS,uBAAA,CAAwB,CAAA,IAAE,OAAA;EAoC9C,SAAA,CAAU,OAAA,EAAS,OAAA,CAAQ,4BAAA,CAA6B,CAAA;AAAA;AAAA,UAShD,uBAAA,WAAkC,OAAA;EACjD,SAAA,EAAW,YAAA,CAAa,CAAA;EACxB,OAAA;AAAA;AAAA,UAGe,mBAAA,WAA8B,OAAA;EAC7C,KAAA;IACE,OAAA;IACA,IAAA,aAAiB,SAAA,EAAW,MAAA,CAAO,CAAA;EAAA;EAErC,GAAA;IACE,OAAA,aAAoB,SAAA,EAAW,MAAA,CAAO,CAAA;EAAA;AAAA;;;cC5H7B,yBAAA,WAAyB,OAAA;QAQpC,QAAA,CAAA,OAAA;;;;;;;;KAEU,mBAAA,GAAsB,MAAA,QAAc,yBAAA;;;cCLnC,yBAAA;EAAA,mBACQ,MAAA,EAAM,MAAA;EAAA,mBACN,GAAA,EADM,gBAAA,CACH,MAAA;EAAA,mBACH,aAAA,EAAa,aAAA;EAAA,mBACb,WAAA,EAAW,WAAA;EAEjB,IAAA,CAAK,OAAA,EAAS,mBAAA,GAAmB,OAAA;;;;;;;;;;;;;EAqCvC,SAAA,CAAU,OAAA,EAAS,mBAAA;;;;EAkBnB,WAAA,CAAY,OAAA,EAAS,mBAAA;;;;;YAmBlB,IAAA,CAAK,OAAA,EAAS,mBAAA;qDAAmB,QAAA,CAAA,WAAA;;;;;;;;;;;;;;;;;AFhD7C;;;;;;;cGba,gBAAA;EAAA,mBACQ,GAAA,EADQ,gBAAA,CACL,MAAA;EAAA,mBACH,EAAA,EAAE,gBAAA;EAAA,mBACF,yBAAA,EAAyB,yBAAA;EAAA,mBAGzB,UAAA,EAAU,aAAA,CAAA,UAAA,UAAA,OAAA;kDAHe,QAAA,CAAA,OAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EHqB5C;EAAA,SGfgB,QAAA,EAAQ,wBAAA,CAAA,kBAAA,UAAA,OAAA;mBAHK,QAAA,CAAA,QAAA;EAAA;EAAA,SAkBb,gBAAA,EAAgB,kBAAA,CAAA,YAAA,UAAA,OAAA;UAfR,QAAA,CAAA,OAAA;;;;;;;;
|
|
1
|
+
{"version":3,"file":"index.d.ts","names":[],"sources":["../../../src/api/notifications/primitives/$notification.ts","../../../src/api/notifications/schemas/notificationPayloadSchema.ts","../../../src/api/notifications/services/NotificationSenderService.ts","../../../src/api/notifications/jobs/NotificationJobs.ts","../../../src/api/notifications/controllers/AdminNotificationController.ts","../../../src/api/notifications/schemas/notificationContactPreferencesSchema.ts","../../../src/api/notifications/schemas/notificationContactSchema.ts","../../../src/api/notifications/schemas/notificationDetailResourceSchema.ts","../../../src/api/notifications/schemas/notificationQuerySchema.ts","../../../src/api/notifications/schemas/notificationResourceSchema.ts","../../../src/api/notifications/index.ts"],"mappings":"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"}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { $inject, $module, Alepha, AlephaError, KIND, Primitive, createPrimitive, t } from "alepha";
|
|
2
|
-
import { $job,
|
|
2
|
+
import { $job, AlephaApiJobs, jobExecutionEntity } from "alepha/api/jobs";
|
|
3
3
|
import { $parameter, AlephaApiParameters } from "alepha/api/parameters";
|
|
4
4
|
import { $repository, pageQuerySchema } from "alepha/orm";
|
|
5
5
|
import { $secure } from "alepha/security";
|
|
6
|
-
import { $action, NotFoundError } from "alepha/server";
|
|
6
|
+
import { $action, NotFoundError, okSchema } from "alepha/server";
|
|
7
7
|
import { DateTimeProvider } from "alepha/datetime";
|
|
8
8
|
import { $logger, logEntrySchema } from "alepha/logger";
|
|
9
9
|
import { EmailProvider } from "alepha/email";
|
|
@@ -190,15 +190,7 @@ var NotificationJobs = class {
|
|
|
190
190
|
name: "api:notifications:sendNotification",
|
|
191
191
|
description: "Sends a notification (email/SMS) and keeps every execution for audit.",
|
|
192
192
|
schema: notificationPayloadSchema,
|
|
193
|
-
retry: {
|
|
194
|
-
retries: 3,
|
|
195
|
-
backoff: {
|
|
196
|
-
initial: [5, "seconds"],
|
|
197
|
-
factor: 4,
|
|
198
|
-
max: [10, "minutes"],
|
|
199
|
-
jitter: true
|
|
200
|
-
}
|
|
201
|
-
},
|
|
193
|
+
retry: { retries: 3 },
|
|
202
194
|
timeout: [30, "seconds"],
|
|
203
195
|
record: "all",
|
|
204
196
|
keep: {
|
|
@@ -322,6 +314,46 @@ var AdminNotificationController = class {
|
|
|
322
314
|
return this.toDetailResource(exec);
|
|
323
315
|
}
|
|
324
316
|
});
|
|
317
|
+
deleteNotification = $action({
|
|
318
|
+
method: "DELETE",
|
|
319
|
+
path: `${this.url}/:id`,
|
|
320
|
+
group: this.group,
|
|
321
|
+
use: [$secure({ permissions: ["admin:notification:delete"] })],
|
|
322
|
+
description: "Delete a notification record",
|
|
323
|
+
schema: {
|
|
324
|
+
params: t.object({ id: t.uuid() }),
|
|
325
|
+
response: okSchema
|
|
326
|
+
},
|
|
327
|
+
handler: async ({ params }) => {
|
|
328
|
+
const exec = await this.executions.findById(params.id);
|
|
329
|
+
if (!exec || exec.jobName !== this.jobName) throw new NotFoundError(`Notification not found: ${params.id}`);
|
|
330
|
+
await this.executions.deleteById(params.id);
|
|
331
|
+
return {
|
|
332
|
+
ok: true,
|
|
333
|
+
id: params.id
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
});
|
|
337
|
+
deleteNotifications = $action({
|
|
338
|
+
method: "POST",
|
|
339
|
+
path: `${this.url}/delete`,
|
|
340
|
+
group: this.group,
|
|
341
|
+
use: [$secure({ permissions: ["admin:notification:delete"] })],
|
|
342
|
+
description: "Delete many notification records in one call",
|
|
343
|
+
schema: {
|
|
344
|
+
body: t.object({ ids: t.array(t.uuid(), {
|
|
345
|
+
minItems: 1,
|
|
346
|
+
maxItems: 1e3
|
|
347
|
+
}) }),
|
|
348
|
+
response: t.object({ deleted: t.array(t.uuid()) })
|
|
349
|
+
},
|
|
350
|
+
handler: async ({ body }) => {
|
|
351
|
+
return { deleted: (await this.executions.deleteMany({
|
|
352
|
+
id: { inArray: body.ids },
|
|
353
|
+
jobName: { eq: this.jobName }
|
|
354
|
+
})).map(String) };
|
|
355
|
+
}
|
|
356
|
+
});
|
|
325
357
|
toResource(exec) {
|
|
326
358
|
const payload = exec.payload ?? {};
|
|
327
359
|
return {
|
|
@@ -370,15 +402,25 @@ const notificationContactSchema = t.object({
|
|
|
370
402
|
*
|
|
371
403
|
* **Features:**
|
|
372
404
|
* - Notification definitions (email/SMS templates)
|
|
373
|
-
* -
|
|
405
|
+
* - Delivery via `$job` with retry and audit trail (`record: "all"` + no ring buffer trim)
|
|
374
406
|
* - Runtime-editable retention window via `$parameter` — purge cron respects it live
|
|
375
407
|
* - Admin API for inspecting sent notifications
|
|
376
408
|
*
|
|
409
|
+
* **Delivery mode** is decided at runtime by the `$job` system:
|
|
410
|
+
* - If your app loads `AlephaApiJobsQueue` (and thus `AlephaQueue`), notifications
|
|
411
|
+
* go through the queue (best for high-volume systems).
|
|
412
|
+
* - Otherwise, notifications run in **direct** mode: pushed to the outbox table
|
|
413
|
+
* and processed in the same process right after the HTTP response is returned.
|
|
414
|
+
* The reconciliation sweep is the safety net for crashes / retries.
|
|
415
|
+
*
|
|
416
|
+
* Direct mode is the recommended default for small / cheap deployments
|
|
417
|
+
* (Cloudflare Workers, single-instance Node) — no queue infrastructure required.
|
|
418
|
+
*
|
|
377
419
|
* @module alepha.api.notifications
|
|
378
420
|
*/
|
|
379
421
|
const AlephaApiNotifications = $module({
|
|
380
422
|
name: "alepha.api.notifications",
|
|
381
|
-
imports: [
|
|
423
|
+
imports: [AlephaApiJobs, AlephaApiParameters],
|
|
382
424
|
primitives: [$notification],
|
|
383
425
|
services: [
|
|
384
426
|
NotificationSenderService,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":[],"sources":["../../../src/api/notifications/schemas/notificationPayloadSchema.ts","../../../src/api/notifications/primitives/$notification.ts","../../../src/api/notifications/services/NotificationSenderService.ts","../../../src/api/notifications/jobs/NotificationJobs.ts","../../../src/api/notifications/schemas/notificationResourceSchema.ts","../../../src/api/notifications/schemas/notificationDetailResourceSchema.ts","../../../src/api/notifications/schemas/notificationQuerySchema.ts","../../../src/api/notifications/controllers/AdminNotificationController.ts","../../../src/api/notifications/schemas/notificationContactPreferencesSchema.ts","../../../src/api/notifications/schemas/notificationContactSchema.ts","../../../src/api/notifications/index.ts"],"sourcesContent":["import { type Static, t } from \"alepha\";\n\nexport const notificationPayloadSchema = t.object({\n type: t.enum([\"email\", \"sms\"]),\n template: t.text(),\n contact: t.text(),\n variables: t.optional(t.record(t.text(), t.any())),\n category: t.optional(t.text()),\n critical: t.optional(t.boolean()),\n sensitive: t.optional(t.boolean()),\n});\n\nexport type NotificationPayload = Static<typeof notificationPayloadSchema>;\n","import {\n $inject,\n createPrimitive,\n KIND,\n Primitive,\n type Static,\n type StaticEncode,\n type TObject,\n} from \"alepha\";\nimport { NotificationJobs } from \"../jobs/NotificationJobs.ts\";\n\n/**\n * Creates a notification primitive for managing email/SMS notification templates.\n *\n * Provides type-safe, reusable notification templates with multi-language support,\n * variable substitution, and categorization for different notification channels.\n *\n * @example\n * ```ts\n * class NotificationTemplates {\n * welcomeEmail = $notification({\n * name: \"welcome-email\",\n * category: \"onboarding\",\n * schema: t.object({ username: t.text(), activationLink: t.text() }),\n * email: {\n * subject: \"Welcome to our platform!\",\n * body: (vars) => `Hello ${vars.username}, click: ${vars.activationLink}`\n * }\n * });\n *\n * async sendWelcome(user: User) {\n * await this.welcomeEmail.push({\n * variables: { username: user.name, activationLink: generateLink() },\n * contact: user.email\n * });\n * }\n * }\n * ```\n */\nexport const $notification = <T extends TObject>(\n options: NotificationPrimitiveOptions<T>,\n) => createPrimitive(NotificationPrimitive<T>, options);\n\n// ---------------------------------------------------------------------------------------------------------------------\n\nexport interface NotificationPrimitiveOptions<T extends TObject>\n extends NotificationMessage<T> {\n name?: string;\n description?: string;\n category?: string;\n critical?: boolean;\n sensitive?: boolean;\n translations?: {\n // e.g., \"en\", \"fr\", even \"en-US\"\n [lang: string]: NotificationMessage<T>;\n };\n schema: T;\n}\n\n// ---------------------------------------------------------------------------------------------------------------------\n\nexport class NotificationPrimitive<T extends TObject> extends Primitive<\n NotificationPrimitiveOptions<T>\n> {\n protected readonly notificationJobs = $inject(NotificationJobs);\n\n public get name() {\n return this.options.name ?? `${this.config.propertyKey}`;\n }\n\n public async push(options: NotificationPushOptions<T>) {\n const pushOpts = this.options.critical\n ? ({ priority: \"critical\" } as const)\n : undefined;\n\n if (this.options.email) {\n await this.notificationJobs.sendNotification.push(\n {\n type: \"email\",\n template: this.name,\n contact: options.contact,\n variables: options.variables as Record<string, unknown>,\n category: this.options.category,\n critical: this.options.critical,\n sensitive: this.options.sensitive,\n },\n pushOpts,\n );\n }\n\n if (this.options.sms) {\n await this.notificationJobs.sendNotification.push(\n {\n type: \"sms\",\n template: this.name,\n contact: options.contact,\n variables: options.variables as Record<string, unknown>,\n category: this.options.category,\n critical: this.options.critical,\n sensitive: this.options.sensitive,\n },\n pushOpts,\n );\n }\n }\n\n public configure(options: Partial<NotificationPrimitiveOptions<T>>) {\n Object.assign(this.options, options);\n }\n}\n\n$notification[KIND] = NotificationPrimitive;\n\n// ---------------------------------------------------------------------------------------------------------------------\n\nexport interface NotificationPushOptions<T extends TObject> {\n variables: StaticEncode<T>;\n contact: string;\n}\n\nexport interface NotificationMessage<T extends TObject> {\n email?: {\n subject: string;\n body: string | ((variables: Static<T>) => string);\n };\n sms?: {\n message: string | ((variables: Static<T>) => string);\n };\n}\n","import { $inject, Alepha, AlephaError } from \"alepha\";\nimport { EmailProvider } from \"alepha/email\";\nimport { $logger } from \"alepha/logger\";\nimport { SmsProvider } from \"alepha/sms\";\nimport { $notification } from \"../primitives/$notification.ts\";\nimport type { NotificationPayload } from \"../schemas/notificationPayloadSchema.ts\";\n\nexport class NotificationSenderService {\n protected readonly alepha = $inject(Alepha);\n protected readonly log = $logger();\n protected readonly emailProvider = $inject(EmailProvider);\n protected readonly smsProvider = $inject(SmsProvider);\n\n public async send(payload: NotificationPayload) {\n this.log.debug(\"Processing notification\", {\n type: payload.type,\n template: payload.template,\n contact: payload.contact,\n });\n\n if (payload.type === \"email\") {\n const rendered = this.renderEmail(payload);\n await this.emailProvider.send(rendered);\n this.log.info(\"Email notification sent\", {\n template: payload.template,\n contact: payload.contact,\n });\n return {\n type: \"email\" as const,\n to: rendered.to,\n subject: rendered.subject,\n body: rendered.body,\n };\n }\n\n if (payload.type === \"sms\") {\n const rendered = this.renderSms(payload);\n await this.smsProvider.send(rendered);\n this.log.info(\"SMS notification sent\", {\n template: payload.template,\n contact: payload.contact,\n });\n return {\n type: \"sms\" as const,\n to: rendered.to,\n message: rendered.message,\n };\n }\n }\n\n public renderSms(payload: NotificationPayload) {\n const { variables, contact, template } = this.load(payload);\n\n const sms = template.options.sms;\n if (!sms) {\n throw new AlephaError(\n `Notification template ${payload.template} has no sms defined`,\n );\n }\n\n const message =\n typeof sms.message === \"function\"\n ? sms.message(variables as any)\n : sms.message;\n\n return { to: contact, message };\n }\n\n public renderEmail(payload: NotificationPayload) {\n const { variables, contact, template } = this.load(payload);\n\n const email = template.options.email;\n if (!email) {\n throw new AlephaError(\n `Notification template ${payload.template} has no email defined`,\n );\n }\n\n const subject = email.subject;\n const body =\n typeof email.body === \"function\"\n ? email.body(variables as any)\n : email.body;\n\n return { to: contact, subject, body };\n }\n\n protected load(payload: NotificationPayload) {\n const variables = payload.variables || {};\n const contact = payload.contact;\n const template = this.alepha\n .primitives($notification)\n .find((it) => it.name === payload.template);\n\n if (!template) {\n throw new AlephaError(\n `No notification template found for ${payload.template}`,\n );\n }\n\n return { template, variables, contact };\n }\n}\n","import { $inject, t } from \"alepha\";\nimport { $job, jobExecutionEntity } from \"alepha/api/jobs\";\nimport { $parameter } from \"alepha/api/parameters\";\nimport { DateTimeProvider } from \"alepha/datetime\";\nimport { $logger } from \"alepha/logger\";\nimport { $repository } from \"alepha/orm\";\nimport { notificationPayloadSchema } from \"../schemas/notificationPayloadSchema.ts\";\nimport { NotificationSenderService } from \"../services/NotificationSenderService.ts\";\n\n/**\n * Notification jobs + runtime-editable retention.\n *\n * - `settings` — a `$parameter` exposing `retentionDays` that admins can\n * update at runtime. Changes propagate across instances via the parameter\n * pub/sub; the next purge run picks up the new value with no restart.\n * - `sendNotification` — queue-mode, audit-oriented. Every execution is kept\n * (`record: \"all\"`, `keep: { ok: 0, error: 0 }` disables the ring-buffer\n * trim) so the audit trail survives even under heavy volume.\n * - `purgeOldNotifications` — cron sweep that deletes notification execution\n * rows whose `completedAt` is older than the current `retentionDays`.\n *\n * Cron expression note: the purge cron is declared statically (`0 3 * * *`)\n * because some runtimes (Cloudflare Workers) freeze cron triggers at deploy\n * time. The *retention window* is fully runtime-editable — that's the knob\n * that actually matters for operators.\n */\nexport class NotificationJobs {\n protected readonly log = $logger();\n protected readonly dt = $inject(DateTimeProvider);\n protected readonly notificationSenderService = $inject(\n NotificationSenderService,\n );\n protected readonly executions = $repository(jobExecutionEntity);\n\n /** Runtime-editable config. Admins can change retentionDays without deploy. */\n public readonly settings = $parameter({\n name: \"alepha.api.notifications\",\n description: \"Notification delivery & retention settings.\",\n schema: t.object({\n retentionDays: t.integer({\n description:\n \"Days to keep notification execution rows before the purge sweep removes them.\",\n minimum: 1,\n }),\n }),\n default: {\n retentionDays: 7,\n },\n });\n\n public readonly sendNotification = $job({\n name: \"api:notifications:sendNotification\",\n description:\n \"Sends a notification (email/SMS) and keeps every execution for audit.\",\n schema: notificationPayloadSchema,\n retry: {\n retries: 3,\n backoff: {\n initial: [5, \"seconds\"],\n factor: 4,\n max: [10, \"minutes\"],\n jitter: true,\n },\n },\n timeout: [30, \"seconds\"],\n record: \"all\",\n keep: { ok: 0, error: 0 },\n handler: async ({ payload }) => {\n await this.notificationSenderService.send(payload);\n },\n });\n\n public readonly purgeOldNotifications = $job({\n name: \"api:notifications:purgeOldNotifications\",\n description:\n \"Hourly sweep that deletes notification execution rows older than the configured retention window.\",\n cron: \"0 * * * *\",\n handler: async ({ now }) => {\n const { retentionDays } = this.settings.cachedCurrentContent;\n const cutoff = now.subtract(retentionDays, \"day\").toISOString();\n const jobName = this.sendNotification.name;\n\n const expired = await this.executions.findMany({\n where: {\n jobName: { eq: jobName },\n status: { inArray: [\"ok\", \"error\", \"cancelled\"] },\n completedAt: { lt: cutoff },\n },\n columns: [\"id\"] as any,\n limit: 5_000,\n });\n\n if (expired.length === 0) {\n this.log.debug(\"Notification purge: nothing to delete\", {\n cutoff,\n retentionDays,\n });\n return;\n }\n\n await this.executions.deleteMany({\n id: { inArray: expired.map((r) => r.id) },\n });\n this.log.info(\n `Notification purge: deleted ${expired.length} row(s) older than ${retentionDays} days`,\n { cutoff },\n );\n },\n });\n}\n","import { type Static, t } from \"alepha\";\n\nexport const notificationResourceSchema = t.object({\n id: t.uuid(),\n createdAt: t.datetime(),\n status: t.text(),\n template: t.optional(t.text()),\n type: t.optional(t.text()),\n contact: t.optional(t.text()),\n category: t.optional(t.text()),\n critical: t.optional(t.boolean()),\n sensitive: t.optional(t.boolean()),\n startedAt: t.optional(t.datetime()),\n completedAt: t.optional(t.datetime()),\n error: t.optional(t.text()),\n});\n\nexport type NotificationResource = Static<typeof notificationResourceSchema>;\n","import { type Static, t } from \"alepha\";\nimport { logEntrySchema } from \"alepha/logger\";\nimport { notificationResourceSchema } from \"./notificationResourceSchema.ts\";\n\nexport const notificationDetailResourceSchema = t.extend(\n notificationResourceSchema,\n {\n variables: t.optional(t.record(t.text(), t.any())),\n rendered: t.optional(t.record(t.text(), t.any())),\n logs: t.optional(t.array(logEntrySchema)),\n },\n {\n title: \"NotificationDetailResource\",\n description: \"A notification resource with rendered content and logs.\",\n },\n);\n\nexport type NotificationDetailResource = Static<\n typeof notificationDetailResourceSchema\n>;\n","import { type Static, t } from \"alepha\";\nimport { pageQuerySchema } from \"alepha/orm\";\n\nexport const notificationQuerySchema = t.extend(pageQuerySchema, {\n status: t.optional(\n t.enum([\n \"pending\",\n \"scheduled\",\n \"retrying\",\n \"running\",\n \"completed\",\n \"dead\",\n \"cancelled\",\n ]),\n ),\n});\n\nexport type NotificationQuery = Static<typeof notificationQuerySchema>;\n","import { $inject, t } from \"alepha\";\nimport { jobExecutionEntity } from \"alepha/api/jobs\";\nimport { $repository } from \"alepha/orm\";\nimport { $secure } from \"alepha/security\";\nimport { $action, NotFoundError } from \"alepha/server\";\nimport { NotificationJobs } from \"../jobs/NotificationJobs.ts\";\nimport { notificationDetailResourceSchema } from \"../schemas/notificationDetailResourceSchema.ts\";\nimport { notificationQuerySchema } from \"../schemas/notificationQuerySchema.ts\";\nimport { notificationResourceSchema } from \"../schemas/notificationResourceSchema.ts\";\n\nexport class AdminNotificationController {\n protected readonly url: string = \"/notifications\";\n protected readonly group: string = \"admin:notifications\";\n protected readonly notificationJobs = $inject(NotificationJobs);\n protected readonly executions = $repository(jobExecutionEntity);\n\n protected get jobName(): string {\n return this.notificationJobs.sendNotification.name;\n }\n\n public readonly findNotifications = $action({\n path: this.url,\n group: this.group,\n use: [$secure({ permissions: [\"admin:notification:read\"] })],\n schema: {\n query: notificationQuerySchema,\n response: t.page(notificationResourceSchema),\n },\n handler: async ({ query }) => {\n query.sort ??= \"-createdAt\";\n const where = this.executions.createQueryWhere();\n where.jobName = { eq: this.jobName };\n const page = await this.executions.paginate(\n query,\n { where },\n { count: true },\n );\n return {\n ...page,\n content: page.content.map((exec) => this.toResource(exec)),\n } as any;\n },\n });\n\n public readonly getNotification = $action({\n path: `${this.url}/:id`,\n group: this.group,\n use: [$secure({ permissions: [\"admin:notification:read\"] })],\n schema: {\n params: t.object({\n id: t.uuid(),\n }),\n response: notificationDetailResourceSchema,\n },\n handler: async ({ params }) => {\n const exec = await this.executions.findById(params.id);\n if (!exec || exec.jobName !== this.jobName) {\n throw new NotFoundError(`Notification not found: ${params.id}`);\n }\n return this.toDetailResource(exec) as any;\n },\n });\n\n protected toResource(exec: Record<string, unknown>) {\n const payload = (exec.payload ?? {}) as Record<string, unknown>;\n return {\n id: exec.id,\n createdAt: exec.createdAt,\n status: exec.status,\n template: payload.template,\n type: payload.type,\n contact: payload.contact,\n category: payload.category,\n critical: payload.critical,\n sensitive: payload.sensitive,\n startedAt: exec.startedAt,\n completedAt: exec.completedAt,\n error: exec.error,\n };\n }\n\n protected toDetailResource(exec: Record<string, unknown>) {\n const payload = (exec.payload ?? {}) as Record<string, unknown>;\n return {\n ...this.toResource(exec),\n variables: payload.variables,\n logs: exec.logs,\n };\n }\n}\n","import { type Static, t } from \"alepha\";\n\nexport const notificationContactPreferencesSchema = t.object({\n language: t.optional(t.text()),\n exclude: t.array(t.text()),\n});\n\nexport type NotificationContactPreferences = Static<\n typeof notificationContactPreferencesSchema\n>;\n","import { type Static, t } from \"alepha\";\n\nexport const notificationContactSchema = t.object({\n email: t.optional(t.email()),\n phoneNumber: t.optional(t.e164()),\n firstName: t.optional(t.shortText()),\n lastName: t.optional(t.text({ size: \"short\" })),\n language: t.optional(t.bcp47()),\n});\n\nexport type NotificationContact = Static<typeof notificationContactSchema>;\n","import { $module } from \"alepha\";\nimport { AlephaApiJobsQueue } from \"alepha/api/jobs\";\nimport { AlephaApiParameters } from \"alepha/api/parameters\";\nimport { AdminNotificationController } from \"./controllers/AdminNotificationController.ts\";\nimport { NotificationJobs } from \"./jobs/NotificationJobs.ts\";\nimport { $notification } from \"./primitives/$notification.ts\";\nimport { NotificationSenderService } from \"./services/NotificationSenderService.ts\";\n\n// ---------------------------------------------------------------------------------------------------------------------\n\nexport * from \"./controllers/AdminNotificationController.ts\";\nexport * from \"./jobs/NotificationJobs.ts\";\nexport * from \"./primitives/$notification.ts\";\nexport * from \"./schemas/notificationContactPreferencesSchema.ts\";\nexport * from \"./schemas/notificationContactSchema.ts\";\nexport * from \"./schemas/notificationDetailResourceSchema.ts\";\nexport * from \"./schemas/notificationPayloadSchema.ts\";\nexport * from \"./schemas/notificationQuerySchema.ts\";\nexport * from \"./schemas/notificationResourceSchema.ts\";\nexport * from \"./services/NotificationSenderService.ts\";\n\n// ---------------------------------------------------------------------------------------------------------------------\n\n/**\n * User notification management.\n *\n * **Features:**\n * - Notification definitions (email/SMS templates)\n * - Queue-based delivery with retry and audit trail (`record: \"all\"` + no ring buffer trim)\n * - Runtime-editable retention window via `$parameter` — purge cron respects it live\n * - Admin API for inspecting sent notifications\n *\n * @module alepha.api.notifications\n */\nexport const AlephaApiNotifications = $module({\n name: \"alepha.api.notifications\",\n imports: [AlephaApiJobsQueue, AlephaApiParameters],\n primitives: [$notification],\n services: [\n NotificationSenderService,\n NotificationJobs,\n AdminNotificationController,\n ],\n});\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../../../src/api/notifications/schemas/notificationPayloadSchema.ts","../../../src/api/notifications/primitives/$notification.ts","../../../src/api/notifications/services/NotificationSenderService.ts","../../../src/api/notifications/jobs/NotificationJobs.ts","../../../src/api/notifications/schemas/notificationResourceSchema.ts","../../../src/api/notifications/schemas/notificationDetailResourceSchema.ts","../../../src/api/notifications/schemas/notificationQuerySchema.ts","../../../src/api/notifications/controllers/AdminNotificationController.ts","../../../src/api/notifications/schemas/notificationContactPreferencesSchema.ts","../../../src/api/notifications/schemas/notificationContactSchema.ts","../../../src/api/notifications/index.ts"],"sourcesContent":["import { type Static, t } from \"alepha\";\n\nexport const notificationPayloadSchema = t.object({\n type: t.enum([\"email\", \"sms\"]),\n template: t.text(),\n contact: t.text(),\n variables: t.optional(t.record(t.text(), t.any())),\n category: t.optional(t.text()),\n critical: t.optional(t.boolean()),\n sensitive: t.optional(t.boolean()),\n});\n\nexport type NotificationPayload = Static<typeof notificationPayloadSchema>;\n","import {\n $inject,\n createPrimitive,\n KIND,\n Primitive,\n type Static,\n type StaticEncode,\n type TObject,\n} from \"alepha\";\nimport { NotificationJobs } from \"../jobs/NotificationJobs.ts\";\n\n/**\n * Creates a notification primitive for managing email/SMS notification templates.\n *\n * Provides type-safe, reusable notification templates with multi-language support,\n * variable substitution, and categorization for different notification channels.\n *\n * @example\n * ```ts\n * class NotificationTemplates {\n * welcomeEmail = $notification({\n * name: \"welcome-email\",\n * category: \"onboarding\",\n * schema: t.object({ username: t.text(), activationLink: t.text() }),\n * email: {\n * subject: \"Welcome to our platform!\",\n * body: (vars) => `Hello ${vars.username}, click: ${vars.activationLink}`\n * }\n * });\n *\n * async sendWelcome(user: User) {\n * await this.welcomeEmail.push({\n * variables: { username: user.name, activationLink: generateLink() },\n * contact: user.email\n * });\n * }\n * }\n * ```\n */\nexport const $notification = <T extends TObject>(\n options: NotificationPrimitiveOptions<T>,\n) => createPrimitive(NotificationPrimitive<T>, options);\n\n// ---------------------------------------------------------------------------------------------------------------------\n\nexport interface NotificationPrimitiveOptions<T extends TObject>\n extends NotificationMessage<T> {\n name?: string;\n description?: string;\n category?: string;\n critical?: boolean;\n sensitive?: boolean;\n translations?: {\n // e.g., \"en\", \"fr\", even \"en-US\"\n [lang: string]: NotificationMessage<T>;\n };\n schema: T;\n}\n\n// ---------------------------------------------------------------------------------------------------------------------\n\nexport class NotificationPrimitive<T extends TObject> extends Primitive<\n NotificationPrimitiveOptions<T>\n> {\n protected readonly notificationJobs = $inject(NotificationJobs);\n\n public get name() {\n return this.options.name ?? `${this.config.propertyKey}`;\n }\n\n public async push(options: NotificationPushOptions<T>) {\n const pushOpts = this.options.critical\n ? ({ priority: \"critical\" } as const)\n : undefined;\n\n if (this.options.email) {\n await this.notificationJobs.sendNotification.push(\n {\n type: \"email\",\n template: this.name,\n contact: options.contact,\n variables: options.variables as Record<string, unknown>,\n category: this.options.category,\n critical: this.options.critical,\n sensitive: this.options.sensitive,\n },\n pushOpts,\n );\n }\n\n if (this.options.sms) {\n await this.notificationJobs.sendNotification.push(\n {\n type: \"sms\",\n template: this.name,\n contact: options.contact,\n variables: options.variables as Record<string, unknown>,\n category: this.options.category,\n critical: this.options.critical,\n sensitive: this.options.sensitive,\n },\n pushOpts,\n );\n }\n }\n\n public configure(options: Partial<NotificationPrimitiveOptions<T>>) {\n Object.assign(this.options, options);\n }\n}\n\n$notification[KIND] = NotificationPrimitive;\n\n// ---------------------------------------------------------------------------------------------------------------------\n\nexport interface NotificationPushOptions<T extends TObject> {\n variables: StaticEncode<T>;\n contact: string;\n}\n\nexport interface NotificationMessage<T extends TObject> {\n email?: {\n subject: string;\n body: string | ((variables: Static<T>) => string);\n };\n sms?: {\n message: string | ((variables: Static<T>) => string);\n };\n}\n","import { $inject, Alepha, AlephaError } from \"alepha\";\nimport { EmailProvider } from \"alepha/email\";\nimport { $logger } from \"alepha/logger\";\nimport { SmsProvider } from \"alepha/sms\";\nimport { $notification } from \"../primitives/$notification.ts\";\nimport type { NotificationPayload } from \"../schemas/notificationPayloadSchema.ts\";\n\nexport class NotificationSenderService {\n protected readonly alepha = $inject(Alepha);\n protected readonly log = $logger();\n protected readonly emailProvider = $inject(EmailProvider);\n protected readonly smsProvider = $inject(SmsProvider);\n\n public async send(payload: NotificationPayload) {\n this.log.debug(\"Processing notification\", {\n type: payload.type,\n template: payload.template,\n contact: payload.contact,\n });\n\n if (payload.type === \"email\") {\n const rendered = this.renderEmail(payload);\n await this.emailProvider.send(rendered);\n this.log.info(\"Email notification sent\", {\n template: payload.template,\n contact: payload.contact,\n });\n return {\n type: \"email\" as const,\n to: rendered.to,\n subject: rendered.subject,\n body: rendered.body,\n };\n }\n\n if (payload.type === \"sms\") {\n const rendered = this.renderSms(payload);\n await this.smsProvider.send(rendered);\n this.log.info(\"SMS notification sent\", {\n template: payload.template,\n contact: payload.contact,\n });\n return {\n type: \"sms\" as const,\n to: rendered.to,\n message: rendered.message,\n };\n }\n }\n\n public renderSms(payload: NotificationPayload) {\n const { variables, contact, template } = this.load(payload);\n\n const sms = template.options.sms;\n if (!sms) {\n throw new AlephaError(\n `Notification template ${payload.template} has no sms defined`,\n );\n }\n\n const message =\n typeof sms.message === \"function\"\n ? sms.message(variables as any)\n : sms.message;\n\n return { to: contact, message };\n }\n\n public renderEmail(payload: NotificationPayload) {\n const { variables, contact, template } = this.load(payload);\n\n const email = template.options.email;\n if (!email) {\n throw new AlephaError(\n `Notification template ${payload.template} has no email defined`,\n );\n }\n\n const subject = email.subject;\n const body =\n typeof email.body === \"function\"\n ? email.body(variables as any)\n : email.body;\n\n return { to: contact, subject, body };\n }\n\n protected load(payload: NotificationPayload) {\n const variables = payload.variables || {};\n const contact = payload.contact;\n const template = this.alepha\n .primitives($notification)\n .find((it) => it.name === payload.template);\n\n if (!template) {\n throw new AlephaError(\n `No notification template found for ${payload.template}`,\n );\n }\n\n return { template, variables, contact };\n }\n}\n","import { $inject, t } from \"alepha\";\nimport { $job, jobExecutionEntity } from \"alepha/api/jobs\";\nimport { $parameter } from \"alepha/api/parameters\";\nimport { DateTimeProvider } from \"alepha/datetime\";\nimport { $logger } from \"alepha/logger\";\nimport { $repository } from \"alepha/orm\";\nimport { notificationPayloadSchema } from \"../schemas/notificationPayloadSchema.ts\";\nimport { NotificationSenderService } from \"../services/NotificationSenderService.ts\";\n\n/**\n * Notification jobs + runtime-editable retention.\n *\n * - `settings` — a `$parameter` exposing `retentionDays` that admins can\n * update at runtime. Changes propagate across instances via the parameter\n * pub/sub; the next purge run picks up the new value with no restart.\n * - `sendNotification` — queue-mode, audit-oriented. Every execution is kept\n * (`record: \"all\"`, `keep: { ok: 0, error: 0 }` disables the ring-buffer\n * trim) so the audit trail survives even under heavy volume.\n * - `purgeOldNotifications` — cron sweep that deletes notification execution\n * rows whose `completedAt` is older than the current `retentionDays`.\n *\n * Cron expression note: the purge cron is declared statically (`0 3 * * *`)\n * because some runtimes (Cloudflare Workers) freeze cron triggers at deploy\n * time. The *retention window* is fully runtime-editable — that's the knob\n * that actually matters for operators.\n */\nexport class NotificationJobs {\n protected readonly log = $logger();\n protected readonly dt = $inject(DateTimeProvider);\n protected readonly notificationSenderService = $inject(\n NotificationSenderService,\n );\n protected readonly executions = $repository(jobExecutionEntity);\n\n /** Runtime-editable config. Admins can change retentionDays without deploy. */\n public readonly settings = $parameter({\n name: \"alepha.api.notifications\",\n description: \"Notification delivery & retention settings.\",\n schema: t.object({\n retentionDays: t.integer({\n description:\n \"Days to keep notification execution rows before the purge sweep removes them.\",\n minimum: 1,\n }),\n }),\n default: {\n retentionDays: 7,\n },\n });\n\n public readonly sendNotification = $job({\n name: \"api:notifications:sendNotification\",\n description:\n \"Sends a notification (email/SMS) and keeps every execution for audit.\",\n schema: notificationPayloadSchema,\n retry: {\n retries: 3,\n },\n timeout: [30, \"seconds\"],\n record: \"all\",\n keep: { ok: 0, error: 0 },\n handler: async ({ payload }) => {\n await this.notificationSenderService.send(payload);\n },\n });\n\n public readonly purgeOldNotifications = $job({\n name: \"api:notifications:purgeOldNotifications\",\n description:\n \"Hourly sweep that deletes notification execution rows older than the configured retention window.\",\n cron: \"0 * * * *\",\n handler: async ({ now }) => {\n const { retentionDays } = this.settings.cachedCurrentContent;\n const cutoff = now.subtract(retentionDays, \"day\").toISOString();\n const jobName = this.sendNotification.name;\n\n const expired = await this.executions.findMany({\n where: {\n jobName: { eq: jobName },\n status: { inArray: [\"ok\", \"error\", \"cancelled\"] },\n completedAt: { lt: cutoff },\n },\n columns: [\"id\"] as any,\n limit: 5_000,\n });\n\n if (expired.length === 0) {\n this.log.debug(\"Notification purge: nothing to delete\", {\n cutoff,\n retentionDays,\n });\n return;\n }\n\n await this.executions.deleteMany({\n id: { inArray: expired.map((r) => r.id) },\n });\n this.log.info(\n `Notification purge: deleted ${expired.length} row(s) older than ${retentionDays} days`,\n { cutoff },\n );\n },\n });\n}\n","import { type Static, t } from \"alepha\";\n\nexport const notificationResourceSchema = t.object({\n id: t.uuid(),\n createdAt: t.datetime(),\n status: t.text(),\n template: t.optional(t.text()),\n type: t.optional(t.text()),\n contact: t.optional(t.text()),\n category: t.optional(t.text()),\n critical: t.optional(t.boolean()),\n sensitive: t.optional(t.boolean()),\n startedAt: t.optional(t.datetime()),\n completedAt: t.optional(t.datetime()),\n error: t.optional(t.text()),\n});\n\nexport type NotificationResource = Static<typeof notificationResourceSchema>;\n","import { type Static, t } from \"alepha\";\nimport { logEntrySchema } from \"alepha/logger\";\nimport { notificationResourceSchema } from \"./notificationResourceSchema.ts\";\n\nexport const notificationDetailResourceSchema = t.extend(\n notificationResourceSchema,\n {\n variables: t.optional(t.record(t.text(), t.any())),\n rendered: t.optional(t.record(t.text(), t.any())),\n logs: t.optional(t.array(logEntrySchema)),\n },\n {\n title: \"NotificationDetailResource\",\n description: \"A notification resource with rendered content and logs.\",\n },\n);\n\nexport type NotificationDetailResource = Static<\n typeof notificationDetailResourceSchema\n>;\n","import { type Static, t } from \"alepha\";\nimport { pageQuerySchema } from \"alepha/orm\";\n\nexport const notificationQuerySchema = t.extend(pageQuerySchema, {\n status: t.optional(\n t.enum([\n \"pending\",\n \"scheduled\",\n \"retrying\",\n \"running\",\n \"completed\",\n \"dead\",\n \"cancelled\",\n ]),\n ),\n});\n\nexport type NotificationQuery = Static<typeof notificationQuerySchema>;\n","import { $inject, t } from \"alepha\";\nimport { jobExecutionEntity } from \"alepha/api/jobs\";\nimport { $repository } from \"alepha/orm\";\nimport { $secure } from \"alepha/security\";\nimport { $action, NotFoundError, okSchema } from \"alepha/server\";\nimport { NotificationJobs } from \"../jobs/NotificationJobs.ts\";\nimport { notificationDetailResourceSchema } from \"../schemas/notificationDetailResourceSchema.ts\";\nimport { notificationQuerySchema } from \"../schemas/notificationQuerySchema.ts\";\nimport { notificationResourceSchema } from \"../schemas/notificationResourceSchema.ts\";\n\nexport class AdminNotificationController {\n protected readonly url: string = \"/notifications\";\n protected readonly group: string = \"admin:notifications\";\n protected readonly notificationJobs = $inject(NotificationJobs);\n protected readonly executions = $repository(jobExecutionEntity);\n\n protected get jobName(): string {\n return this.notificationJobs.sendNotification.name;\n }\n\n public readonly findNotifications = $action({\n path: this.url,\n group: this.group,\n use: [$secure({ permissions: [\"admin:notification:read\"] })],\n schema: {\n query: notificationQuerySchema,\n response: t.page(notificationResourceSchema),\n },\n handler: async ({ query }) => {\n query.sort ??= \"-createdAt\";\n const where = this.executions.createQueryWhere();\n where.jobName = { eq: this.jobName };\n const page = await this.executions.paginate(\n query,\n { where },\n { count: true },\n );\n return {\n ...page,\n content: page.content.map((exec) => this.toResource(exec)),\n } as any;\n },\n });\n\n public readonly getNotification = $action({\n path: `${this.url}/:id`,\n group: this.group,\n use: [$secure({ permissions: [\"admin:notification:read\"] })],\n schema: {\n params: t.object({\n id: t.uuid(),\n }),\n response: notificationDetailResourceSchema,\n },\n handler: async ({ params }) => {\n const exec = await this.executions.findById(params.id);\n if (!exec || exec.jobName !== this.jobName) {\n throw new NotFoundError(`Notification not found: ${params.id}`);\n }\n return this.toDetailResource(exec) as any;\n },\n });\n\n public readonly deleteNotification = $action({\n method: \"DELETE\",\n path: `${this.url}/:id`,\n group: this.group,\n use: [$secure({ permissions: [\"admin:notification:delete\"] })],\n description: \"Delete a notification record\",\n schema: {\n params: t.object({\n id: t.uuid(),\n }),\n response: okSchema,\n },\n handler: async ({ params }) => {\n const exec = await this.executions.findById(params.id);\n if (!exec || exec.jobName !== this.jobName) {\n throw new NotFoundError(`Notification not found: ${params.id}`);\n }\n await this.executions.deleteById(params.id);\n return { ok: true, id: params.id };\n },\n });\n\n public readonly deleteNotifications = $action({\n method: \"POST\",\n path: `${this.url}/delete`,\n group: this.group,\n use: [$secure({ permissions: [\"admin:notification:delete\"] })],\n description: \"Delete many notification records in one call\",\n schema: {\n body: t.object({\n ids: t.array(t.uuid(), { minItems: 1, maxItems: 1000 }),\n }),\n response: t.object({\n deleted: t.array(t.uuid()),\n }),\n },\n handler: async ({ body }) => {\n // Constrain to this job's executions so an admin can't delete arbitrary\n // job rows through this endpoint.\n const deleted = await this.executions.deleteMany({\n id: { inArray: body.ids },\n jobName: { eq: this.jobName },\n });\n return { deleted: deleted.map(String) };\n },\n });\n\n protected toResource(exec: Record<string, unknown>) {\n const payload = (exec.payload ?? {}) as Record<string, unknown>;\n return {\n id: exec.id,\n createdAt: exec.createdAt,\n status: exec.status,\n template: payload.template,\n type: payload.type,\n contact: payload.contact,\n category: payload.category,\n critical: payload.critical,\n sensitive: payload.sensitive,\n startedAt: exec.startedAt,\n completedAt: exec.completedAt,\n error: exec.error,\n };\n }\n\n protected toDetailResource(exec: Record<string, unknown>) {\n const payload = (exec.payload ?? {}) as Record<string, unknown>;\n return {\n ...this.toResource(exec),\n variables: payload.variables,\n logs: exec.logs,\n };\n }\n}\n","import { type Static, t } from \"alepha\";\n\nexport const notificationContactPreferencesSchema = t.object({\n language: t.optional(t.text()),\n exclude: t.array(t.text()),\n});\n\nexport type NotificationContactPreferences = Static<\n typeof notificationContactPreferencesSchema\n>;\n","import { type Static, t } from \"alepha\";\n\nexport const notificationContactSchema = t.object({\n email: t.optional(t.email()),\n phoneNumber: t.optional(t.e164()),\n firstName: t.optional(t.shortText()),\n lastName: t.optional(t.text({ size: \"short\" })),\n language: t.optional(t.bcp47()),\n});\n\nexport type NotificationContact = Static<typeof notificationContactSchema>;\n","import { $module } from \"alepha\";\nimport { AlephaApiJobs } from \"alepha/api/jobs\";\nimport { AlephaApiParameters } from \"alepha/api/parameters\";\nimport { AdminNotificationController } from \"./controllers/AdminNotificationController.ts\";\nimport { NotificationJobs } from \"./jobs/NotificationJobs.ts\";\nimport { $notification } from \"./primitives/$notification.ts\";\nimport { NotificationSenderService } from \"./services/NotificationSenderService.ts\";\n\n// ---------------------------------------------------------------------------------------------------------------------\n\nexport * from \"./controllers/AdminNotificationController.ts\";\nexport * from \"./jobs/NotificationJobs.ts\";\nexport * from \"./primitives/$notification.ts\";\nexport * from \"./schemas/notificationContactPreferencesSchema.ts\";\nexport * from \"./schemas/notificationContactSchema.ts\";\nexport * from \"./schemas/notificationDetailResourceSchema.ts\";\nexport * from \"./schemas/notificationPayloadSchema.ts\";\nexport * from \"./schemas/notificationQuerySchema.ts\";\nexport * from \"./schemas/notificationResourceSchema.ts\";\nexport * from \"./services/NotificationSenderService.ts\";\n\n// ---------------------------------------------------------------------------------------------------------------------\n\n/**\n * User notification management.\n *\n * **Features:**\n * - Notification definitions (email/SMS templates)\n * - Delivery via `$job` with retry and audit trail (`record: \"all\"` + no ring buffer trim)\n * - Runtime-editable retention window via `$parameter` — purge cron respects it live\n * - Admin API for inspecting sent notifications\n *\n * **Delivery mode** is decided at runtime by the `$job` system:\n * - If your app loads `AlephaApiJobsQueue` (and thus `AlephaQueue`), notifications\n * go through the queue (best for high-volume systems).\n * - Otherwise, notifications run in **direct** mode: pushed to the outbox table\n * and processed in the same process right after the HTTP response is returned.\n * The reconciliation sweep is the safety net for crashes / retries.\n *\n * Direct mode is the recommended default for small / cheap deployments\n * (Cloudflare Workers, single-instance Node) — no queue infrastructure required.\n *\n * @module alepha.api.notifications\n */\nexport const AlephaApiNotifications = $module({\n name: \"alepha.api.notifications\",\n imports: [AlephaApiJobs, AlephaApiParameters],\n primitives: [$notification],\n services: [\n NotificationSenderService,\n NotificationJobs,\n AdminNotificationController,\n ],\n});\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":[],"sources":["../../../src/api/organizations/schemas/createOrganizationSchema.ts","../../../src/api/organizations/schemas/organizationQuerySchema.ts","../../../src/api/organizations/entities/organizations.ts","../../../src/api/organizations/schemas/organizationResourceSchema.ts","../../../src/api/organizations/schemas/updateOrganizationSchema.ts","../../../src/api/organizations/services/OrganizationService.ts","../../../src/api/organizations/controllers/AdminOrganizationController.ts","../../../src/api/organizations/index.ts"],"sourcesContent":["import type { Static } from \"alepha\";\nimport { t } from \"alepha\";\n\nexport const createOrganizationSchema = t.object({\n name: t.text(),\n slug: t.text({ minLength: 2, maxLength: 100 }),\n enabled: t.optional(t.boolean()),\n});\n\nexport type CreateOrganization = Static<typeof createOrganizationSchema>;\n","import type { Static } from \"alepha\";\nimport { t } from \"alepha\";\nimport { pageQuerySchema } from \"alepha/orm\";\n\nexport const organizationQuerySchema = t.extend(pageQuerySchema, {\n name: t.optional(t.text({ description: \"Filter by name (partial match)\" })),\n enabled: t.optional(t.boolean({ description: \"Filter by enabled status\" })),\n});\n\nexport type OrganizationQuery = Static<typeof organizationQuerySchema>;\n","import type { Static } from \"alepha\";\nimport { t } from \"alepha\";\nimport { $entity, db } from \"alepha/orm\";\n\nexport const organizations = $entity({\n name: \"organizations\",\n schema: t.object({\n id: db.primaryKey(t.uuid()),\n version: db.version(),\n createdAt: db.createdAt(),\n updatedAt: db.updatedAt(),\n\n name: t.text(),\n slug: t.text({ minLength: 2, maxLength: 100 }),\n enabled: db.default(t.boolean(), true),\n }),\n indexes: [{ columns: [\"slug\"], unique: true }],\n});\n\nexport type OrganizationEntity = Static<typeof organizations.schema>;\n","import type { Static } from \"alepha\";\nimport { organizations } from \"../entities/organizations.ts\";\n\nexport const organizationResourceSchema = organizations.schema;\n\nexport type OrganizationResource = Static<typeof organizationResourceSchema>;\n","import type { Static } from \"alepha\";\nimport { t } from \"alepha\";\nimport { createOrganizationSchema } from \"./createOrganizationSchema.ts\";\n\nexport const updateOrganizationSchema = t.partial(createOrganizationSchema);\n\nexport type UpdateOrganization = Static<typeof updateOrganizationSchema>;\n","import { $inject, Alepha } from \"alepha\";\nimport { $logger } from \"alepha/logger\";\nimport { $repository, type Page } from \"alepha/orm\";\nimport {\n type OrganizationEntity,\n organizations,\n} from \"../entities/organizations.ts\";\nimport type { CreateOrganization } from \"../schemas/createOrganizationSchema.ts\";\nimport type { OrganizationQuery } from \"../schemas/organizationQuerySchema.ts\";\nimport type { UpdateOrganization } from \"../schemas/updateOrganizationSchema.ts\";\n\nexport class OrganizationService {\n protected readonly alepha = $inject(Alepha);\n protected readonly log = $logger();\n protected readonly repo = $repository(organizations);\n\n /**\n * Find organizations with pagination and filtering.\n */\n public async find(\n query: OrganizationQuery = {},\n ): Promise<Page<OrganizationEntity>> {\n query.sort ??= \"-createdAt\";\n\n const where = this.repo.createQueryWhere();\n\n if (query.name) {\n where.name = { like: `%${query.name}%` };\n }\n\n if (query.enabled !== undefined) {\n where.enabled = { eq: query.enabled };\n }\n\n return this.repo.paginate(query, { where }, { count: true });\n }\n\n /**\n * Get an organization by ID.\n */\n public async getById(id: string): Promise<OrganizationEntity> {\n return this.repo.getById(id);\n }\n\n /**\n * Get an organization by slug.\n */\n public async getBySlug(slug: string): Promise<OrganizationEntity> {\n return this.repo.getOne({ where: { slug: { eq: slug } } });\n }\n\n /**\n * Create a new organization.\n */\n public async create(data: CreateOrganization): Promise<OrganizationEntity> {\n return this.repo.create(data);\n }\n\n /**\n * Update an organization.\n */\n public async update(\n id: string,\n data: UpdateOrganization,\n ): Promise<OrganizationEntity> {\n return this.repo.updateById(id, data);\n }\n\n /**\n * Delete an organization.\n */\n public async delete(id: string): Promise<void> {\n await this.repo.deleteById(id);\n }\n}\n","import { $inject, t } from \"alepha\";\nimport { $secure } from \"alepha/security\";\nimport { $action, okSchema } from \"alepha/server\";\nimport { createOrganizationSchema } from \"../schemas/createOrganizationSchema.ts\";\nimport { organizationQuerySchema } from \"../schemas/organizationQuerySchema.ts\";\nimport { organizationResourceSchema } from \"../schemas/organizationResourceSchema.ts\";\nimport { updateOrganizationSchema } from \"../schemas/updateOrganizationSchema.ts\";\nimport { OrganizationService } from \"../services/OrganizationService.ts\";\n\nexport class AdminOrganizationController {\n protected readonly url = \"/organizations\";\n protected readonly group = \"admin:organizations\";\n protected readonly organizationService = $inject(OrganizationService);\n\n /**\n * Find organizations with pagination and filtering.\n */\n public readonly findOrganizations = $action({\n path: this.url,\n group: this.group,\n use: [$secure({ permissions: [\"admin:organization:read\"] })],\n description: \"Find organizations with pagination and filtering\",\n schema: {\n query: organizationQuerySchema,\n response: t.page(organizationResourceSchema),\n },\n handler: ({ query }) => this.organizationService.find(query),\n });\n\n /**\n * Get an organization by ID.\n */\n public readonly getOrganization = $action({\n path: `${this.url}/:id`,\n group: this.group,\n use: [$secure({ permissions: [\"admin:organization:read\"] })],\n description: \"Get an organization by ID\",\n schema: {\n params: t.object({\n id: t.uuid(),\n }),\n response: organizationResourceSchema,\n },\n handler: ({ params }) => this.organizationService.getById(params.id),\n });\n\n /**\n * Create a new organization.\n */\n public readonly createOrganization = $action({\n method: \"POST\",\n path: this.url,\n group: this.group,\n use: [$secure({ permissions: [\"admin:organization:create\"] })],\n description: \"Create a new organization\",\n schema: {\n body: createOrganizationSchema,\n response: organizationResourceSchema,\n },\n handler: ({ body }) => this.organizationService.create(body),\n });\n\n /**\n * Update an organization.\n */\n public readonly updateOrganization = $action({\n method: \"PATCH\",\n path: `${this.url}/:id`,\n group: this.group,\n use: [$secure({ permissions: [\"admin:organization:update\"] })],\n description: \"Update an organization\",\n schema: {\n params: t.object({\n id: t.uuid(),\n }),\n body: updateOrganizationSchema,\n response: organizationResourceSchema,\n },\n handler: ({ params, body }) =>\n this.organizationService.update(params.id, body),\n });\n\n /**\n * Delete an organization.\n */\n public readonly deleteOrganization = $action({\n method: \"DELETE\",\n path: `${this.url}/:id`,\n group: this.group,\n use: [$secure({ permissions: [\"admin:organization:delete\"] })],\n description: \"Delete an organization\",\n schema: {\n params: t.object({\n id: t.uuid(),\n }),\n response: okSchema,\n },\n handler: async ({ params }) => {\n await this.organizationService.delete(params.id);\n return { ok: true, id: params.id };\n },\n });\n}\n","import { $module } from \"alepha\";\nimport { AdminOrganizationController } from \"./controllers/AdminOrganizationController.ts\";\nimport { OrganizationService } from \"./services/OrganizationService.ts\";\n\n// ---------------------------------------------------------------------------------------------------------------------\n\nexport * from \"./controllers/AdminOrganizationController.ts\";\nexport * from \"./entities/organizations.ts\";\nexport * from \"./schemas/createOrganizationSchema.ts\";\nexport * from \"./schemas/organizationQuerySchema.ts\";\nexport * from \"./schemas/organizationResourceSchema.ts\";\nexport * from \"./schemas/updateOrganizationSchema.ts\";\nexport * from \"./services/OrganizationService.ts\";\n\n// ---------------------------------------------------------------------------------------------------------------------\n\n/**\n * Organization management for multi-tenancy.\n *\n * **Features:**\n * - Admin CRUD for organizations\n * - Organization scoping via `db.organization()` on entities\n * - User with no organization = god mode (sees all resources)\n * - User with an organization = scoped to that organization\n *\n * @module alepha.api.organizations\n */\nexport const AlephaApiOrganizations = $module({\n name: \"alepha.api.organizations\",\n services: [OrganizationService, AdminOrganizationController],\n});\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../../../src/api/organizations/schemas/createOrganizationSchema.ts","../../../src/api/organizations/schemas/organizationQuerySchema.ts","../../../src/api/organizations/entities/organizations.ts","../../../src/api/organizations/schemas/organizationResourceSchema.ts","../../../src/api/organizations/schemas/updateOrganizationSchema.ts","../../../src/api/organizations/services/OrganizationService.ts","../../../src/api/organizations/controllers/AdminOrganizationController.ts","../../../src/api/organizations/index.ts"],"sourcesContent":["import type { Static } from \"alepha\";\nimport { t } from \"alepha\";\n\nexport const createOrganizationSchema = t.object({\n name: t.text(),\n slug: t.text({ minLength: 2, maxLength: 100 }),\n enabled: t.optional(t.boolean()),\n});\n\nexport type CreateOrganization = Static<typeof createOrganizationSchema>;\n","import type { Static } from \"alepha\";\nimport { t } from \"alepha\";\nimport { pageQuerySchema } from \"alepha/orm\";\n\nexport const organizationQuerySchema = t.extend(pageQuerySchema, {\n name: t.optional(t.text({ description: \"Filter by name (partial match)\" })),\n enabled: t.optional(t.boolean({ description: \"Filter by enabled status\" })),\n});\n\nexport type OrganizationQuery = Static<typeof organizationQuerySchema>;\n","import type { Static } from \"alepha\";\nimport { t } from \"alepha\";\nimport { $entity, db } from \"alepha/orm\";\n\nexport const organizations = $entity({\n name: \"organizations\",\n schema: t.object({\n id: db.primaryKey(t.uuid()),\n version: db.version(),\n createdAt: db.createdAt(),\n updatedAt: db.updatedAt(),\n\n name: t.text(),\n slug: t.text({ minLength: 2, maxLength: 100 }),\n enabled: db.default(t.boolean(), true),\n }),\n indexes: [{ columns: [\"slug\"], unique: true }],\n});\n\nexport type OrganizationEntity = Static<typeof organizations.schema>;\n","import type { Static } from \"alepha\";\nimport { organizations } from \"../entities/organizations.ts\";\n\nexport const organizationResourceSchema = organizations.schema;\n\nexport type OrganizationResource = Static<typeof organizationResourceSchema>;\n","import type { Static } from \"alepha\";\nimport { t } from \"alepha\";\nimport { createOrganizationSchema } from \"./createOrganizationSchema.ts\";\n\nexport const updateOrganizationSchema = t.partial(createOrganizationSchema);\n\nexport type UpdateOrganization = Static<typeof updateOrganizationSchema>;\n","import { $inject, Alepha } from \"alepha\";\nimport { $logger } from \"alepha/logger\";\nimport { $repository, type Page } from \"alepha/orm\";\nimport {\n type OrganizationEntity,\n organizations,\n} from \"../entities/organizations.ts\";\nimport type { CreateOrganization } from \"../schemas/createOrganizationSchema.ts\";\nimport type { OrganizationQuery } from \"../schemas/organizationQuerySchema.ts\";\nimport type { UpdateOrganization } from \"../schemas/updateOrganizationSchema.ts\";\n\nexport class OrganizationService {\n protected readonly alepha = $inject(Alepha);\n protected readonly log = $logger();\n protected readonly repo = $repository(organizations);\n\n /**\n * Find organizations with pagination and filtering.\n */\n public async find(\n query: OrganizationQuery = {},\n ): Promise<Page<OrganizationEntity>> {\n query.sort ??= \"-createdAt\";\n\n const where = this.repo.createQueryWhere();\n\n if (query.name) {\n where.name = { like: `%${query.name}%` };\n }\n\n if (query.enabled !== undefined) {\n where.enabled = { eq: query.enabled };\n }\n\n return this.repo.paginate(query, { where }, { count: true });\n }\n\n /**\n * Get an organization by ID.\n */\n public async getById(id: string): Promise<OrganizationEntity> {\n return this.repo.getById(id);\n }\n\n /**\n * Get an organization by slug.\n */\n public async getBySlug(slug: string): Promise<OrganizationEntity> {\n return this.repo.getOne({ where: { slug: { eq: slug } } });\n }\n\n /**\n * Create a new organization.\n */\n public async create(data: CreateOrganization): Promise<OrganizationEntity> {\n return this.repo.create(data);\n }\n\n /**\n * Update an organization.\n */\n public async update(\n id: string,\n data: UpdateOrganization,\n ): Promise<OrganizationEntity> {\n return this.repo.updateById(id, data);\n }\n\n /**\n * Delete an organization.\n */\n public async delete(id: string): Promise<void> {\n await this.repo.deleteById(id);\n }\n}\n","import { $inject, t } from \"alepha\";\nimport { $secure } from \"alepha/security\";\nimport { $action, okSchema } from \"alepha/server\";\nimport { createOrganizationSchema } from \"../schemas/createOrganizationSchema.ts\";\nimport { organizationQuerySchema } from \"../schemas/organizationQuerySchema.ts\";\nimport { organizationResourceSchema } from \"../schemas/organizationResourceSchema.ts\";\nimport { updateOrganizationSchema } from \"../schemas/updateOrganizationSchema.ts\";\nimport { OrganizationService } from \"../services/OrganizationService.ts\";\n\nexport class AdminOrganizationController {\n protected readonly url = \"/organizations\";\n protected readonly group = \"admin:organizations\";\n protected readonly organizationService = $inject(OrganizationService);\n\n /**\n * Find organizations with pagination and filtering.\n */\n public readonly findOrganizations = $action({\n path: this.url,\n group: this.group,\n use: [$secure({ permissions: [\"admin:organization:read\"] })],\n description: \"Find organizations with pagination and filtering\",\n schema: {\n query: organizationQuerySchema,\n response: t.page(organizationResourceSchema),\n },\n handler: ({ query }) => this.organizationService.find(query),\n });\n\n /**\n * Get an organization by ID.\n */\n public readonly getOrganization = $action({\n path: `${this.url}/:id`,\n group: this.group,\n use: [$secure({ permissions: [\"admin:organization:read\"] })],\n description: \"Get an organization by ID\",\n schema: {\n params: t.object({\n id: t.uuid(),\n }),\n response: organizationResourceSchema,\n },\n handler: ({ params }) => this.organizationService.getById(params.id),\n });\n\n /**\n * Create a new organization.\n */\n public readonly createOrganization = $action({\n method: \"POST\",\n path: this.url,\n group: this.group,\n use: [$secure({ permissions: [\"admin:organization:create\"] })],\n description: \"Create a new organization\",\n schema: {\n body: createOrganizationSchema,\n response: organizationResourceSchema,\n },\n handler: ({ body }) => this.organizationService.create(body),\n });\n\n /**\n * Update an organization.\n */\n public readonly updateOrganization = $action({\n method: \"PATCH\",\n path: `${this.url}/:id`,\n group: this.group,\n use: [$secure({ permissions: [\"admin:organization:update\"] })],\n description: \"Update an organization\",\n schema: {\n params: t.object({\n id: t.uuid(),\n }),\n body: updateOrganizationSchema,\n response: organizationResourceSchema,\n },\n handler: ({ params, body }) =>\n this.organizationService.update(params.id, body),\n });\n\n /**\n * Delete an organization.\n */\n public readonly deleteOrganization = $action({\n method: \"DELETE\",\n path: `${this.url}/:id`,\n group: this.group,\n use: [$secure({ permissions: [\"admin:organization:delete\"] })],\n description: \"Delete an organization\",\n schema: {\n params: t.object({\n id: t.uuid(),\n }),\n response: okSchema,\n },\n handler: async ({ params }) => {\n await this.organizationService.delete(params.id);\n return { ok: true, id: params.id };\n },\n });\n}\n","import { $module } from \"alepha\";\nimport { AdminOrganizationController } from \"./controllers/AdminOrganizationController.ts\";\nimport { OrganizationService } from \"./services/OrganizationService.ts\";\n\n// ---------------------------------------------------------------------------------------------------------------------\n\nexport * from \"./controllers/AdminOrganizationController.ts\";\nexport * from \"./entities/organizations.ts\";\nexport * from \"./schemas/createOrganizationSchema.ts\";\nexport * from \"./schemas/organizationQuerySchema.ts\";\nexport * from \"./schemas/organizationResourceSchema.ts\";\nexport * from \"./schemas/updateOrganizationSchema.ts\";\nexport * from \"./services/OrganizationService.ts\";\n\n// ---------------------------------------------------------------------------------------------------------------------\n\n/**\n * Organization management for multi-tenancy.\n *\n * **Features:**\n * - Admin CRUD for organizations\n * - Organization scoping via `db.organization()` on entities\n * - User with no organization = god mode (sees all resources)\n * - User with an organization = scoped to that organization\n *\n * @module alepha.api.organizations\n */\nexport const AlephaApiOrganizations = $module({\n name: \"alepha.api.organizations\",\n services: [OrganizationService, AdminOrganizationController],\n});\n"],"mappings":"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"}
|