@flaghoist/server 0.2.3 → 0.3.1
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 +34 -18
- package/dist/index.js +34 -18
- 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 = {
|
|
@@ -473,17 +479,19 @@ async function safeEqual(a, b) {
|
|
|
473
479
|
}
|
|
474
480
|
var MIN_SECRET_LENGTH = 16;
|
|
475
481
|
var warnedWeakSecrets = /* @__PURE__ */ new Set();
|
|
482
|
+
function weakSecretKey(secret) {
|
|
483
|
+
let hash = 5381;
|
|
484
|
+
for (let i = 0; i < secret.length; i++) hash = (hash << 5) + hash ^ secret.charCodeAt(i);
|
|
485
|
+
return hash >>> 0;
|
|
486
|
+
}
|
|
476
487
|
function warnIfWeakSecret(kind, secret) {
|
|
477
488
|
if (secret.length >= MIN_SECRET_LENGTH) return;
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
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
|
-
})();
|
|
489
|
+
const key = weakSecretKey(secret);
|
|
490
|
+
if (warnedWeakSecrets.has(key)) return;
|
|
491
|
+
warnedWeakSecrets.add(key);
|
|
492
|
+
console.warn(
|
|
493
|
+
`[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.`
|
|
494
|
+
);
|
|
487
495
|
}
|
|
488
496
|
function apiKey(expected) {
|
|
489
497
|
warnIfWeakSecret("read API key", expected);
|
|
@@ -690,6 +698,7 @@ function createFlagServer(config) {
|
|
|
690
698
|
if (!auth.ok) return c.json({ error: auth.message ?? "Unauthorized" }, auth.status ?? 401);
|
|
691
699
|
const flag = await cfg.storage.get(c.req.param("key"));
|
|
692
700
|
if (!flag) return c.json({ error: "Flag not found" }, 404);
|
|
701
|
+
c.header("ETag", flagEtag(flag));
|
|
693
702
|
return c.json(flag);
|
|
694
703
|
});
|
|
695
704
|
app.put(`${prefix}/flags/:key`, async (c) => {
|
|
@@ -697,17 +706,24 @@ function createFlagServer(config) {
|
|
|
697
706
|
const auth = await cfg.auth.admin(c.req.raw.headers);
|
|
698
707
|
if (!auth.ok) return c.json({ error: auth.message ?? "Unauthorized" }, auth.status ?? 401);
|
|
699
708
|
const key = c.req.param("key");
|
|
709
|
+
const existing = await cfg.storage.get(key);
|
|
710
|
+
const ifMatch = c.req.header("If-Match")?.trim();
|
|
711
|
+
if (ifMatch !== void 0) {
|
|
712
|
+
const precondition = ifMatch === "*" ? existing !== null : existing !== null && flagEtag(existing) === ifMatch;
|
|
713
|
+
if (!precondition) {
|
|
714
|
+
return c.json(
|
|
715
|
+
{ error: "This flag changed since you loaded it. Reload and reapply your change." },
|
|
716
|
+
412
|
|
717
|
+
);
|
|
718
|
+
}
|
|
719
|
+
}
|
|
700
720
|
const parsed = await readJsonBody(await c.req.text());
|
|
701
721
|
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
|
-
);
|
|
722
|
+
const built = buildFlag(key, parsed.value, auth.identity ?? "unknown", existing);
|
|
708
723
|
if (!built.ok) return c.json({ error: built.error }, 400);
|
|
709
724
|
await cfg.storage.put(key, built.flag);
|
|
710
725
|
cache.invalidate();
|
|
726
|
+
c.header("ETag", flagEtag(built.flag));
|
|
711
727
|
return c.json(built.flag);
|
|
712
728
|
});
|
|
713
729
|
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 = {
|
|
@@ -443,17 +449,19 @@ async function safeEqual(a, b) {
|
|
|
443
449
|
}
|
|
444
450
|
var MIN_SECRET_LENGTH = 16;
|
|
445
451
|
var warnedWeakSecrets = /* @__PURE__ */ new Set();
|
|
452
|
+
function weakSecretKey(secret) {
|
|
453
|
+
let hash = 5381;
|
|
454
|
+
for (let i = 0; i < secret.length; i++) hash = (hash << 5) + hash ^ secret.charCodeAt(i);
|
|
455
|
+
return hash >>> 0;
|
|
456
|
+
}
|
|
446
457
|
function warnIfWeakSecret(kind, secret) {
|
|
447
458
|
if (secret.length >= MIN_SECRET_LENGTH) return;
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
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
|
-
})();
|
|
459
|
+
const key = weakSecretKey(secret);
|
|
460
|
+
if (warnedWeakSecrets.has(key)) return;
|
|
461
|
+
warnedWeakSecrets.add(key);
|
|
462
|
+
console.warn(
|
|
463
|
+
`[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.`
|
|
464
|
+
);
|
|
457
465
|
}
|
|
458
466
|
function apiKey(expected) {
|
|
459
467
|
warnIfWeakSecret("read API key", expected);
|
|
@@ -660,6 +668,7 @@ function createFlagServer(config) {
|
|
|
660
668
|
if (!auth.ok) return c.json({ error: auth.message ?? "Unauthorized" }, auth.status ?? 401);
|
|
661
669
|
const flag = await cfg.storage.get(c.req.param("key"));
|
|
662
670
|
if (!flag) return c.json({ error: "Flag not found" }, 404);
|
|
671
|
+
c.header("ETag", flagEtag(flag));
|
|
663
672
|
return c.json(flag);
|
|
664
673
|
});
|
|
665
674
|
app.put(`${prefix}/flags/:key`, async (c) => {
|
|
@@ -667,17 +676,24 @@ function createFlagServer(config) {
|
|
|
667
676
|
const auth = await cfg.auth.admin(c.req.raw.headers);
|
|
668
677
|
if (!auth.ok) return c.json({ error: auth.message ?? "Unauthorized" }, auth.status ?? 401);
|
|
669
678
|
const key = c.req.param("key");
|
|
679
|
+
const existing = await cfg.storage.get(key);
|
|
680
|
+
const ifMatch = c.req.header("If-Match")?.trim();
|
|
681
|
+
if (ifMatch !== void 0) {
|
|
682
|
+
const precondition = ifMatch === "*" ? existing !== null : existing !== null && flagEtag(existing) === ifMatch;
|
|
683
|
+
if (!precondition) {
|
|
684
|
+
return c.json(
|
|
685
|
+
{ error: "This flag changed since you loaded it. Reload and reapply your change." },
|
|
686
|
+
412
|
|
687
|
+
);
|
|
688
|
+
}
|
|
689
|
+
}
|
|
670
690
|
const parsed = await readJsonBody(await c.req.text());
|
|
671
691
|
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
|
-
);
|
|
692
|
+
const built = buildFlag(key, parsed.value, auth.identity ?? "unknown", existing);
|
|
678
693
|
if (!built.ok) return c.json({ error: built.error }, 400);
|
|
679
694
|
await cfg.storage.put(key, built.flag);
|
|
680
695
|
cache.invalidate();
|
|
696
|
+
c.header("ETag", flagEtag(built.flag));
|
|
681
697
|
return c.json(built.flag);
|
|
682
698
|
});
|
|
683
699
|
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.1",
|
|
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/adapter-memory": "0.1.2",
|
|
53
|
+
"@flaghoist/dashboard": "0.0.0"
|
|
54
54
|
},
|
|
55
55
|
"publishConfig": {
|
|
56
56
|
"access": "public"
|