@shaferllc/keel 0.59.0 → 0.68.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (166) hide show
  1. package/AGENTS.md +167 -0
  2. package/README.md +30 -1
  3. package/bin/keel-mcp.mjs +9 -0
  4. package/dist/core/application.d.ts +5 -5
  5. package/dist/core/application.js +14 -2
  6. package/dist/core/auth.d.ts +47 -0
  7. package/dist/core/auth.js +77 -0
  8. package/dist/core/authorization.d.ts +9 -1
  9. package/dist/core/authorization.js +22 -2
  10. package/dist/core/cache.d.ts +82 -5
  11. package/dist/core/cache.js +181 -23
  12. package/dist/core/cli/stubs.d.ts +12 -0
  13. package/dist/core/cli/stubs.js +120 -0
  14. package/dist/core/container.d.ts +20 -0
  15. package/dist/core/container.js +52 -0
  16. package/dist/core/cors.d.ts +29 -0
  17. package/dist/core/cors.js +72 -0
  18. package/dist/core/crypto.d.ts +40 -4
  19. package/dist/core/crypto.js +66 -6
  20. package/dist/core/csrf.d.ts +25 -0
  21. package/dist/core/csrf.js +78 -0
  22. package/dist/core/database.d.ts +49 -4
  23. package/dist/core/database.js +89 -21
  24. package/dist/core/events.d.ts +129 -5
  25. package/dist/core/events.js +165 -7
  26. package/dist/core/health.d.ts +141 -0
  27. package/dist/core/health.js +226 -0
  28. package/dist/core/helpers.d.ts +15 -3
  29. package/dist/core/helpers.js +23 -3
  30. package/dist/core/index.d.ts +33 -18
  31. package/dist/core/index.js +16 -8
  32. package/dist/core/lock.d.ts +139 -0
  33. package/dist/core/lock.js +215 -0
  34. package/dist/core/logger.d.ts +82 -4
  35. package/dist/core/logger.js +141 -23
  36. package/dist/core/mail.d.ts +128 -7
  37. package/dist/core/mail.js +264 -16
  38. package/dist/core/model.d.ts +2 -0
  39. package/dist/core/model.js +16 -14
  40. package/dist/core/provider.d.ts +7 -0
  41. package/dist/core/provider.js +7 -0
  42. package/dist/core/queue.d.ts +134 -9
  43. package/dist/core/queue.js +304 -14
  44. package/dist/core/rate-limit.js +3 -0
  45. package/dist/core/relations.js +13 -13
  46. package/dist/core/request.d.ts +26 -0
  47. package/dist/core/request.js +77 -0
  48. package/dist/core/shield.d.ts +39 -0
  49. package/dist/core/shield.js +60 -0
  50. package/dist/core/social.d.ts +173 -0
  51. package/dist/core/social.js +337 -0
  52. package/dist/core/storage.d.ts +159 -6
  53. package/dist/core/storage.js +287 -7
  54. package/dist/core/tokens.d.ts +74 -0
  55. package/dist/core/tokens.js +155 -0
  56. package/dist/db/d1.d.ts +32 -0
  57. package/dist/db/d1.js +26 -0
  58. package/dist/db/libsql.d.ts +29 -0
  59. package/dist/db/libsql.js +32 -0
  60. package/dist/db/pg.d.ts +29 -0
  61. package/dist/db/pg.js +33 -0
  62. package/dist/mcp/server.d.ts +19 -0
  63. package/dist/mcp/server.js +355 -0
  64. package/docs/ai-manifest.json +2472 -0
  65. package/docs/ai.md +128 -0
  66. package/docs/architecture.md +331 -0
  67. package/docs/authentication.md +453 -0
  68. package/docs/authorization.md +167 -0
  69. package/docs/broadcasting.md +137 -0
  70. package/docs/broker.md +500 -0
  71. package/docs/cache.md +558 -0
  72. package/docs/configuration.md +311 -0
  73. package/docs/console.md +356 -0
  74. package/docs/container.md +467 -0
  75. package/docs/controllers.md +265 -0
  76. package/docs/cors.md +51 -0
  77. package/docs/database.md +530 -0
  78. package/docs/debugging.md +129 -0
  79. package/docs/decorators.md +127 -0
  80. package/docs/errors.md +395 -0
  81. package/docs/events.md +496 -0
  82. package/docs/examples/architecture-app.ts +27 -0
  83. package/docs/examples/authentication.ts +61 -0
  84. package/docs/examples/authorization.ts +79 -0
  85. package/docs/examples/broadcasting.ts +60 -0
  86. package/docs/examples/broker-cache-validate.ts +34 -0
  87. package/docs/examples/broker-fault-tolerance.ts +29 -0
  88. package/docs/examples/broker-middleware.ts +27 -0
  89. package/docs/examples/broker.ts +203 -0
  90. package/docs/examples/cache.ts +222 -0
  91. package/docs/examples/configuration.ts +81 -0
  92. package/docs/examples/container.ts +134 -0
  93. package/docs/examples/controllers.ts +86 -0
  94. package/docs/examples/database.ts +118 -0
  95. package/docs/examples/debugging.ts +41 -0
  96. package/docs/examples/decorators.ts +40 -0
  97. package/docs/examples/errors.ts +121 -0
  98. package/docs/examples/events.ts +204 -0
  99. package/docs/examples/factories.ts +84 -0
  100. package/docs/examples/hashing.ts +71 -0
  101. package/docs/examples/health.ts +94 -0
  102. package/docs/examples/helpers.ts +171 -0
  103. package/docs/examples/hooks.ts +54 -0
  104. package/docs/examples/inertia.ts +81 -0
  105. package/docs/examples/locks.ts +120 -0
  106. package/docs/examples/logger.ts +92 -0
  107. package/docs/examples/mail.ts +160 -0
  108. package/docs/examples/middleware.ts +119 -0
  109. package/docs/examples/migrations.ts +126 -0
  110. package/docs/examples/models.ts +239 -0
  111. package/docs/examples/notification.ts +124 -0
  112. package/docs/examples/providers.ts +123 -0
  113. package/docs/examples/queues.ts +254 -0
  114. package/docs/examples/rate-limiting.ts +42 -0
  115. package/docs/examples/redis.ts +99 -0
  116. package/docs/examples/request-response.ts +197 -0
  117. package/docs/examples/routing.ts +186 -0
  118. package/docs/examples/scheduling.ts +62 -0
  119. package/docs/examples/sessions.ts +102 -0
  120. package/docs/examples/static-files.ts +63 -0
  121. package/docs/examples/storage.ts +132 -0
  122. package/docs/examples/templates.ts +58 -0
  123. package/docs/examples/testing.ts +66 -0
  124. package/docs/examples/transformer.ts +141 -0
  125. package/docs/examples/transformers.ts +49 -0
  126. package/docs/examples/url-builder.ts +86 -0
  127. package/docs/examples/validation.ts +102 -0
  128. package/docs/examples/views.tsx +62 -0
  129. package/docs/examples/vite.ts +106 -0
  130. package/docs/factories.md +166 -0
  131. package/docs/getting-started.md +290 -0
  132. package/docs/hashing.md +259 -0
  133. package/docs/health.md +225 -0
  134. package/docs/helpers.md +347 -0
  135. package/docs/hono.md +186 -0
  136. package/docs/hooks.md +118 -0
  137. package/docs/inertia.md +241 -0
  138. package/docs/locks.md +323 -0
  139. package/docs/logger.md +290 -0
  140. package/docs/mail.md +678 -0
  141. package/docs/middleware.md +425 -0
  142. package/docs/migrations.md +476 -0
  143. package/docs/models.md +810 -0
  144. package/docs/notifications.md +474 -0
  145. package/docs/providers.md +363 -0
  146. package/docs/queues.md +679 -0
  147. package/docs/rate-limiting.md +155 -0
  148. package/docs/redis.md +178 -0
  149. package/docs/request-response.md +953 -0
  150. package/docs/routing.md +804 -0
  151. package/docs/scheduling.md +110 -0
  152. package/docs/security.md +85 -0
  153. package/docs/sessions.md +354 -0
  154. package/docs/social-auth.md +174 -0
  155. package/docs/static-files.md +211 -0
  156. package/docs/storage.md +450 -0
  157. package/docs/templates.md +315 -0
  158. package/docs/testing.md +125 -0
  159. package/docs/transformers.md +381 -0
  160. package/docs/url-builder.md +295 -0
  161. package/docs/validation.md +288 -0
  162. package/docs/views.md +267 -0
  163. package/docs/vite.md +434 -0
  164. package/llms-full.txt +17694 -0
  165. package/llms.txt +116 -0
  166. package/package.json +38 -7
@@ -4,35 +4,46 @@ export type { Token, Constructor, Factory } from "./container.js";
4
4
  export { Application } from "./application.js";
5
5
  export type { BootOptions, LifecycleHook, Configurator } from "./application.js";
6
6
  export { Config, env } from "./config.js";
7
- export { app, config, view, bind, singleton, instance, make, bound, events, emit, listen, cache, logger, onReady, onShutdown, terminate, } from "./helpers.js";
7
+ export { app, config, view, bind, singleton, instance, make, bound, alias, swap, restore, events, emit, listen, cache, logger, onReady, onShutdown, terminate, } from "./helpers.js";
8
8
  export { Logger } from "./logger.js";
9
- export type { LogLevel, LoggerOptions } from "./logger.js";
9
+ export { consoleSink, MemorySink, setLogger, namedLogger } from "./logger.js";
10
+ export type { LogLevel, LoggerOptions, LogRecord, Sink, RedactOptions } from "./logger.js";
10
11
  export { requestLogger, requestLog } from "./request-logger.js";
11
12
  export type { RequestLoggerOptions } from "./request-logger.js";
12
- export { Events } from "./events.js";
13
- export type { Listener } from "./events.js";
13
+ export { Events, EventBuffer } from "./events.js";
14
+ export type { Listener, AnyListener, ErrorHandler, EventsList, EventName, PayloadOf, RecordedEvent, } from "./events.js";
14
15
  export { Cache, MemoryStore } from "./cache.js";
15
- export type { CacheStore } from "./cache.js";
16
+ export type { CacheStore, RememberOptions, PutOptions } from "./cache.js";
16
17
  export { serveStatic } from "./static.js";
17
18
  export type { StaticOptions } from "./static.js";
18
- export { Storage, MemoryDisk, storage, setDisk } from "./storage.js";
19
- export type { Disk, Contents } from "./storage.js";
19
+ export { lock, restoreLock, setLockStore, getLockStore, Lock, MemoryLockStore, LockNotHeldError, } from "./lock.js";
20
+ export type { LockStore, AcquireOptions } from "./lock.js";
21
+ export { health, healthCheck, check, Result, BaseCheck, HealthChecks, DatabaseCheck, RedisCheck, CacheCheck, } from "./health.js";
22
+ export type { HealthStatus, HealthReport, CheckReport, HealthCheckOptions, } from "./health.js";
23
+ export { Storage, FakeStorage, MemoryDisk, storage, setDisk, fakeDisk, restoreDisk, serveStorage, signStorageUrl, verifyStorageUrl, contentTypeFor, } from "./storage.js";
24
+ export type { Disk, Contents, FileVisibility, WriteOptions, FileMetadata, SignedFileOptions, SignedUploadOptions, ServeStorageOptions, } from "./storage.js";
20
25
  export { dump, dd } from "./debug.js";
21
26
  export { hash, encryption, jwt } from "./crypto.js";
22
- export type { JwtPayload, JwtSignOptions, JwtVerifyOptions } from "./crypto.js";
27
+ export type { JwtPayload, JwtSignOptions, JwtVerifyOptions, EncryptOptions } from "./crypto.js";
23
28
  export { rateLimiter } from "./rate-limit.js";
24
29
  export type { RateLimiterOptions } from "./rate-limit.js";
25
- export { db, setConnection, QueryBuilder } from "./database.js";
26
- export type { Connection, WriteResult, Row, Dialect, Operator, Paginated } from "./database.js";
30
+ export { cors } from "./cors.js";
31
+ export type { CorsOptions } from "./cors.js";
32
+ export { securityHeaders } from "./shield.js";
33
+ export type { SecurityHeadersOptions, HstsOptions } from "./shield.js";
34
+ export { csrf, csrfToken, csrfField } from "./csrf.js";
35
+ export type { CsrfOptions } from "./csrf.js";
36
+ export { db, connection, setConnection, addConnection, setDefaultConnection, connectionNames, clearConnections, QueryBuilder, } from "./database.js";
37
+ export type { Connection, ConnectionHandle, WriteResult, Row, Dialect, Operator, Paginated } from "./database.js";
27
38
  export { Model } from "./model.js";
28
39
  export type { CastType, Casts } from "./casts.js";
29
40
  export { Relation, HasOne, HasMany, BelongsTo, BelongsToMany } from "./relations.js";
30
41
  export { Faker, Factory as ModelFactory, factory, Seeder, seed } from "./factory.js";
31
42
  export type { Definition } from "./factory.js";
32
- export { Mailer, PendingMail, ArrayTransport, LogTransport, fetchTransport, mail, setMailer, getMailer, } from "./mail.js";
33
- export type { Message, Transport, MailerOptions, FetchTransportOptions } from "./mail.js";
34
- export { Job, Queue, SyncDriver, MemoryDriver, dispatch, work, setQueue, getQueue, } from "./queue.js";
35
- export type { Dispatchable, JobOptions, QueueDriver, Drainable, QueuedJob } from "./queue.js";
43
+ export { Mailer, FakeMailer, PendingMail, BaseMail, SendMailJob, ArrayTransport, LogTransport, fetchTransport, mail, mailer, send, sendLater, setMailer, getMailer, fakeMail, restoreMail, } from "./mail.js";
44
+ export type { Message, Transport, MailerOptions, FetchTransportOptions, Attachment, RecordedMail, } from "./mail.js";
45
+ export { Job, Queue, FakeQueue, SyncDriver, MemoryDriver, dispatch, work, setQueue, getQueue, fakeQueue, restoreQueue, exponentialBackoff, linearBackoff, fixedBackoff, noBackoff, } from "./queue.js";
46
+ export type { Dispatchable, JobOptions, JobContext, JobClass, QueueDriver, Drainable, QueuedJob, FailedJob, Backoff, } from "./queue.js";
36
47
  export { Scheduler, ScheduledTask, scheduler, setScheduler, schedule, cronMatches } from "./scheduler.js";
37
48
  export { MemoryBroadcaster, broadcast, setBroadcaster, getBroadcaster, channelAuth, authorizeChannel, clearChannels, } from "./broadcasting.js";
38
49
  export type { Broadcaster, Subscriber, ChannelAuthorizer } from "./broadcasting.js";
@@ -63,10 +74,14 @@ export { validate, validateRequest, validated } from "./validation.js";
63
74
  export type { Schema, RequestSchemas } from "./validation.js";
64
75
  export { Session, session, sessionMiddleware } from "./session.js";
65
76
  export type { SessionOptions } from "./session.js";
66
- export { Auth, auth, authGuard, bearerAuth, setUserProvider } from "./auth.js";
67
- export type { UserProvider } from "./auth.js";
68
- export { define, policy, gateBefore, setUserResolver, clearAuthorization, can, cannot, canFor, authorize, authorizeFor, } from "./authorization.js";
69
- export type { GateCallback, BeforeCallback } from "./authorization.js";
77
+ export { Auth, auth, authGuard, bearerAuth, basicAuth, tokenAuth, token, tokenCan, setUserProvider } from "./auth.js";
78
+ export type { UserProvider, BasicVerifier } from "./auth.js";
79
+ export { createToken, verifyToken, revokeToken, revokeTokens, listTokens, tokenAllows, tokenDenies, setTokensTable, } from "./tokens.js";
80
+ export type { AccessToken, CreateTokenOptions, IssuedToken } from "./tokens.js";
81
+ export { social, github, google, discord, oauthDriver, oauthState, OAuthDriver, OAuthError, twitter, oauth1Driver, oauth1Signature, OAuth1Driver, } from "./social.js";
82
+ export type { SocialUser, OAuthToken, OAuthConfig, ProviderSpec, RedirectOptions, OAuth1Config, OAuth1Token, OAuth1ProviderSpec, } from "./social.js";
83
+ export { define, policy, gateBefore, gateAfter, setUserResolver, clearAuthorization, can, cannot, canFor, authorize, authorizeFor, } from "./authorization.js";
84
+ export type { GateCallback, BeforeCallback, AfterCallback } from "./authorization.js";
70
85
  export { Transformer } from "./transformer.js";
71
86
  export type { Attributes, DocumentOptions } from "./transformer.js";
72
87
  export { Broker, Service, LocalTransporter, ServiceNotFoundError, RequestTimeoutError, broker, setBroker, } from "./broker.js";
@@ -2,22 +2,28 @@
2
2
  export { Container } from "./container.js";
3
3
  export { Application } from "./application.js";
4
4
  export { Config, env } from "./config.js";
5
- export { app, config, view, bind, singleton, instance, make, bound, events, emit, listen, cache, logger, onReady, onShutdown, terminate, } from "./helpers.js";
5
+ export { app, config, view, bind, singleton, instance, make, bound, alias, swap, restore, events, emit, listen, cache, logger, onReady, onShutdown, terminate, } from "./helpers.js";
6
6
  export { Logger } from "./logger.js";
7
+ export { consoleSink, MemorySink, setLogger, namedLogger } from "./logger.js";
7
8
  export { requestLogger, requestLog } from "./request-logger.js";
8
- export { Events } from "./events.js";
9
+ export { Events, EventBuffer } from "./events.js";
9
10
  export { Cache, MemoryStore } from "./cache.js";
10
11
  export { serveStatic } from "./static.js";
11
- export { Storage, MemoryDisk, storage, setDisk } from "./storage.js";
12
+ export { lock, restoreLock, setLockStore, getLockStore, Lock, MemoryLockStore, LockNotHeldError, } from "./lock.js";
13
+ export { health, healthCheck, check, Result, BaseCheck, HealthChecks, DatabaseCheck, RedisCheck, CacheCheck, } from "./health.js";
14
+ export { Storage, FakeStorage, MemoryDisk, storage, setDisk, fakeDisk, restoreDisk, serveStorage, signStorageUrl, verifyStorageUrl, contentTypeFor, } from "./storage.js";
12
15
  export { dump, dd } from "./debug.js";
13
16
  export { hash, encryption, jwt } from "./crypto.js";
14
17
  export { rateLimiter } from "./rate-limit.js";
15
- export { db, setConnection, QueryBuilder } from "./database.js";
18
+ export { cors } from "./cors.js";
19
+ export { securityHeaders } from "./shield.js";
20
+ export { csrf, csrfToken, csrfField } from "./csrf.js";
21
+ export { db, connection, setConnection, addConnection, setDefaultConnection, connectionNames, clearConnections, QueryBuilder, } from "./database.js";
16
22
  export { Model } from "./model.js";
17
23
  export { Relation, HasOne, HasMany, BelongsTo, BelongsToMany } from "./relations.js";
18
24
  export { Faker, Factory as ModelFactory, factory, Seeder, seed } from "./factory.js";
19
- export { Mailer, PendingMail, ArrayTransport, LogTransport, fetchTransport, mail, setMailer, getMailer, } from "./mail.js";
20
- export { Job, Queue, SyncDriver, MemoryDriver, dispatch, work, setQueue, getQueue, } from "./queue.js";
25
+ export { Mailer, FakeMailer, PendingMail, BaseMail, SendMailJob, ArrayTransport, LogTransport, fetchTransport, mail, mailer, send, sendLater, setMailer, getMailer, fakeMail, restoreMail, } from "./mail.js";
26
+ export { Job, Queue, FakeQueue, SyncDriver, MemoryDriver, dispatch, work, setQueue, getQueue, fakeQueue, restoreQueue, exponentialBackoff, linearBackoff, fixedBackoff, noBackoff, } from "./queue.js";
21
27
  export { Scheduler, ScheduledTask, scheduler, setScheduler, schedule, cronMatches } from "./scheduler.js";
22
28
  export { MemoryBroadcaster, broadcast, setBroadcaster, getBroadcaster, channelAuth, authorizeChannel, clearChannels, } from "./broadcasting.js";
23
29
  export { Redis, MemoryRedis, redis, setRedis, redisStore } from "./redis.js";
@@ -35,8 +41,10 @@ export { TestClient, TestResponse, testClient } from "./testing.js";
35
41
  export { HttpException, BadRequestException, UnauthorizedException, PaymentRequiredException, ForbiddenException, NotFoundException, MethodNotAllowedException, NotAcceptableException, RequestTimeoutException, ConflictException, LengthRequiredException, ValidationException, TooManyRequestsException, ServerErrorException, NotImplementedException, BadGatewayException, ServiceUnavailableException, createError, STATUS_TEXT, } from "./exceptions.js";
36
42
  export { validate, validateRequest, validated } from "./validation.js";
37
43
  export { Session, session, sessionMiddleware } from "./session.js";
38
- export { Auth, auth, authGuard, bearerAuth, setUserProvider } from "./auth.js";
39
- export { define, policy, gateBefore, setUserResolver, clearAuthorization, can, cannot, canFor, authorize, authorizeFor, } from "./authorization.js";
44
+ export { Auth, auth, authGuard, bearerAuth, basicAuth, tokenAuth, token, tokenCan, setUserProvider } from "./auth.js";
45
+ export { createToken, verifyToken, revokeToken, revokeTokens, listTokens, tokenAllows, tokenDenies, setTokensTable, } from "./tokens.js";
46
+ export { social, github, google, discord, oauthDriver, oauthState, OAuthDriver, OAuthError, twitter, oauth1Driver, oauth1Signature, OAuth1Driver, } from "./social.js";
47
+ export { define, policy, gateBefore, gateAfter, setUserResolver, clearAuthorization, can, cannot, canFor, authorize, authorizeFor, } from "./authorization.js";
40
48
  export { Transformer } from "./transformer.js";
41
49
  export { Broker, Service, LocalTransporter, ServiceNotFoundError, RequestTimeoutError, broker, setBroker, } from "./broker.js";
42
50
  export { Vite, viteTags, viteAsset, viteReactRefresh } from "./vite.js";
@@ -0,0 +1,139 @@
1
+ /**
2
+ * Distributed locks — "only one of you may do this at a time", across processes
3
+ * and across nodes. The counterpart to the cache's stampede protection: that
4
+ * collapses concurrent work *inside one isolate*, this one coordinates work
5
+ * *between* them.
6
+ *
7
+ * const [ran] = await lock("invoice:42").run(async () => charge(invoice));
8
+ * if (!ran) return; // someone else is already charging it
9
+ *
10
+ * Like every other backend in keel, the store is a small pluggable seam and the
11
+ * core imports no driver — `MemoryLockStore` is the built-in default (per
12
+ * isolate, so it's for tests and single-process apps); the locks guide has Redis
13
+ * and database recipes for the real thing.
14
+ *
15
+ * ## Two safety properties, both easy to get wrong
16
+ *
17
+ * **Ownership.** Every acquisition mints a random owner token, and release and
18
+ * extend only succeed for the owner. Without that, a lock whose TTL expired
19
+ * mid-work gets picked up by process B, and process A's `release()` — arriving
20
+ * late — would delete *B's* lock, letting a third process in. A store must
21
+ * compare-and-delete, not just delete.
22
+ *
23
+ * **TTL.** A lock always expires. If a holder crashes, the lock must not be held
24
+ * forever, so there's no "hold until released" mode. Pick a TTL longer than the
25
+ * work; if the work might outrun it, `extend()` from inside.
26
+ */
27
+ /**
28
+ * The bridge to a lock backend.
29
+ *
30
+ * Every method is keyed by `owner` for the reason above: implementations MUST
31
+ * make `acquire` atomic (set-if-absent) and `release`/`extend` conditional on
32
+ * the owner matching. A store that can't do that isn't a lock.
33
+ */
34
+ export interface LockStore {
35
+ /** Set the key if absent. Returns whether this owner got it. Must be atomic. */
36
+ acquire(key: string, owner: string, ttlMs: number): Promise<boolean>;
37
+ /** Delete the key only if `owner` still holds it. Returns whether it did. */
38
+ release(key: string, owner: string): Promise<boolean>;
39
+ /** Push the expiry out, only if `owner` still holds it. */
40
+ extend(key: string, owner: string, ttlMs: number): Promise<boolean>;
41
+ /** Whether anyone currently holds the key. */
42
+ isLocked(key: string): Promise<boolean>;
43
+ /** Milliseconds until the key expires, or null if nobody holds it. */
44
+ remainingTime(key: string): Promise<number | null>;
45
+ }
46
+ /**
47
+ * An in-memory `LockStore` — the default. Per-isolate, so it coordinates
48
+ * *within* one process and nothing more: perfect for tests and single-process
49
+ * apps, useless across a cluster. Point `setLockStore()` at Redis for that.
50
+ */
51
+ export declare class MemoryLockStore implements LockStore {
52
+ private entries;
53
+ /** The live entry for a key, dropping it if it has expired. */
54
+ private live;
55
+ acquire(key: string, owner: string, ttlMs: number): Promise<boolean>;
56
+ release(key: string, owner: string): Promise<boolean>;
57
+ extend(key: string, owner: string, ttlMs: number): Promise<boolean>;
58
+ isLocked(key: string): Promise<boolean>;
59
+ remainingTime(key: string): Promise<number | null>;
60
+ }
61
+ export interface AcquireOptions {
62
+ /**
63
+ * Give up after this many milliseconds of waiting. Default: 0 — don't wait at
64
+ * all, fail immediately if the lock is held.
65
+ */
66
+ timeout?: number;
67
+ /** Milliseconds between attempts while waiting. Default: 50. */
68
+ retryDelay?: number;
69
+ }
70
+ /** A lock that has expired or was never held. */
71
+ export declare class LockNotHeldError extends Error {
72
+ constructor(key: string);
73
+ }
74
+ export declare class Lock {
75
+ readonly key: string;
76
+ /** How long the lock is held before it expires on its own. Milliseconds. */
77
+ readonly ttlMs: number;
78
+ private store;
79
+ /** Non-null only while this instance holds the lock. */
80
+ private owner?;
81
+ constructor(key: string,
82
+ /** How long the lock is held before it expires on its own. Milliseconds. */
83
+ ttlMs: number, store: LockStore, owner?: string);
84
+ /**
85
+ * Take the lock, run `fn`, and always give it back — the form you want almost
86
+ * every time, because the `finally` is what stops a throwing callback from
87
+ * leaving the lock held until its TTL runs out.
88
+ *
89
+ * Returns `[ran, result]`: `ran` is false if someone else holds it, in which
90
+ * case `fn` never ran.
91
+ *
92
+ * const [ran, invoice] = await lock("invoice:42").run(() => charge(id));
93
+ * if (!ran) return; // another worker is on it
94
+ */
95
+ run<T>(fn: () => Promise<T> | T, options?: AcquireOptions): Promise<[boolean, T | undefined]>;
96
+ /** `run()`, but never waits: if the lock is held, give up at once. */
97
+ runImmediately<T>(fn: () => Promise<T> | T): Promise<[boolean, T | undefined]>;
98
+ /**
99
+ * Take the lock, waiting up to `timeout` for it. Returns whether we got it.
100
+ *
101
+ * Prefer `run()` — with a bare `acquire()` you own the `try/finally`, and a
102
+ * throw between here and `release()` leaks the lock for the rest of its TTL.
103
+ */
104
+ acquire(options?: AcquireOptions): Promise<boolean>;
105
+ /** Take the lock only if it's free right now. */
106
+ acquireImmediately(): Promise<boolean>;
107
+ /** Give the lock back. A no-op if we no longer hold it (it may have expired). */
108
+ release(): Promise<boolean>;
109
+ /**
110
+ * Push the expiry out — for work that might outrun the TTL. Throws
111
+ * `LockNotHeldError` if we've already lost it, because the alternative (a
112
+ * silent no-op) would let you carry on believing you hold a lock you don't.
113
+ */
114
+ extend(ttlMs?: number): Promise<void>;
115
+ /** Whether *anyone* holds this key — not necessarily us. */
116
+ isLocked(): Promise<boolean>;
117
+ /** Whether we held this lock but no longer do. */
118
+ isExpired(): Promise<boolean>;
119
+ /** Milliseconds until the lock expires, or null if nobody holds it. */
120
+ getRemainingTime(): Promise<number | null>;
121
+ /**
122
+ * Freeze the lock (key, TTL, owner token) to a string, so another process can
123
+ * `restoreLock()` it and release or extend the *same* lock — the handoff you
124
+ * need when one process takes the lock and another finishes the work.
125
+ */
126
+ serialize(): string;
127
+ }
128
+ /** Register the lock store used by `lock()`. */
129
+ export declare function setLockStore(next: LockStore): LockStore;
130
+ /** The active lock store. */
131
+ export declare function getLockStore(): LockStore;
132
+ /**
133
+ * A lock on `key`, held for `ttlMs` once acquired (default 30s).
134
+ *
135
+ * const [ran] = await lock("invoice:42").run(() => charge(invoice));
136
+ */
137
+ export declare function lock(key: string, ttlMs?: number): Lock;
138
+ /** Rebuild a lock from `serialize()` — same key, same TTL, same owner token. */
139
+ export declare function restoreLock(serialized: string): Lock;
@@ -0,0 +1,215 @@
1
+ /**
2
+ * Distributed locks — "only one of you may do this at a time", across processes
3
+ * and across nodes. The counterpart to the cache's stampede protection: that
4
+ * collapses concurrent work *inside one isolate*, this one coordinates work
5
+ * *between* them.
6
+ *
7
+ * const [ran] = await lock("invoice:42").run(async () => charge(invoice));
8
+ * if (!ran) return; // someone else is already charging it
9
+ *
10
+ * Like every other backend in keel, the store is a small pluggable seam and the
11
+ * core imports no driver — `MemoryLockStore` is the built-in default (per
12
+ * isolate, so it's for tests and single-process apps); the locks guide has Redis
13
+ * and database recipes for the real thing.
14
+ *
15
+ * ## Two safety properties, both easy to get wrong
16
+ *
17
+ * **Ownership.** Every acquisition mints a random owner token, and release and
18
+ * extend only succeed for the owner. Without that, a lock whose TTL expired
19
+ * mid-work gets picked up by process B, and process A's `release()` — arriving
20
+ * late — would delete *B's* lock, letting a third process in. A store must
21
+ * compare-and-delete, not just delete.
22
+ *
23
+ * **TTL.** A lock always expires. If a holder crashes, the lock must not be held
24
+ * forever, so there's no "hold until released" mode. Pick a TTL longer than the
25
+ * work; if the work might outrun it, `extend()` from inside.
26
+ */
27
+ /**
28
+ * An in-memory `LockStore` — the default. Per-isolate, so it coordinates
29
+ * *within* one process and nothing more: perfect for tests and single-process
30
+ * apps, useless across a cluster. Point `setLockStore()` at Redis for that.
31
+ */
32
+ export class MemoryLockStore {
33
+ entries = new Map();
34
+ /** The live entry for a key, dropping it if it has expired. */
35
+ live(key) {
36
+ const entry = this.entries.get(key);
37
+ if (!entry)
38
+ return undefined;
39
+ if (entry.expiresAt <= Date.now()) {
40
+ this.entries.delete(key);
41
+ return undefined;
42
+ }
43
+ return entry;
44
+ }
45
+ async acquire(key, owner, ttlMs) {
46
+ if (this.live(key))
47
+ return false;
48
+ this.entries.set(key, { owner, expiresAt: Date.now() + ttlMs });
49
+ return true;
50
+ }
51
+ async release(key, owner) {
52
+ const entry = this.live(key);
53
+ if (!entry || entry.owner !== owner)
54
+ return false;
55
+ this.entries.delete(key);
56
+ return true;
57
+ }
58
+ async extend(key, owner, ttlMs) {
59
+ const entry = this.live(key);
60
+ if (!entry || entry.owner !== owner)
61
+ return false;
62
+ entry.expiresAt = Date.now() + ttlMs;
63
+ return true;
64
+ }
65
+ async isLocked(key) {
66
+ return this.live(key) !== undefined;
67
+ }
68
+ async remainingTime(key) {
69
+ const entry = this.live(key);
70
+ return entry ? entry.expiresAt - Date.now() : null;
71
+ }
72
+ }
73
+ /** A lock that has expired or was never held. */
74
+ export class LockNotHeldError extends Error {
75
+ constructor(key) {
76
+ super(`The lock "${key}" is not held by you — it expired or was released.`);
77
+ this.name = "LockNotHeldError";
78
+ }
79
+ }
80
+ const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
81
+ /** A random owner token. Web Crypto, so it works on Node and the edge. */
82
+ function mintOwner() {
83
+ return crypto.randomUUID();
84
+ }
85
+ /* ---------------------------------- lock ---------------------------------- */
86
+ export class Lock {
87
+ key;
88
+ ttlMs;
89
+ store;
90
+ /** Non-null only while this instance holds the lock. */
91
+ owner;
92
+ constructor(key,
93
+ /** How long the lock is held before it expires on its own. Milliseconds. */
94
+ ttlMs, store, owner) {
95
+ this.key = key;
96
+ this.ttlMs = ttlMs;
97
+ this.store = store;
98
+ this.owner = owner;
99
+ }
100
+ /**
101
+ * Take the lock, run `fn`, and always give it back — the form you want almost
102
+ * every time, because the `finally` is what stops a throwing callback from
103
+ * leaving the lock held until its TTL runs out.
104
+ *
105
+ * Returns `[ran, result]`: `ran` is false if someone else holds it, in which
106
+ * case `fn` never ran.
107
+ *
108
+ * const [ran, invoice] = await lock("invoice:42").run(() => charge(id));
109
+ * if (!ran) return; // another worker is on it
110
+ */
111
+ async run(fn, options = {}) {
112
+ if (!(await this.acquire(options)))
113
+ return [false, undefined];
114
+ try {
115
+ return [true, await fn()];
116
+ }
117
+ finally {
118
+ await this.release();
119
+ }
120
+ }
121
+ /** `run()`, but never waits: if the lock is held, give up at once. */
122
+ runImmediately(fn) {
123
+ return this.run(fn, { timeout: 0 });
124
+ }
125
+ /**
126
+ * Take the lock, waiting up to `timeout` for it. Returns whether we got it.
127
+ *
128
+ * Prefer `run()` — with a bare `acquire()` you own the `try/finally`, and a
129
+ * throw between here and `release()` leaks the lock for the rest of its TTL.
130
+ */
131
+ async acquire(options = {}) {
132
+ const { timeout = 0, retryDelay = 50 } = options;
133
+ const deadline = Date.now() + timeout;
134
+ const owner = mintOwner();
135
+ for (;;) {
136
+ if (await this.store.acquire(this.key, owner, this.ttlMs)) {
137
+ this.owner = owner;
138
+ return true;
139
+ }
140
+ // Wait only if there's time left to wait *and* to sleep before the deadline.
141
+ if (Date.now() + retryDelay >= deadline)
142
+ return false;
143
+ await sleep(retryDelay);
144
+ }
145
+ }
146
+ /** Take the lock only if it's free right now. */
147
+ acquireImmediately() {
148
+ return this.acquire({ timeout: 0 });
149
+ }
150
+ /** Give the lock back. A no-op if we no longer hold it (it may have expired). */
151
+ async release() {
152
+ if (!this.owner)
153
+ return false;
154
+ const released = await this.store.release(this.key, this.owner);
155
+ this.owner = undefined;
156
+ return released;
157
+ }
158
+ /**
159
+ * Push the expiry out — for work that might outrun the TTL. Throws
160
+ * `LockNotHeldError` if we've already lost it, because the alternative (a
161
+ * silent no-op) would let you carry on believing you hold a lock you don't.
162
+ */
163
+ async extend(ttlMs = this.ttlMs) {
164
+ if (!this.owner || !(await this.store.extend(this.key, this.owner, ttlMs))) {
165
+ this.owner = undefined;
166
+ throw new LockNotHeldError(this.key);
167
+ }
168
+ }
169
+ /** Whether *anyone* holds this key — not necessarily us. */
170
+ isLocked() {
171
+ return this.store.isLocked(this.key);
172
+ }
173
+ /** Whether we held this lock but no longer do. */
174
+ async isExpired() {
175
+ if (!this.owner)
176
+ return false; // never acquired — not the same as expired
177
+ return !(await this.store.isLocked(this.key));
178
+ }
179
+ /** Milliseconds until the lock expires, or null if nobody holds it. */
180
+ getRemainingTime() {
181
+ return this.store.remainingTime(this.key);
182
+ }
183
+ /**
184
+ * Freeze the lock (key, TTL, owner token) to a string, so another process can
185
+ * `restoreLock()` it and release or extend the *same* lock — the handoff you
186
+ * need when one process takes the lock and another finishes the work.
187
+ */
188
+ serialize() {
189
+ return JSON.stringify({ key: this.key, ttlMs: this.ttlMs, owner: this.owner });
190
+ }
191
+ }
192
+ /* --------------------------------- global --------------------------------- */
193
+ let store = new MemoryLockStore();
194
+ /** Register the lock store used by `lock()`. */
195
+ export function setLockStore(next) {
196
+ store = next;
197
+ return store;
198
+ }
199
+ /** The active lock store. */
200
+ export function getLockStore() {
201
+ return store;
202
+ }
203
+ /**
204
+ * A lock on `key`, held for `ttlMs` once acquired (default 30s).
205
+ *
206
+ * const [ran] = await lock("invoice:42").run(() => charge(invoice));
207
+ */
208
+ export function lock(key, ttlMs = 30_000) {
209
+ return new Lock(key, ttlMs, store);
210
+ }
211
+ /** Rebuild a lock from `serialize()` — same key, same TTL, same owner token. */
212
+ export function restoreLock(serialized) {
213
+ const { key, ttlMs, owner } = JSON.parse(serialized);
214
+ return new Lock(key, ttlMs, store, owner);
215
+ }
@@ -5,8 +5,37 @@
5
5
  *
6
6
  * logger().info("user registered", { userId: user.id });
7
7
  * logger().error("payment failed", { orderId, error });
8
+ *
9
+ * Output goes through a `Sink` — `console` by default, but any function, so logs
10
+ * can go to a file, an HTTP collector, or a buffer in tests. Register extra
11
+ * loggers by name (`setLogger(audit, "audit")`) when a subsystem needs its own
12
+ * level or destination.
8
13
  */
9
- export type LogLevel = "debug" | "info" | "warn" | "error";
14
+ /** Levels, quietest last. `trace` is the most verbose; `fatal` the most severe. */
15
+ export type LogLevel = "trace" | "debug" | "info" | "warn" | "error" | "fatal";
16
+ /** One log event, as handed to a `Sink`. */
17
+ export interface LogRecord {
18
+ level: LogLevel;
19
+ /** ISO 8601. */
20
+ time: string;
21
+ msg: string;
22
+ /** Bindings merged with the call's context, after redaction. */
23
+ fields: Record<string, unknown>;
24
+ }
25
+ /** Where log lines go. Return nothing; a sink that throws is a sink that lies. */
26
+ export type Sink = (record: LogRecord) => void;
27
+ export interface RedactOptions {
28
+ /**
29
+ * Field paths to redact — a top-level key (`"password"`), a dot path
30
+ * (`"req.headers.authorization"`), or a wildcard segment (`"*.password"`,
31
+ * `"user.*.token"`).
32
+ */
33
+ paths: string[];
34
+ /** What to replace matched values with. Default: `"[redacted]"`. */
35
+ censor?: string;
36
+ /** Delete the key outright instead of censoring it. Default: false. */
37
+ remove?: boolean;
38
+ }
10
39
  export interface LoggerOptions {
11
40
  /** Minimum level to emit. Default: "info". */
12
41
  level?: LogLevel;
@@ -15,20 +44,69 @@ export interface LoggerOptions {
15
44
  /** Fields merged into every log line. */
16
45
  bindings?: Record<string, unknown>;
17
46
  /**
18
- * Field paths to redact from log output top-level keys (`"password"`) or dot
19
- * paths (`"req.headers.authorization"`). Matched values become `"[redacted]"`.
47
+ * Field paths to redact. A `string[]` is shorthand for `{ paths }` matched
48
+ * values become `"[redacted]"`.
20
49
  */
21
- redact?: string[];
50
+ redact?: string[] | RedactOptions;
51
+ /** Where lines go. Default: the console (`console.log` / `.warn` / `.error`). */
52
+ sink?: Sink;
53
+ /** Silence the logger entirely — nothing is emitted, at any level. */
54
+ enabled?: boolean;
55
+ }
56
+ /** The default sink: JSON to stdout, or a pretty single line when `pretty`. */
57
+ export declare function consoleSink(pretty?: boolean): Sink;
58
+ /** A sink that collects records in memory — for tests. Assert on `.records`. */
59
+ export declare class MemorySink {
60
+ readonly records: LogRecord[];
61
+ /** The sink function to hand to `LoggerOptions.sink`. */
62
+ readonly sink: Sink;
63
+ /** Records at one level. */
64
+ at(level: LogLevel): LogRecord[];
65
+ /** The messages logged, in order. */
66
+ messages(): string[];
67
+ clear(): void;
22
68
  }
23
69
  export declare class Logger {
24
70
  private options;
25
71
  private threshold;
72
+ private redact?;
73
+ private sink;
26
74
  constructor(options?: LoggerOptions);
75
+ /**
76
+ * Whether a level would be emitted. Check it before building an expensive
77
+ * context object — the work of assembling it happens whether or not the line
78
+ * is ever written.
79
+ *
80
+ * if (logger().isLevelEnabled("debug")) {
81
+ * logger().debug("state", { snapshot: expensiveSnapshot() });
82
+ * }
83
+ */
84
+ isLevelEnabled(level: LogLevel): boolean;
85
+ /** Run `fn` only if `level` would be emitted — the callback form of the above. */
86
+ ifLevelEnabled(level: LogLevel, fn: (log: Logger) => void): void;
27
87
  private write;
88
+ trace(message: string, context?: Record<string, unknown>): void;
28
89
  debug(message: string, context?: Record<string, unknown>): void;
29
90
  info(message: string, context?: Record<string, unknown>): void;
30
91
  warn(message: string, context?: Record<string, unknown>): void;
31
92
  error(message: string, context?: Record<string, unknown>): void;
93
+ /** An unrecoverable failure — the loudest level. */
94
+ fatal(message: string, context?: Record<string, unknown>): void;
95
+ /** Log at a level chosen at runtime. */
96
+ log(level: LogLevel, message: string, context?: Record<string, unknown>): void;
32
97
  /** A child logger with additional bound fields (e.g. a request id). */
33
98
  child(bindings: Record<string, unknown>): Logger;
34
99
  }
100
+ /**
101
+ * Register a logger under a name, so a subsystem can have its own level or
102
+ * destination:
103
+ *
104
+ * setLogger(new Logger({ level: "trace", sink: auditSink }), "audit");
105
+ * namedLogger("audit").trace("permission granted", { userId });
106
+ *
107
+ * The default logger is the application's `Logger` singleton — resolve that with
108
+ * the global `logger()` helper, not this.
109
+ */
110
+ export declare function setLogger(instance: Logger, name: string): Logger;
111
+ /** A logger registered with `setLogger`. Throws for an unknown name. */
112
+ export declare function namedLogger(name: string): Logger;