@flaghoist/server 0.2.2 → 0.3.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
@@ -64,7 +64,10 @@ function buildFlag(key, body, identity, existing) {
64
64
  return { ok: false, error: `Description exceeds ${import_core.LIMITS.maxDescriptionLength} characters` };
65
65
  }
66
66
  const inputRules = Array.isArray(b.rules) ? b.rules : [];
67
- const now = (/* @__PURE__ */ new Date()).toISOString();
67
+ const nowMs = Date.now();
68
+ const prevMs = existing ? Date.parse(existing.metadata.updatedAt) : 0;
69
+ const updatedAt = new Date(nowMs > prevMs ? nowMs : prevMs + 1).toISOString();
70
+ const createdAt = existing?.metadata.createdAt ?? new Date(nowMs).toISOString();
68
71
  const candidate = {
69
72
  key,
70
73
  enabled,
@@ -73,9 +76,9 @@ function buildFlag(key, body, identity, existing) {
73
76
  description,
74
77
  metadata: {
75
78
  createdBy: existing?.metadata.createdBy ?? identity,
76
- createdAt: existing?.metadata.createdAt ?? now,
79
+ createdAt,
77
80
  updatedBy: identity,
78
- updatedAt: now
81
+ updatedAt
79
82
  }
80
83
  };
81
84
  const validated = (0, import_core.parseFlag)(candidate);
@@ -85,6 +88,9 @@ function buildFlag(key, body, identity, existing) {
85
88
  }
86
89
  return { ok: true, flag: validated };
87
90
  }
91
+ function flagEtag(flag) {
92
+ return `"${flag.metadata.updatedAt}"`;
93
+ }
88
94
 
89
95
  // src/openapi.ts
90
96
  var openApiDocument = {
@@ -574,6 +580,14 @@ function createFlagServer(config) {
574
580
  const cache = createDefinitionCache();
575
581
  const resolve = (env) => typeof config === "function" ? config(env) : config;
576
582
  const app = new import_hono.Hono();
583
+ app.use("*", async (c, next) => {
584
+ await next();
585
+ c.header("X-Content-Type-Options", "nosniff");
586
+ c.header("X-Frame-Options", "DENY");
587
+ c.header("Content-Security-Policy", "frame-ancestors 'none'");
588
+ c.header("Referrer-Policy", "no-referrer");
589
+ c.header("Permissions-Policy", "geolocation=(), microphone=(), camera=(), payment=(), usb=()");
590
+ });
577
591
  app.use("*", async (c, next) => {
578
592
  const cfg = resolve(c.env);
579
593
  const origin = c.req.header("Origin");
@@ -682,6 +696,7 @@ function createFlagServer(config) {
682
696
  if (!auth.ok) return c.json({ error: auth.message ?? "Unauthorized" }, auth.status ?? 401);
683
697
  const flag = await cfg.storage.get(c.req.param("key"));
684
698
  if (!flag) return c.json({ error: "Flag not found" }, 404);
699
+ c.header("ETag", flagEtag(flag));
685
700
  return c.json(flag);
686
701
  });
687
702
  app.put(`${prefix}/flags/:key`, async (c) => {
@@ -689,17 +704,24 @@ function createFlagServer(config) {
689
704
  const auth = await cfg.auth.admin(c.req.raw.headers);
690
705
  if (!auth.ok) return c.json({ error: auth.message ?? "Unauthorized" }, auth.status ?? 401);
691
706
  const key = c.req.param("key");
707
+ const existing = await cfg.storage.get(key);
708
+ const ifMatch = c.req.header("If-Match")?.trim();
709
+ if (ifMatch !== void 0) {
710
+ const precondition = ifMatch === "*" ? existing !== null : existing !== null && flagEtag(existing) === ifMatch;
711
+ if (!precondition) {
712
+ return c.json(
713
+ { error: "This flag changed since you loaded it. Reload and reapply your change." },
714
+ 412
715
+ );
716
+ }
717
+ }
692
718
  const parsed = await readJsonBody(await c.req.text());
693
719
  if (!parsed.ok) return c.json({ error: parsed.message }, parsed.status);
694
- const built = buildFlag(
695
- key,
696
- parsed.value,
697
- auth.identity ?? "unknown",
698
- await cfg.storage.get(key)
699
- );
720
+ const built = buildFlag(key, parsed.value, auth.identity ?? "unknown", existing);
700
721
  if (!built.ok) return c.json({ error: built.error }, 400);
701
722
  await cfg.storage.put(key, built.flag);
702
723
  cache.invalidate();
724
+ c.header("ETag", flagEtag(built.flag));
703
725
  return c.json(built.flag);
704
726
  });
705
727
  app.delete(`${prefix}/flags/:key`, async (c) => {
package/dist/index.js CHANGED
@@ -34,7 +34,10 @@ function buildFlag(key, body, identity, existing) {
34
34
  return { ok: false, error: `Description exceeds ${LIMITS.maxDescriptionLength} characters` };
35
35
  }
36
36
  const inputRules = Array.isArray(b.rules) ? b.rules : [];
37
- const now = (/* @__PURE__ */ new Date()).toISOString();
37
+ const nowMs = Date.now();
38
+ const prevMs = existing ? Date.parse(existing.metadata.updatedAt) : 0;
39
+ const updatedAt = new Date(nowMs > prevMs ? nowMs : prevMs + 1).toISOString();
40
+ const createdAt = existing?.metadata.createdAt ?? new Date(nowMs).toISOString();
38
41
  const candidate = {
39
42
  key,
40
43
  enabled,
@@ -43,9 +46,9 @@ function buildFlag(key, body, identity, existing) {
43
46
  description,
44
47
  metadata: {
45
48
  createdBy: existing?.metadata.createdBy ?? identity,
46
- createdAt: existing?.metadata.createdAt ?? now,
49
+ createdAt,
47
50
  updatedBy: identity,
48
- updatedAt: now
51
+ updatedAt
49
52
  }
50
53
  };
51
54
  const validated = parseFlag(candidate);
@@ -55,6 +58,9 @@ function buildFlag(key, body, identity, existing) {
55
58
  }
56
59
  return { ok: true, flag: validated };
57
60
  }
61
+ function flagEtag(flag) {
62
+ return `"${flag.metadata.updatedAt}"`;
63
+ }
58
64
 
59
65
  // src/openapi.ts
60
66
  var openApiDocument = {
@@ -544,6 +550,14 @@ function createFlagServer(config) {
544
550
  const cache = createDefinitionCache();
545
551
  const resolve = (env) => typeof config === "function" ? config(env) : config;
546
552
  const app = new Hono();
553
+ app.use("*", async (c, next) => {
554
+ await next();
555
+ c.header("X-Content-Type-Options", "nosniff");
556
+ c.header("X-Frame-Options", "DENY");
557
+ c.header("Content-Security-Policy", "frame-ancestors 'none'");
558
+ c.header("Referrer-Policy", "no-referrer");
559
+ c.header("Permissions-Policy", "geolocation=(), microphone=(), camera=(), payment=(), usb=()");
560
+ });
547
561
  app.use("*", async (c, next) => {
548
562
  const cfg = resolve(c.env);
549
563
  const origin = c.req.header("Origin");
@@ -652,6 +666,7 @@ function createFlagServer(config) {
652
666
  if (!auth.ok) return c.json({ error: auth.message ?? "Unauthorized" }, auth.status ?? 401);
653
667
  const flag = await cfg.storage.get(c.req.param("key"));
654
668
  if (!flag) return c.json({ error: "Flag not found" }, 404);
669
+ c.header("ETag", flagEtag(flag));
655
670
  return c.json(flag);
656
671
  });
657
672
  app.put(`${prefix}/flags/:key`, async (c) => {
@@ -659,17 +674,24 @@ function createFlagServer(config) {
659
674
  const auth = await cfg.auth.admin(c.req.raw.headers);
660
675
  if (!auth.ok) return c.json({ error: auth.message ?? "Unauthorized" }, auth.status ?? 401);
661
676
  const key = c.req.param("key");
677
+ const existing = await cfg.storage.get(key);
678
+ const ifMatch = c.req.header("If-Match")?.trim();
679
+ if (ifMatch !== void 0) {
680
+ const precondition = ifMatch === "*" ? existing !== null : existing !== null && flagEtag(existing) === ifMatch;
681
+ if (!precondition) {
682
+ return c.json(
683
+ { error: "This flag changed since you loaded it. Reload and reapply your change." },
684
+ 412
685
+ );
686
+ }
687
+ }
662
688
  const parsed = await readJsonBody(await c.req.text());
663
689
  if (!parsed.ok) return c.json({ error: parsed.message }, parsed.status);
664
- const built = buildFlag(
665
- key,
666
- parsed.value,
667
- auth.identity ?? "unknown",
668
- await cfg.storage.get(key)
669
- );
690
+ const built = buildFlag(key, parsed.value, auth.identity ?? "unknown", existing);
670
691
  if (!built.ok) return c.json({ error: built.error }, 400);
671
692
  await cfg.storage.put(key, built.flag);
672
693
  cache.invalidate();
694
+ c.header("ETag", flagEtag(built.flag));
673
695
  return c.json(built.flag);
674
696
  });
675
697
  app.delete(`${prefix}/flags/:key`, async (c) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@flaghoist/server",
3
- "version": "0.2.2",
3
+ "version": "0.3.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",
@@ -49,8 +49,8 @@
49
49
  "@flaghoist/core": "0.1.2"
50
50
  },
51
51
  "devDependencies": {
52
- "@flaghoist/adapter-memory": "0.1.2",
53
- "@flaghoist/dashboard": "0.0.0"
52
+ "@flaghoist/dashboard": "0.0.0",
53
+ "@flaghoist/adapter-memory": "0.1.2"
54
54
  },
55
55
  "publishConfig": {
56
56
  "access": "public"