@flaghoist/server 0.1.4 → 0.2.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.
package/dist/index.cjs CHANGED
@@ -23,6 +23,8 @@ __export(index_exports, {
23
23
  apiKey: () => apiKey,
24
24
  bearerToken: () => bearerToken,
25
25
  createFlagServer: () => createFlagServer,
26
+ defaultRateLimitKey: () => defaultRateLimitKey,
27
+ memoryRateLimit: () => memoryRateLimit,
26
28
  oidc: () => oidc,
27
29
  openApiDocument: () => openApiDocument
28
30
  });
@@ -58,6 +60,9 @@ function buildFlag(key, body, identity, existing) {
58
60
  const rollout = typeof b.rollout === "object" && b.rollout !== null ? b.rollout : {};
59
61
  const percentage = typeof rollout.percentage === "number" ? (0, import_core.clampPercentage)(rollout.percentage) : 0;
60
62
  const description = typeof b.description === "string" ? b.description : "";
63
+ if (description.length > import_core.LIMITS.maxDescriptionLength) {
64
+ return { ok: false, error: `Description exceeds ${import_core.LIMITS.maxDescriptionLength} characters` };
65
+ }
61
66
  const inputRules = Array.isArray(b.rules) ? b.rules : [];
62
67
  const now = (/* @__PURE__ */ new Date()).toISOString();
63
68
  const candidate = {
@@ -402,6 +407,49 @@ var openApiDocument = {
402
407
  }
403
408
  };
404
409
 
410
+ // src/ratelimit.ts
411
+ function defaultRateLimitKey(headers) {
412
+ const cf = headers.get("cf-connecting-ip");
413
+ if (cf) return cf;
414
+ const fwd = headers.get("x-forwarded-for");
415
+ if (fwd) {
416
+ const first = fwd.split(",")[0]?.trim();
417
+ if (first) return first;
418
+ }
419
+ return "anonymous";
420
+ }
421
+ function memoryRateLimit(options = {}) {
422
+ const max = options.max ?? 120;
423
+ const windowMs = options.windowMs ?? 6e4;
424
+ const maxKeys = options.maxKeys ?? 1e5;
425
+ const hits = /* @__PURE__ */ new Map();
426
+ const sweep = (now) => {
427
+ for (const [key, entry] of hits) {
428
+ if (entry.resetAt <= now) hits.delete(key);
429
+ }
430
+ };
431
+ return {
432
+ key: defaultRateLimitKey,
433
+ check(key) {
434
+ const now = Date.now();
435
+ let entry = hits.get(key);
436
+ if (!entry || entry.resetAt <= now) {
437
+ if (!hits.has(key) && hits.size >= maxKeys) {
438
+ sweep(now);
439
+ if (hits.size >= maxKeys) return { ok: true };
440
+ }
441
+ entry = { count: 0, resetAt: now + windowMs };
442
+ hits.set(key, entry);
443
+ }
444
+ entry.count += 1;
445
+ if (entry.count > max) {
446
+ return { ok: false, retryAfter: Math.max(1, Math.ceil((entry.resetAt - now) / 1e3)) };
447
+ }
448
+ return { ok: true };
449
+ }
450
+ };
451
+ }
452
+
405
453
  // src/auth.ts
406
454
  var import_jose = require("jose");
407
455
  function extractBearer(headers) {
@@ -423,7 +471,22 @@ async function safeEqual(a, b) {
423
471
  for (let i = 0; i < va.length; i++) diff |= va[i] ^ vb[i];
424
472
  return diff === 0;
425
473
  }
474
+ var MIN_SECRET_LENGTH = 16;
475
+ var warnedWeakSecrets = /* @__PURE__ */ new Set();
476
+ function warnIfWeakSecret(kind, secret) {
477
+ if (secret.length >= MIN_SECRET_LENGTH) return;
478
+ void (async () => {
479
+ const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(secret));
480
+ const id = Array.from(new Uint8Array(digest).slice(0, 6)).map((b) => b.toString(16).padStart(2, "0")).join("");
481
+ if (warnedWeakSecrets.has(id)) return;
482
+ warnedWeakSecrets.add(id);
483
+ console.warn(
484
+ `[flaghoist] the ${kind} is ${secret.length} characters. Use a long random value (for example \`openssl rand -hex 32\`); a short secret is guessable, especially since Flaghoist does not rate limit authentication.`
485
+ );
486
+ })();
487
+ }
426
488
  function apiKey(expected) {
489
+ warnIfWeakSecret("read API key", expected);
427
490
  return async (headers) => {
428
491
  const provided = headers.get("x-api-key");
429
492
  if (!provided || !await safeEqual(provided, expected)) {
@@ -433,6 +496,7 @@ function apiKey(expected) {
433
496
  };
434
497
  }
435
498
  function bearerToken(expected) {
499
+ warnIfWeakSecret("admin token", expected);
436
500
  return async (headers) => {
437
501
  const provided = extractBearer(headers);
438
502
  if (!provided || !await safeEqual(provided, expected)) {
@@ -525,8 +589,23 @@ function createFlagServer(config) {
525
589
  }
526
590
  return next();
527
591
  });
592
+ app.use("*", async (c, next) => {
593
+ const cfg = resolve(c.env);
594
+ if (!cfg.rateLimit || c.req.path === "/health") return next();
595
+ const deriveKey = cfg.rateLimit.key ?? defaultRateLimitKey;
596
+ const result = await cfg.rateLimit.check(deriveKey(c.req.raw.headers));
597
+ if (!result.ok) {
598
+ if (result.retryAfter) c.header("Retry-After", String(result.retryAfter));
599
+ return c.json({ error: "Too many requests" }, 429);
600
+ }
601
+ return next();
602
+ });
528
603
  app.get("/health", (c) => c.json({ status: "ok" }));
529
- app.get("/api/v1/openapi.json", (c) => c.json(openApiDocument));
604
+ app.get("/api/v1/openapi.json", (c) => {
605
+ const cfg = resolve(c.env);
606
+ if (cfg.exposeOpenApi === false) return c.text("Not found", 404);
607
+ return c.json(openApiDocument);
608
+ });
530
609
  app.get("/admin", (c) => {
531
610
  const cfg = resolve(c.env);
532
611
  return cfg.dashboard ? c.html(cfg.dashboard) : c.text("Dashboard not configured", 404);
@@ -646,6 +725,8 @@ function createFlagServer(config) {
646
725
  apiKey,
647
726
  bearerToken,
648
727
  createFlagServer,
728
+ defaultRateLimitKey,
729
+ memoryRateLimit,
649
730
  oidc,
650
731
  openApiDocument
651
732
  });
package/dist/index.d.cts CHANGED
@@ -3,6 +3,61 @@ import { Hono } from 'hono';
3
3
  import { StorageAdapter, AttributeValue } from '@flaghoist/core';
4
4
  import { JWTVerifyGetKey } from 'jose';
5
5
 
6
+ /**
7
+ * Rate limiting for the server. Opt-in: with no `rateLimit` in the config the server behaves
8
+ * exactly as before, because a limiter with the wrong bucket key is worse than none, and only the
9
+ * operator knows how their deployment is fronted.
10
+ *
11
+ * On a single Node or container process the in-memory limiter here is genuinely effective. On
12
+ * Cloudflare Workers it is per-isolate, so it caps a burst against one isolate but not the fleet;
13
+ * the real answer there is Cloudflare's own Rate Limiting rules in front of the Worker. The read
14
+ * path already fails closed under a 429 (an OFREP client returns the caller's default), so limiting
15
+ * it degrades safely.
16
+ */
17
+ /** Outcome of a rate-limit check. `retryAfter` is whole seconds until the caller may try again. */
18
+ interface RateLimitResult {
19
+ ok: boolean;
20
+ retryAfter?: number;
21
+ }
22
+ /**
23
+ * A rate limiter. `check` decides whether a bucket may proceed; `key` chooses the bucket for a
24
+ * request. Bring your own (a Cloudflare binding, a Redis counter) or use `memoryRateLimit`.
25
+ */
26
+ interface RateLimit {
27
+ check(key: string): RateLimitResult | Promise<RateLimitResult>;
28
+ /**
29
+ * Derive the bucket key from request headers. The default trusts `CF-Connecting-IP` (set by
30
+ * Cloudflare and not spoofable there), then the first hop of `X-Forwarded-For`, then a single
31
+ * shared `anonymous` bucket. On a Node process exposed directly, with no proxy setting a
32
+ * forwarded header, every caller lands in that shared bucket, which caps total throughput rather
33
+ * than per-client; supply your own `key` if you have a trustworthy client identifier.
34
+ */
35
+ key?(headers: Headers): string;
36
+ }
37
+ interface MemoryRateLimitOptions {
38
+ /** Maximum requests allowed per key within the window. Default 120. */
39
+ max?: number;
40
+ /** Window length in milliseconds. Default 60000 (one minute). */
41
+ windowMs?: number;
42
+ /**
43
+ * Cap on how many distinct keys are tracked, to bound memory against a many-IP flood. When the
44
+ * table is full of live entries the limiter fails open (allows) rather than growing without
45
+ * limit or blocking new clients. Default 100000.
46
+ */
47
+ maxKeys?: number;
48
+ }
49
+ /** The default bucket key: Cloudflare's client IP, then a forwarded header, then a shared bucket. */
50
+ declare function defaultRateLimitKey(headers: Headers): string;
51
+ /**
52
+ * A fixed-window, in-memory rate limiter. Counts requests per key within a window and denies once
53
+ * the count exceeds `max`, reporting the seconds until the window resets. Memory is bounded: expired
54
+ * entries are swept lazily, and if the table stays full the limiter fails open rather than growing.
55
+ */
56
+ declare function memoryRateLimit(options?: MemoryRateLimitOptions): {
57
+ check(key: string): RateLimitResult;
58
+ key(headers: Headers): string;
59
+ };
60
+
6
61
  /** Result of an authentication attempt. `ok: false` carries the status/message to return. */
7
62
  interface AuthResult {
8
63
  ok: boolean;
@@ -31,6 +86,12 @@ interface ServerConfig {
31
86
  allowedOrigins?: string[];
32
87
  /** TTL in seconds for the in-isolate flag-definition cache on the read path. Default: 30. */
33
88
  cacheTtlSeconds?: number;
89
+ /**
90
+ * Optional rate limiter, applied to every route except `/health`. Off by default. On a single
91
+ * process the built-in `memoryRateLimit` is effective; on Cloudflare it is per-isolate, so prefer
92
+ * Cloudflare's own Rate Limiting there. See `packages/server/src/ratelimit.ts`.
93
+ */
94
+ rateLimit?: RateLimit;
34
95
  /**
35
96
  * Inject trusted context attributes derived from headers you control (e.g. a validated
36
97
  * session). These override client-supplied context, so security-relevant targeting can be
@@ -39,9 +100,16 @@ interface ServerConfig {
39
100
  trustedContext?: (headers: Headers) => Record<string, AttributeValue>;
40
101
  /**
41
102
  * Prebuilt admin dashboard HTML (a single-file SPA build) to serve at `/admin`. When set, a
42
- * single deploy gives you the read API, the admin API, and the management UI together.
103
+ * single deploy gives you the read API, the admin API, and the management UI together. Omit it
104
+ * (or set `dashboard = false` in flaghoist.toml) to run the APIs without the UI.
43
105
  */
44
106
  dashboard?: string;
107
+ /**
108
+ * Serve the OpenAPI 3.1 document at `/api/v1/openapi.json`. On by default. It is unauthenticated
109
+ * and describes every route and auth scheme, so a locked-down deployment that does not need it can
110
+ * set this to `false` and stop advertising its own API surface.
111
+ */
112
+ exposeOpenApi?: boolean;
45
113
  }
46
114
  /** Config, or a function that derives it from the runtime environment (e.g. Workers bindings). */
47
115
  type ConfigResolver<Env> = ServerConfig | ((env: Env) => ServerConfig);
@@ -98,4 +166,4 @@ declare function createFlagServer<Env extends object = Record<string, unknown>>(
98
166
  Bindings: Env;
99
167
  }, hono_types.BlankSchema, "/">;
100
168
 
101
- export { type AuthResult, type Authenticator, type ConfigResolver, type OidcOptions, type ServerConfig, apiKey, bearerToken, createFlagServer, oidc, openApiDocument };
169
+ export { type AuthResult, type Authenticator, type ConfigResolver, type MemoryRateLimitOptions, type OidcOptions, type RateLimit, type RateLimitResult, type ServerConfig, apiKey, bearerToken, createFlagServer, defaultRateLimitKey, memoryRateLimit, oidc, openApiDocument };
package/dist/index.d.ts CHANGED
@@ -3,6 +3,61 @@ import { Hono } from 'hono';
3
3
  import { StorageAdapter, AttributeValue } from '@flaghoist/core';
4
4
  import { JWTVerifyGetKey } from 'jose';
5
5
 
6
+ /**
7
+ * Rate limiting for the server. Opt-in: with no `rateLimit` in the config the server behaves
8
+ * exactly as before, because a limiter with the wrong bucket key is worse than none, and only the
9
+ * operator knows how their deployment is fronted.
10
+ *
11
+ * On a single Node or container process the in-memory limiter here is genuinely effective. On
12
+ * Cloudflare Workers it is per-isolate, so it caps a burst against one isolate but not the fleet;
13
+ * the real answer there is Cloudflare's own Rate Limiting rules in front of the Worker. The read
14
+ * path already fails closed under a 429 (an OFREP client returns the caller's default), so limiting
15
+ * it degrades safely.
16
+ */
17
+ /** Outcome of a rate-limit check. `retryAfter` is whole seconds until the caller may try again. */
18
+ interface RateLimitResult {
19
+ ok: boolean;
20
+ retryAfter?: number;
21
+ }
22
+ /**
23
+ * A rate limiter. `check` decides whether a bucket may proceed; `key` chooses the bucket for a
24
+ * request. Bring your own (a Cloudflare binding, a Redis counter) or use `memoryRateLimit`.
25
+ */
26
+ interface RateLimit {
27
+ check(key: string): RateLimitResult | Promise<RateLimitResult>;
28
+ /**
29
+ * Derive the bucket key from request headers. The default trusts `CF-Connecting-IP` (set by
30
+ * Cloudflare and not spoofable there), then the first hop of `X-Forwarded-For`, then a single
31
+ * shared `anonymous` bucket. On a Node process exposed directly, with no proxy setting a
32
+ * forwarded header, every caller lands in that shared bucket, which caps total throughput rather
33
+ * than per-client; supply your own `key` if you have a trustworthy client identifier.
34
+ */
35
+ key?(headers: Headers): string;
36
+ }
37
+ interface MemoryRateLimitOptions {
38
+ /** Maximum requests allowed per key within the window. Default 120. */
39
+ max?: number;
40
+ /** Window length in milliseconds. Default 60000 (one minute). */
41
+ windowMs?: number;
42
+ /**
43
+ * Cap on how many distinct keys are tracked, to bound memory against a many-IP flood. When the
44
+ * table is full of live entries the limiter fails open (allows) rather than growing without
45
+ * limit or blocking new clients. Default 100000.
46
+ */
47
+ maxKeys?: number;
48
+ }
49
+ /** The default bucket key: Cloudflare's client IP, then a forwarded header, then a shared bucket. */
50
+ declare function defaultRateLimitKey(headers: Headers): string;
51
+ /**
52
+ * A fixed-window, in-memory rate limiter. Counts requests per key within a window and denies once
53
+ * the count exceeds `max`, reporting the seconds until the window resets. Memory is bounded: expired
54
+ * entries are swept lazily, and if the table stays full the limiter fails open rather than growing.
55
+ */
56
+ declare function memoryRateLimit(options?: MemoryRateLimitOptions): {
57
+ check(key: string): RateLimitResult;
58
+ key(headers: Headers): string;
59
+ };
60
+
6
61
  /** Result of an authentication attempt. `ok: false` carries the status/message to return. */
7
62
  interface AuthResult {
8
63
  ok: boolean;
@@ -31,6 +86,12 @@ interface ServerConfig {
31
86
  allowedOrigins?: string[];
32
87
  /** TTL in seconds for the in-isolate flag-definition cache on the read path. Default: 30. */
33
88
  cacheTtlSeconds?: number;
89
+ /**
90
+ * Optional rate limiter, applied to every route except `/health`. Off by default. On a single
91
+ * process the built-in `memoryRateLimit` is effective; on Cloudflare it is per-isolate, so prefer
92
+ * Cloudflare's own Rate Limiting there. See `packages/server/src/ratelimit.ts`.
93
+ */
94
+ rateLimit?: RateLimit;
34
95
  /**
35
96
  * Inject trusted context attributes derived from headers you control (e.g. a validated
36
97
  * session). These override client-supplied context, so security-relevant targeting can be
@@ -39,9 +100,16 @@ interface ServerConfig {
39
100
  trustedContext?: (headers: Headers) => Record<string, AttributeValue>;
40
101
  /**
41
102
  * Prebuilt admin dashboard HTML (a single-file SPA build) to serve at `/admin`. When set, a
42
- * single deploy gives you the read API, the admin API, and the management UI together.
103
+ * single deploy gives you the read API, the admin API, and the management UI together. Omit it
104
+ * (or set `dashboard = false` in flaghoist.toml) to run the APIs without the UI.
43
105
  */
44
106
  dashboard?: string;
107
+ /**
108
+ * Serve the OpenAPI 3.1 document at `/api/v1/openapi.json`. On by default. It is unauthenticated
109
+ * and describes every route and auth scheme, so a locked-down deployment that does not need it can
110
+ * set this to `false` and stop advertising its own API surface.
111
+ */
112
+ exposeOpenApi?: boolean;
45
113
  }
46
114
  /** Config, or a function that derives it from the runtime environment (e.g. Workers bindings). */
47
115
  type ConfigResolver<Env> = ServerConfig | ((env: Env) => ServerConfig);
@@ -98,4 +166,4 @@ declare function createFlagServer<Env extends object = Record<string, unknown>>(
98
166
  Bindings: Env;
99
167
  }, hono_types.BlankSchema, "/">;
100
168
 
101
- export { type AuthResult, type Authenticator, type ConfigResolver, type OidcOptions, type ServerConfig, apiKey, bearerToken, createFlagServer, oidc, openApiDocument };
169
+ export { type AuthResult, type Authenticator, type ConfigResolver, type MemoryRateLimitOptions, type OidcOptions, type RateLimit, type RateLimitResult, type ServerConfig, apiKey, bearerToken, createFlagServer, defaultRateLimitKey, memoryRateLimit, oidc, openApiDocument };
package/dist/index.js CHANGED
@@ -20,7 +20,7 @@ function createDefinitionCache() {
20
20
  }
21
21
 
22
22
  // src/flags.ts
23
- import { clampPercentage, parseFlag } from "@flaghoist/core";
23
+ import { clampPercentage, LIMITS, parseFlag } from "@flaghoist/core";
24
24
  function buildFlag(key, body, identity, existing) {
25
25
  if (typeof body !== "object" || body === null) {
26
26
  return { ok: false, error: "Request body must be a JSON object" };
@@ -30,6 +30,9 @@ function buildFlag(key, body, identity, existing) {
30
30
  const rollout = typeof b.rollout === "object" && b.rollout !== null ? b.rollout : {};
31
31
  const percentage = typeof rollout.percentage === "number" ? clampPercentage(rollout.percentage) : 0;
32
32
  const description = typeof b.description === "string" ? b.description : "";
33
+ if (description.length > LIMITS.maxDescriptionLength) {
34
+ return { ok: false, error: `Description exceeds ${LIMITS.maxDescriptionLength} characters` };
35
+ }
33
36
  const inputRules = Array.isArray(b.rules) ? b.rules : [];
34
37
  const now = (/* @__PURE__ */ new Date()).toISOString();
35
38
  const candidate = {
@@ -374,6 +377,49 @@ var openApiDocument = {
374
377
  }
375
378
  };
376
379
 
380
+ // src/ratelimit.ts
381
+ function defaultRateLimitKey(headers) {
382
+ const cf = headers.get("cf-connecting-ip");
383
+ if (cf) return cf;
384
+ const fwd = headers.get("x-forwarded-for");
385
+ if (fwd) {
386
+ const first = fwd.split(",")[0]?.trim();
387
+ if (first) return first;
388
+ }
389
+ return "anonymous";
390
+ }
391
+ function memoryRateLimit(options = {}) {
392
+ const max = options.max ?? 120;
393
+ const windowMs = options.windowMs ?? 6e4;
394
+ const maxKeys = options.maxKeys ?? 1e5;
395
+ const hits = /* @__PURE__ */ new Map();
396
+ const sweep = (now) => {
397
+ for (const [key, entry] of hits) {
398
+ if (entry.resetAt <= now) hits.delete(key);
399
+ }
400
+ };
401
+ return {
402
+ key: defaultRateLimitKey,
403
+ check(key) {
404
+ const now = Date.now();
405
+ let entry = hits.get(key);
406
+ if (!entry || entry.resetAt <= now) {
407
+ if (!hits.has(key) && hits.size >= maxKeys) {
408
+ sweep(now);
409
+ if (hits.size >= maxKeys) return { ok: true };
410
+ }
411
+ entry = { count: 0, resetAt: now + windowMs };
412
+ hits.set(key, entry);
413
+ }
414
+ entry.count += 1;
415
+ if (entry.count > max) {
416
+ return { ok: false, retryAfter: Math.max(1, Math.ceil((entry.resetAt - now) / 1e3)) };
417
+ }
418
+ return { ok: true };
419
+ }
420
+ };
421
+ }
422
+
377
423
  // src/auth.ts
378
424
  import { createRemoteJWKSet, jwtVerify } from "jose";
379
425
  function extractBearer(headers) {
@@ -395,7 +441,22 @@ async function safeEqual(a, b) {
395
441
  for (let i = 0; i < va.length; i++) diff |= va[i] ^ vb[i];
396
442
  return diff === 0;
397
443
  }
444
+ var MIN_SECRET_LENGTH = 16;
445
+ var warnedWeakSecrets = /* @__PURE__ */ new Set();
446
+ function warnIfWeakSecret(kind, secret) {
447
+ if (secret.length >= MIN_SECRET_LENGTH) return;
448
+ void (async () => {
449
+ const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(secret));
450
+ const id = Array.from(new Uint8Array(digest).slice(0, 6)).map((b) => b.toString(16).padStart(2, "0")).join("");
451
+ if (warnedWeakSecrets.has(id)) return;
452
+ warnedWeakSecrets.add(id);
453
+ console.warn(
454
+ `[flaghoist] the ${kind} is ${secret.length} characters. Use a long random value (for example \`openssl rand -hex 32\`); a short secret is guessable, especially since Flaghoist does not rate limit authentication.`
455
+ );
456
+ })();
457
+ }
398
458
  function apiKey(expected) {
459
+ warnIfWeakSecret("read API key", expected);
399
460
  return async (headers) => {
400
461
  const provided = headers.get("x-api-key");
401
462
  if (!provided || !await safeEqual(provided, expected)) {
@@ -405,6 +466,7 @@ function apiKey(expected) {
405
466
  };
406
467
  }
407
468
  function bearerToken(expected) {
469
+ warnIfWeakSecret("admin token", expected);
408
470
  return async (headers) => {
409
471
  const provided = extractBearer(headers);
410
472
  if (!provided || !await safeEqual(provided, expected)) {
@@ -497,8 +559,23 @@ function createFlagServer(config) {
497
559
  }
498
560
  return next();
499
561
  });
562
+ app.use("*", async (c, next) => {
563
+ const cfg = resolve(c.env);
564
+ if (!cfg.rateLimit || c.req.path === "/health") return next();
565
+ const deriveKey = cfg.rateLimit.key ?? defaultRateLimitKey;
566
+ const result = await cfg.rateLimit.check(deriveKey(c.req.raw.headers));
567
+ if (!result.ok) {
568
+ if (result.retryAfter) c.header("Retry-After", String(result.retryAfter));
569
+ return c.json({ error: "Too many requests" }, 429);
570
+ }
571
+ return next();
572
+ });
500
573
  app.get("/health", (c) => c.json({ status: "ok" }));
501
- app.get("/api/v1/openapi.json", (c) => c.json(openApiDocument));
574
+ app.get("/api/v1/openapi.json", (c) => {
575
+ const cfg = resolve(c.env);
576
+ if (cfg.exposeOpenApi === false) return c.text("Not found", 404);
577
+ return c.json(openApiDocument);
578
+ });
502
579
  app.get("/admin", (c) => {
503
580
  const cfg = resolve(c.env);
504
581
  return cfg.dashboard ? c.html(cfg.dashboard) : c.text("Dashboard not configured", 404);
@@ -617,6 +694,8 @@ export {
617
694
  apiKey,
618
695
  bearerToken,
619
696
  createFlagServer,
697
+ defaultRateLimitKey,
698
+ memoryRateLimit,
620
699
  oidc,
621
700
  openApiDocument
622
701
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@flaghoist/server",
3
- "version": "0.1.4",
3
+ "version": "0.2.0",
4
4
  "description": "Hono app factory for Flaghoist: OFREP evaluate, admin CRUD, and pluggable auth.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -46,10 +46,10 @@
46
46
  "dependencies": {
47
47
  "hono": "^4.13.3",
48
48
  "jose": "^6.2.9",
49
- "@flaghoist/core": "0.1.1"
49
+ "@flaghoist/core": "0.1.2"
50
50
  },
51
51
  "devDependencies": {
52
- "@flaghoist/adapter-memory": "0.1.1",
52
+ "@flaghoist/adapter-memory": "0.1.2",
53
53
  "@flaghoist/dashboard": "0.0.0"
54
54
  },
55
55
  "publishConfig": {