@flaghoist/server 0.2.3 → 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 = {
@@ -690,6 +696,7 @@ function createFlagServer(config) {
690
696
  if (!auth.ok) return c.json({ error: auth.message ?? "Unauthorized" }, auth.status ?? 401);
691
697
  const flag = await cfg.storage.get(c.req.param("key"));
692
698
  if (!flag) return c.json({ error: "Flag not found" }, 404);
699
+ c.header("ETag", flagEtag(flag));
693
700
  return c.json(flag);
694
701
  });
695
702
  app.put(`${prefix}/flags/:key`, async (c) => {
@@ -697,17 +704,24 @@ function createFlagServer(config) {
697
704
  const auth = await cfg.auth.admin(c.req.raw.headers);
698
705
  if (!auth.ok) return c.json({ error: auth.message ?? "Unauthorized" }, auth.status ?? 401);
699
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
+ }
700
718
  const parsed = await readJsonBody(await c.req.text());
701
719
  if (!parsed.ok) return c.json({ error: parsed.message }, parsed.status);
702
- const built = buildFlag(
703
- key,
704
- parsed.value,
705
- auth.identity ?? "unknown",
706
- await cfg.storage.get(key)
707
- );
720
+ const built = buildFlag(key, parsed.value, auth.identity ?? "unknown", existing);
708
721
  if (!built.ok) return c.json({ error: built.error }, 400);
709
722
  await cfg.storage.put(key, built.flag);
710
723
  cache.invalidate();
724
+ c.header("ETag", flagEtag(built.flag));
711
725
  return c.json(built.flag);
712
726
  });
713
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 = {
@@ -660,6 +666,7 @@ function createFlagServer(config) {
660
666
  if (!auth.ok) return c.json({ error: auth.message ?? "Unauthorized" }, auth.status ?? 401);
661
667
  const flag = await cfg.storage.get(c.req.param("key"));
662
668
  if (!flag) return c.json({ error: "Flag not found" }, 404);
669
+ c.header("ETag", flagEtag(flag));
663
670
  return c.json(flag);
664
671
  });
665
672
  app.put(`${prefix}/flags/:key`, async (c) => {
@@ -667,17 +674,24 @@ function createFlagServer(config) {
667
674
  const auth = await cfg.auth.admin(c.req.raw.headers);
668
675
  if (!auth.ok) return c.json({ error: auth.message ?? "Unauthorized" }, auth.status ?? 401);
669
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
+ }
670
688
  const parsed = await readJsonBody(await c.req.text());
671
689
  if (!parsed.ok) return c.json({ error: parsed.message }, parsed.status);
672
- const built = buildFlag(
673
- key,
674
- parsed.value,
675
- auth.identity ?? "unknown",
676
- await cfg.storage.get(key)
677
- );
690
+ const built = buildFlag(key, parsed.value, auth.identity ?? "unknown", existing);
678
691
  if (!built.ok) return c.json({ error: built.error }, 400);
679
692
  await cfg.storage.put(key, built.flag);
680
693
  cache.invalidate();
694
+ c.header("ETag", flagEtag(built.flag));
681
695
  return c.json(built.flag);
682
696
  });
683
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.3",
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",