@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/dashboard.cjs +1 -1
- package/dist/dashboard.js +1 -1
- package/dist/index.cjs +31 -9
- package/dist/index.js +31 -9
- package/package.json +3 -3
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
|
|
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
|
|
79
|
+
createdAt,
|
|
77
80
|
updatedBy: identity,
|
|
78
|
-
updatedAt
|
|
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
|
|
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
|
|
49
|
+
createdAt,
|
|
47
50
|
updatedBy: identity,
|
|
48
|
-
updatedAt
|
|
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.
|
|
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/
|
|
53
|
-
"@flaghoist/
|
|
52
|
+
"@flaghoist/dashboard": "0.0.0",
|
|
53
|
+
"@flaghoist/adapter-memory": "0.1.2"
|
|
54
54
|
},
|
|
55
55
|
"publishConfig": {
|
|
56
56
|
"access": "public"
|