@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
@@ -5,6 +5,23 @@
5
5
  * the global `cache()` helper.
6
6
  *
7
7
  * const stats = await cache().remember("stats", 60, () => computeStats());
8
+ *
9
+ * Features borrowed from bentocache (AdonisJS' cache), kept inside keel's
10
+ * single-store, edge-safe model:
11
+ *
12
+ * - Stampede protection — concurrent `remember()` calls for the same cold key
13
+ * run the factory ONCE and share the result, instead of dog-piling.
14
+ * - Grace / stale-on-error — with a `grace` window, an expired value is kept
15
+ * a little longer and served if the refreshing factory throws.
16
+ * - Tags — group entries and invalidate them together with `deleteByTag()`.
17
+ * - Namespaces — `namespace("users")` scopes keys under a prefix, and its
18
+ * `flush()` clears only that namespace.
19
+ *
20
+ * Tag and namespace invalidation use version stamps, not a key index: each tag
21
+ * carries a counter, every entry records the counter values it was written at,
22
+ * and invalidation just bumps the counter — so an entry whose tag has moved on
23
+ * reads as a miss. O(1) invalidation, no key-set bookkeeping, works on any
24
+ * `CacheStore`.
8
25
  */
9
26
  /** The default in-memory store. */
10
27
  export class MemoryStore {
@@ -29,24 +46,109 @@ export class MemoryStore {
29
46
  this.data.clear();
30
47
  }
31
48
  }
49
+ function isEntry(x) {
50
+ return typeof x === "object" && x !== null && x.__keelCache === 1;
51
+ }
52
+ // The store key that holds a tag's version counter. Never namespaced, never
53
+ // expired — a bump must outlive every entry that recorded the old version.
54
+ const TAG_VERSION_PREFIX = "\u0000keel:tagv:";
32
55
  export class Cache {
33
56
  store;
34
57
  constructor(store = new MemoryStore()) {
35
58
  this.store = store;
36
59
  }
60
+ // Key prefix + implicit tag for a namespaced cache (empty on the root).
61
+ prefix = "";
62
+ nsTag;
63
+ // Stampede protection: in-flight factory promises, keyed by prefixed key.
64
+ inflight = new Map();
65
+ /** Apply this cache's namespace prefix to a logical key. */
66
+ k(key) {
67
+ return this.prefix + key;
68
+ }
69
+ tagKey(tag) {
70
+ return TAG_VERSION_PREFIX + tag;
71
+ }
72
+ async tagVersion(tag) {
73
+ const v = await this.store.get(this.tagKey(tag));
74
+ return typeof v === "number" ? v : 0;
75
+ }
76
+ /** Snapshot the current version of each tag, to stamp onto a new entry. */
77
+ async stampTags(tags) {
78
+ const out = {};
79
+ for (const tag of tags)
80
+ out[tag] = await this.tagVersion(tag);
81
+ return out;
82
+ }
83
+ /** True if any recorded tag version is behind the live one (invalidated). */
84
+ async tagsInvalidated(recorded) {
85
+ for (const [tag, ver] of Object.entries(recorded)) {
86
+ if ((await this.tagVersion(tag)) > ver)
87
+ return true;
88
+ }
89
+ return false;
90
+ }
91
+ /**
92
+ * Read the raw envelope for a key. Returns undefined when absent OR when a tag
93
+ * has invalidated it — tag/namespace invalidation is a hard miss (not grace-
94
+ * eligible). TTL freshness is left to callers, so grace can still see a stale
95
+ * entry.
96
+ */
97
+ async entry(key) {
98
+ const raw = await this.store.get(this.k(key));
99
+ if (raw === undefined)
100
+ return undefined;
101
+ const e = isEntry(raw) ? raw : { __keelCache: 1, v: raw, e: 0 };
102
+ if (e.t && (await this.tagsInvalidated(e.t)))
103
+ return undefined;
104
+ return e;
105
+ }
106
+ async write(key, value, ttlSeconds, graceSeconds = 0, tags = []) {
107
+ const allTags = this.nsTag ? [this.nsTag, ...tags] : tags;
108
+ const now = Date.now();
109
+ const e = ttlSeconds ? now + ttlSeconds * 1000 : 0;
110
+ const storeTtlMs = ttlSeconds ? (ttlSeconds + graceSeconds) * 1000 : undefined;
111
+ const t = allTags.length ? await this.stampTags(allTags) : undefined;
112
+ const entry = { __keelCache: 1, v: value, e };
113
+ if (t)
114
+ entry.t = t;
115
+ await this.store.set(this.k(key), entry, storeTtlMs);
116
+ }
37
117
  async get(key, fallback) {
38
- const value = await this.store.get(key);
39
- return (value === undefined ? fallback : value);
118
+ const entry = await this.entry(key);
119
+ if (!entry)
120
+ return fallback;
121
+ if (entry.e && entry.e < Date.now())
122
+ return fallback; // expired (grace-retained)
123
+ return entry.v;
124
+ }
125
+ /** Store a value, optionally expiring after `ttlSeconds`, with optional tags. */
126
+ async put(key, value, ttlSeconds, options) {
127
+ await this.write(key, value, ttlSeconds, 0, options?.tags ?? []);
40
128
  }
41
- /** Store a value, optionally expiring after `ttlSeconds`. */
42
- async put(key, value, ttlSeconds) {
43
- await this.store.set(key, value, ttlSeconds ? ttlSeconds * 1000 : undefined);
129
+ /** Store a value only if the key is absent. Returns whether it was written. */
130
+ async add(key, value, ttlSeconds, options) {
131
+ if (await this.has(key))
132
+ return false;
133
+ await this.put(key, value, ttlSeconds, options);
134
+ return true;
44
135
  }
45
136
  async has(key) {
46
- return (await this.store.get(key)) !== undefined;
137
+ const entry = await this.entry(key);
138
+ if (!entry)
139
+ return false;
140
+ return !(entry.e && entry.e < Date.now());
141
+ }
142
+ /** The inverse of `has` — true when the key is absent or expired. */
143
+ async missing(key) {
144
+ return !(await this.has(key));
47
145
  }
48
146
  async forget(key) {
49
- await this.store.delete(key);
147
+ await this.store.delete(this.k(key));
148
+ }
149
+ /** Forget several keys at once. */
150
+ async forgetMany(keys) {
151
+ await Promise.all(keys.map((k) => this.store.delete(this.k(k))));
50
152
  }
51
153
  /** Read and remove a value. */
52
154
  async pull(key, fallback) {
@@ -55,27 +157,83 @@ export class Cache {
55
157
  return value;
56
158
  }
57
159
  async flush() {
58
- await this.store.clear();
160
+ // A namespace clears itself by moving its tag version on; the root wipes the
161
+ // whole store.
162
+ if (this.nsTag)
163
+ await this.deleteByTag([this.nsTag]);
164
+ else
165
+ await this.store.clear();
166
+ }
167
+ /**
168
+ * Invalidate every entry carrying any of these tags by bumping the tag's
169
+ * version — entries stamped with the old version then read as a miss. O(#tags),
170
+ * no key scan; invalidated entries fall out on their own TTL.
171
+ */
172
+ async deleteByTag(tags) {
173
+ for (const tag of tags) {
174
+ const current = await this.tagVersion(tag);
175
+ await this.store.set(this.tagKey(tag), current + 1); // no TTL — must persist
176
+ }
177
+ }
178
+ /**
179
+ * A cache scoped under a key prefix. Keys written through it live at
180
+ * `name:key`, and its `flush()` clears only this namespace (via an implicit
181
+ * tag) — the rest of the store is untouched. Namespaces nest.
182
+ *
183
+ * const users = cache().namespace("users");
184
+ * await users.put("1", user); // stored at "users:1"
185
+ * await users.flush(); // clears only the users namespace
186
+ */
187
+ namespace(name) {
188
+ const child = new Cache(this.store);
189
+ child.prefix = `${this.prefix}${name}:`;
190
+ child.nsTag = `\u0000keel:ns:${this.prefix}${name}`;
191
+ return child;
59
192
  }
60
193
  /**
61
194
  * Return the cached value, or compute it with `factory`, cache it for
62
195
  * `ttlSeconds`, and return it.
196
+ *
197
+ * Concurrent calls for the same cold key share one factory run (stampede
198
+ * protection). With `{ grace }`, an expired value is retained that many extra
199
+ * seconds and served if the refreshing factory throws (stale-on-error). With
200
+ * `{ tags }`, the cached value joins those tags for `deleteByTag`.
63
201
  */
64
- async remember(key, ttlSeconds, factory) {
65
- const existing = await this.store.get(key);
66
- if (existing !== undefined)
67
- return existing;
68
- const value = await factory();
69
- await this.put(key, value, ttlSeconds);
70
- return value;
202
+ remember(key, ttlSeconds, factory, options) {
203
+ return this.resolve(key, ttlSeconds, factory, options?.grace ?? 0, options?.tags ?? []);
71
204
  }
72
- /** Like remember(), but cached forever (no TTL). */
73
- async rememberForever(key, factory) {
74
- const existing = await this.store.get(key);
75
- if (existing !== undefined)
76
- return existing;
77
- const value = await factory();
78
- await this.put(key, value);
79
- return value;
205
+ /** Like remember(), but cached forever (no TTL). Also stampede-protected. */
206
+ rememberForever(key, factory, options) {
207
+ return this.resolve(key, undefined, factory, 0, options?.tags ?? []);
208
+ }
209
+ async resolve(key, ttlSeconds, factory, graceSeconds, tags) {
210
+ const entry = await this.entry(key);
211
+ const fresh = entry && (!entry.e || entry.e > Date.now());
212
+ if (fresh)
213
+ return entry.v;
214
+ // Stampede protection: join an in-flight computation for this key.
215
+ const pending = this.inflight.get(key);
216
+ if (pending)
217
+ return pending;
218
+ const run = (async () => {
219
+ try {
220
+ // Defer to a microtask so a synchronously-throwing factory rejects the
221
+ // shared promise only after every joiner has attached its handler.
222
+ const value = await Promise.resolve().then(factory);
223
+ await this.write(key, value, ttlSeconds, graceSeconds, tags);
224
+ return value;
225
+ }
226
+ catch (err) {
227
+ // Grace: a stale-but-retained value rescues a failing refresh.
228
+ if (entry && graceSeconds > 0)
229
+ return entry.v;
230
+ throw err;
231
+ }
232
+ finally {
233
+ this.inflight.delete(key);
234
+ }
235
+ })();
236
+ this.inflight.set(key, run);
237
+ return run;
80
238
  }
81
239
  }
@@ -0,0 +1,12 @@
1
+ /** Code-generation templates for `keel make:*`. */
2
+ export declare function controllerStub(name: string): string;
3
+ export declare function resourceControllerStub(name: string): string;
4
+ export declare function providerStub(name: string): string;
5
+ export declare function middlewareStub(name: string): string;
6
+ /** `name` is the model class (e.g. "User") the factory builds. */
7
+ export declare function factoryStub(model: string): string;
8
+ export declare function seederStub(name: string): string;
9
+ export declare function jobStub(name: string): string;
10
+ export declare function notificationStub(name: string): string;
11
+ /** `name` is the class (e.g. "UserTransformer"); `model` is the value it maps. */
12
+ export declare function transformerStub(name: string, model: string): string;
@@ -0,0 +1,120 @@
1
+ /** Code-generation templates for `keel make:*`. */
2
+ export function controllerStub(name) {
3
+ return `import type { Ctx } from "@shaferllc/keel/core";
4
+
5
+ export class ${name} {
6
+ index(c: Ctx) {
7
+ return c.json({ controller: "${name}", action: "index" });
8
+ }
9
+ }
10
+ `;
11
+ }
12
+ export function resourceControllerStub(name) {
13
+ const actions = ["index", "create", "store", "show", "edit", "update", "destroy"];
14
+ const body = actions
15
+ .map((a) => ` ${a}(c: Ctx) {\n return c.json({ action: "${a}" });\n }`)
16
+ .join("\n\n");
17
+ return `import type { Ctx } from "@shaferllc/keel/core";
18
+
19
+ export class ${name} {
20
+ ${body}
21
+ }
22
+ `;
23
+ }
24
+ export function providerStub(name) {
25
+ return `import { ServiceProvider } from "@shaferllc/keel/core";
26
+
27
+ export class ${name} extends ServiceProvider {
28
+ register(): void {
29
+ // Bind services into the container here.
30
+ }
31
+
32
+ boot(): void {
33
+ // Resolve and wire things up here.
34
+ }
35
+ }
36
+ `;
37
+ }
38
+ export function middlewareStub(name) {
39
+ const fn = name.charAt(0).toLowerCase() + name.slice(1);
40
+ return `import type { MiddlewareHandler } from "hono";
41
+
42
+ export const ${fn}: MiddlewareHandler = async (c, next) => {
43
+ // ...before
44
+ await next();
45
+ // ...after
46
+ };
47
+ `;
48
+ }
49
+ /** `name` is the model class (e.g. "User") the factory builds. */
50
+ export function factoryStub(model) {
51
+ return `import { factory } from "@shaferllc/keel/core";
52
+ import { ${model} } from "../../app/Models/${model}.js";
53
+
54
+ export const ${model.toLowerCase()}Factory = factory(${model}, (f) => ({
55
+ // Describe one ${model}'s attributes; \`f\` is a Faker.
56
+ name: f.name(),
57
+ email: f.email(),
58
+ }));
59
+ `;
60
+ }
61
+ export function seederStub(name) {
62
+ return `import { Seeder } from "@shaferllc/keel/core";
63
+
64
+ export class ${name} extends Seeder {
65
+ async run(): Promise<void> {
66
+ // Populate the database, e.g.:
67
+ // await userFactory.count(10).create();
68
+ }
69
+ }
70
+ `;
71
+ }
72
+ export function jobStub(name) {
73
+ return `import { Job } from "@shaferllc/keel/core";
74
+
75
+ export class ${name} extends Job {
76
+ constructor(/* pass the data this job needs */) {
77
+ super();
78
+ }
79
+
80
+ async handle(): Promise<void> {
81
+ // Do the background work here.
82
+ }
83
+ }
84
+ `;
85
+ }
86
+ export function notificationStub(name) {
87
+ return `import { Notification, type Notifiable, type MailContent } from "@shaferllc/keel/core";
88
+
89
+ export class ${name} extends Notification {
90
+ via(_notifiable: Notifiable): string[] {
91
+ return ["mail"];
92
+ }
93
+
94
+ toMail(_notifiable: Notifiable): MailContent {
95
+ return {
96
+ subject: "${name}",
97
+ text: "Notification body.",
98
+ };
99
+ }
100
+ }
101
+ `;
102
+ }
103
+ /** `name` is the class (e.g. "UserTransformer"); `model` is the value it maps. */
104
+ export function transformerStub(name, model) {
105
+ return `import { Transformer, type Attributes } from "@shaferllc/keel/core";
106
+ // import { ${model} } from "../Models/${model}.js";
107
+
108
+ export class ${name} extends Transformer</* ${model} */ any> {
109
+ transform(item: /* ${model} */ any): Attributes {
110
+ // Map the value to the exact shape your API exposes.
111
+ return {
112
+ id: item.id,
113
+ // name: item.name,
114
+ // email: this.when(canSeeEmail, item.email),
115
+ // posts: this.whenLoaded(item, "posts", new PostTransformer()),
116
+ };
117
+ }
118
+ }
119
+ `;
120
+ }
@@ -13,12 +13,32 @@ export type Factory<T> = (app: Container) => T;
13
13
  export declare class Container {
14
14
  private bindings;
15
15
  private instances;
16
+ private swaps;
16
17
  /** Register a transient binding — a fresh value every resolve. */
17
18
  bind<T>(token: Token<T>, factory: Factory<T>): this;
18
19
  /** Register a shared binding — resolved once, then cached. */
19
20
  singleton<T>(token: Token<T>, factory: Factory<T>): this;
20
21
  /** Register an already-constructed value as a shared instance. */
21
22
  instance<T>(token: Token<T>, value: T): T;
23
+ /**
24
+ * Register an alias that resolves to another token — `alias("router", Router)`
25
+ * lets `make("router")` return whatever `make(Router)` does, honoring the
26
+ * target's own sharing (the target owns the singleton; the alias just points).
27
+ */
28
+ alias<T>(alias: Token<T>, target: Token<T>): this;
29
+ /**
30
+ * Temporarily replace a binding with a fake — for tests. The replacement is
31
+ * shared (resolved once), and the original binding/instance is remembered so
32
+ * `restore()` can put it back. Idempotent per token: the first swap saves the
33
+ * original; later swaps just change the fake.
34
+ *
35
+ * app.swap(Mailer, () => fakeMailer);
36
+ * // … exercise code that resolves Mailer …
37
+ * app.restore(Mailer);
38
+ */
39
+ swap<T>(token: Token<T>, factory: Factory<T>): this;
40
+ /** Undo a `swap()` — restore the original binding. No token restores every swap. */
41
+ restore(token?: Token): this;
22
42
  /** True if the token is bound or has a cached instance. */
23
43
  bound(token: Token): boolean;
24
44
  /** Resolve a token out of the container. */
@@ -10,6 +10,7 @@
10
10
  export class Container {
11
11
  bindings = new Map();
12
12
  instances = new Map();
13
+ swaps = new Map();
13
14
  /** Register a transient binding — a fresh value every resolve. */
14
15
  bind(token, factory) {
15
16
  this.bindings.set(token, { factory, shared: false });
@@ -25,6 +26,57 @@ export class Container {
25
26
  this.instances.set(token, value);
26
27
  return value;
27
28
  }
29
+ /**
30
+ * Register an alias that resolves to another token — `alias("router", Router)`
31
+ * lets `make("router")` return whatever `make(Router)` does, honoring the
32
+ * target's own sharing (the target owns the singleton; the alias just points).
33
+ */
34
+ alias(alias, target) {
35
+ this.bindings.set(alias, { factory: (app) => app.make(target), shared: false });
36
+ return this;
37
+ }
38
+ /**
39
+ * Temporarily replace a binding with a fake — for tests. The replacement is
40
+ * shared (resolved once), and the original binding/instance is remembered so
41
+ * `restore()` can put it back. Idempotent per token: the first swap saves the
42
+ * original; later swaps just change the fake.
43
+ *
44
+ * app.swap(Mailer, () => fakeMailer);
45
+ * // … exercise code that resolves Mailer …
46
+ * app.restore(Mailer);
47
+ */
48
+ swap(token, factory) {
49
+ if (!this.swaps.has(token)) {
50
+ this.swaps.set(token, {
51
+ binding: this.bindings.get(token),
52
+ instance: this.instances.get(token),
53
+ hadInstance: this.instances.has(token),
54
+ });
55
+ }
56
+ this.bindings.set(token, { factory, shared: true });
57
+ this.instances.delete(token); // force the next make() through the fake
58
+ return this;
59
+ }
60
+ /** Undo a `swap()` — restore the original binding. No token restores every swap. */
61
+ restore(token) {
62
+ if (token === undefined) {
63
+ for (const t of [...this.swaps.keys()])
64
+ this.restore(t);
65
+ return this;
66
+ }
67
+ const saved = this.swaps.get(token);
68
+ if (!saved)
69
+ return this;
70
+ this.swaps.delete(token);
71
+ this.instances.delete(token);
72
+ if (saved.binding)
73
+ this.bindings.set(token, saved.binding);
74
+ else
75
+ this.bindings.delete(token);
76
+ if (saved.hadInstance)
77
+ this.instances.set(token, saved.instance);
78
+ return this;
79
+ }
28
80
  /** True if the token is bound or has a cached instance. */
29
81
  bound(token) {
30
82
  return this.bindings.has(token) || this.instances.has(token);
@@ -0,0 +1,29 @@
1
+ /**
2
+ * CORS — Cross-Origin Resource Sharing. Register it in your HTTP kernel (or on a
3
+ * route group) to let browsers on other origins call your API. It answers
4
+ * preflight `OPTIONS` requests automatically and sets the `Access-Control-*`
5
+ * headers on every response.
6
+ *
7
+ * this.use(cors()); // reflect any origin (dev)
8
+ * this.use(cors({ origin: ["https://app.example.com"], credentials: true }));
9
+ *
10
+ * `origin` accepts a boolean (`true` reflects the caller, `false` blocks), `"*"`,
11
+ * an allowlist array, or a predicate. When `credentials` is on, `"*"` isn't legal
12
+ * per the spec, so the caller's origin is reflected and `Vary: Origin` is set.
13
+ */
14
+ import type { Context, MiddlewareHandler } from "hono";
15
+ export interface CorsOptions {
16
+ /** Who may call: `true` reflects the request origin, `false` blocks, `"*"` any, an allowlist, or a predicate. Default `true`. */
17
+ origin?: boolean | string | string[] | ((origin: string, c: Context) => boolean | string);
18
+ /** Allowed methods for cross-origin requests. */
19
+ methods?: string[];
20
+ /** Allowed request headers: `true` reflects what the browser asks for, or an allowlist. Default `true`. */
21
+ headers?: boolean | string[];
22
+ /** Response headers JS may read (`Access-Control-Expose-Headers`). */
23
+ exposeHeaders?: string[];
24
+ /** Send `Access-Control-Allow-Credentials: true` (cookies/authorization). Default `false`. */
25
+ credentials?: boolean;
26
+ /** Preflight cache seconds (`Access-Control-Max-Age`). `null` omits it. Default 86400. */
27
+ maxAge?: number | null;
28
+ }
29
+ export declare function cors(options?: CorsOptions): MiddlewareHandler;
@@ -0,0 +1,72 @@
1
+ /**
2
+ * CORS — Cross-Origin Resource Sharing. Register it in your HTTP kernel (or on a
3
+ * route group) to let browsers on other origins call your API. It answers
4
+ * preflight `OPTIONS` requests automatically and sets the `Access-Control-*`
5
+ * headers on every response.
6
+ *
7
+ * this.use(cors()); // reflect any origin (dev)
8
+ * this.use(cors({ origin: ["https://app.example.com"], credentials: true }));
9
+ *
10
+ * `origin` accepts a boolean (`true` reflects the caller, `false` blocks), `"*"`,
11
+ * an allowlist array, or a predicate. When `credentials` is on, `"*"` isn't legal
12
+ * per the spec, so the caller's origin is reflected and `Vary: Origin` is set.
13
+ */
14
+ const DEFAULT_METHODS = ["GET", "HEAD", "PUT", "PATCH", "POST", "DELETE"];
15
+ /** Resolve the allowed origin for this request, or `null` to disallow. */
16
+ function resolveOrigin(option, requestOrigin, c) {
17
+ if (option === undefined || option === true)
18
+ return requestOrigin || "*";
19
+ if (option === false)
20
+ return null;
21
+ if (option === "*")
22
+ return "*";
23
+ if (typeof option === "string")
24
+ return option;
25
+ if (Array.isArray(option))
26
+ return option.includes(requestOrigin) ? requestOrigin : null;
27
+ const result = option(requestOrigin, c);
28
+ if (result === true)
29
+ return requestOrigin || "*";
30
+ if (result === false)
31
+ return null;
32
+ return result;
33
+ }
34
+ export function cors(options = {}) {
35
+ const methods = options.methods ?? DEFAULT_METHODS;
36
+ const maxAge = options.maxAge === undefined ? 86400 : options.maxAge;
37
+ const credentials = options.credentials ?? false;
38
+ return async (c, next) => {
39
+ const requestOrigin = c.req.header("origin") ?? "";
40
+ let allowOrigin = resolveOrigin(options.origin, requestOrigin, c);
41
+ // With credentials, "*" is illegal — reflect the concrete origin instead.
42
+ if (allowOrigin === "*" && credentials)
43
+ allowOrigin = requestOrigin || null;
44
+ const shared = {};
45
+ if (allowOrigin)
46
+ shared["Access-Control-Allow-Origin"] = allowOrigin;
47
+ // Reflecting a specific origin makes the response vary by it (caches).
48
+ if (allowOrigin && allowOrigin !== "*")
49
+ shared["Vary"] = "Origin";
50
+ if (credentials)
51
+ shared["Access-Control-Allow-Credentials"] = "true";
52
+ // Preflight — answer directly with all headers, never reaching the route.
53
+ if (c.req.method === "OPTIONS" && c.req.header("access-control-request-method")) {
54
+ const headers = { ...shared, "Access-Control-Allow-Methods": methods.join(", ") };
55
+ const allowHeaders = options.headers === undefined || options.headers === true
56
+ ? c.req.header("access-control-request-headers") ?? ""
57
+ : options.headers.join(", ");
58
+ if (allowHeaders)
59
+ headers["Access-Control-Allow-Headers"] = allowHeaders;
60
+ if (maxAge != null)
61
+ headers["Access-Control-Max-Age"] = String(maxAge);
62
+ return c.body(null, 204, headers);
63
+ }
64
+ await next();
65
+ // Set on the final response (survives a handler that returns a fresh Response).
66
+ for (const [name, value] of Object.entries(shared))
67
+ c.header(name, value);
68
+ if (options.exposeHeaders?.length) {
69
+ c.header("Access-Control-Expose-Headers", options.exposeHeaders.join(", "));
70
+ }
71
+ };
72
+ }
@@ -18,12 +18,48 @@ export declare const hash: {
18
18
  verify(hashed: string, password: string): Promise<boolean>;
19
19
  /** Whether a hash was made with fewer iterations than the current default. */
20
20
  needsRehash(hashed: string, iterations?: number): boolean;
21
+ /**
22
+ * Swap real hashing for a trivial, **insecure** scheme so tests that create
23
+ * many users don't pay for PBKDF2. `make` returns `fake$<password>` and
24
+ * `verify` just compares — near-instant. Call in a test setup hook; undo with
25
+ * `restore()`. Never use outside tests.
26
+ */
27
+ fake(): void;
28
+ /** Restore real PBKDF2 hashing after `fake()`. */
29
+ restore(): void;
30
+ /**
31
+ * A valid dummy hash (of a random secret) at the default cost. Compare against
32
+ * it when a user *isn't* found so login spends the same time as a wrong
33
+ * password — otherwise a fast "no such user" response leaks which emails are
34
+ * registered (a timing/enumeration attack).
35
+ *
36
+ * const user = await findUserByEmail(email);
37
+ * const ok = await hash.verify(user?.password ?? hash.dummy, password);
38
+ * if (ok && user) auth().login(user.id); // `user &&` so the dummy never authenticates
39
+ */
40
+ dummy: string;
21
41
  };
42
+ export interface EncryptOptions {
43
+ /** Time-to-live — seconds (number) or a duration string (`"30m"`, `"1h"`, `"7d"`). */
44
+ expiresIn?: number | string;
45
+ /** Bind the token to a context; `decrypt` must pass the same `purpose` or gets `null`. */
46
+ purpose?: string;
47
+ }
22
48
  export declare const encryption: {
23
- /** Encrypt any JSON-serializable value (AES-GCM), keyed by config('app.key'). */
24
- encrypt(value: unknown): Promise<string>;
25
- /** Decrypt a value; returns null if the payload is tampered or invalid. */
26
- decrypt<T = unknown>(payload: string): Promise<T | null>;
49
+ /**
50
+ * Encrypt any JSON-serializable value (AES-GCM), keyed by `config('app.key')`.
51
+ * `expiresIn` makes the token self-expire; `purpose` binds it to a context
52
+ * (e.g. `"password-reset"`) so a token minted for one use can't be replayed for
53
+ * another.
54
+ */
55
+ encrypt(value: unknown, options?: EncryptOptions): Promise<string>;
56
+ /**
57
+ * Decrypt a value; returns `null` if the payload is tampered, invalid, expired,
58
+ * or minted for a different `purpose`. Never throws.
59
+ */
60
+ decrypt<T = unknown>(payload: string, options?: {
61
+ purpose?: string;
62
+ }): Promise<T | null>;
27
63
  };
28
64
  /** Standard registered claims, plus whatever custom fields you sign. */
29
65
  export interface JwtPayload {