fest-gateway 0.5.1 → 0.6.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/README.md +2 -0
- package/dist/cli/claude.js +10 -4
- package/dist/cli/claude.js.map +1 -1
- package/dist/cli/codex.js +8 -4
- package/dist/cli/codex.js.map +1 -1
- package/dist/cli/copilot.js +11 -5
- package/dist/cli/copilot.js.map +1 -1
- package/dist/cli/tags.js +258 -0
- package/dist/cli/tags.js.map +1 -0
- package/dist/server/api/live.js +1 -0
- package/dist/server/api/live.js.map +1 -1
- package/dist/server/api/routes.js +23 -2
- package/dist/server/api/routes.js.map +1 -1
- package/dist/server/auth/posture.js +39 -10
- package/dist/server/auth/posture.js.map +1 -1
- package/dist/server/bin/fest.js +29 -12
- package/dist/server/bin/fest.js.map +1 -1
- package/dist/server/http/body.js +42 -2
- package/dist/server/http/body.js.map +1 -1
- package/dist/server/pipeline/dispatch.js +1 -0
- package/dist/server/pipeline/dispatch.js.map +1 -1
- package/dist/server/pipeline/passthrough.js +13 -4
- package/dist/server/pipeline/passthrough.js.map +1 -1
- package/dist/server/pipeline/record.js +1 -0
- package/dist/server/pipeline/record.js.map +1 -1
- package/dist/server/store/migrations/007-tags.sql +94 -0
- package/dist/server/store/queries.js +112 -6
- package/dist/server/store/queries.js.map +1 -1
- package/dist/server/store/seed.js +17 -0
- package/dist/server/store/seed.js.map +1 -1
- package/dist/server/store/write.js +9 -2
- package/dist/server/store/write.js.map +1 -1
- package/dist/shared/tags.js +155 -0
- package/dist/shared/tags.js.map +1 -0
- package/dist/web/dist/assets/index-AJc39GYv.js +56 -0
- package/dist/web/dist/assets/index-AJc39GYv.js.map +1 -0
- package/dist/web/dist/assets/index-BgK6vcz0.css +2 -0
- package/dist/web/dist/index.html +2 -2
- package/package.json +1 -1
- package/dist/web/dist/assets/index-C7n3_lX2.css +0 -2
- package/dist/web/dist/assets/index-DJb4NfM5.js +0 -56
- package/dist/web/dist/assets/index-DJb4NfM5.js.map +0 -1
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Request tags: free-form `key=value` pairs a client declares about its own
|
|
3
|
+
* traffic (`team=platform,project=fest`), so usage can be grouped and filtered
|
|
4
|
+
* by something other than who sent it.
|
|
5
|
+
*
|
|
6
|
+
* Tags are self-declared, like `x-fest-client`. They are labels for reporting,
|
|
7
|
+
* never an input to authorisation or routing — a client that lies about its
|
|
8
|
+
* project mislabels its own usage and nothing else.
|
|
9
|
+
*
|
|
10
|
+
* Two carriers, one format:
|
|
11
|
+
* - header `x-fest-tags: team=platform,project=fest`
|
|
12
|
+
* - path `/t/<tok>/~team=platform,project=fest/v1/messages`
|
|
13
|
+
* The path form exists because Copilot on its own login can set a base URL but
|
|
14
|
+
* no headers. Both are stripped before anything reaches upstream.
|
|
15
|
+
*
|
|
16
|
+
* Shared by the server (parsing what arrives) and the CLI (building what it
|
|
17
|
+
* sends), so the two cannot disagree about what a valid tag is.
|
|
18
|
+
*/
|
|
19
|
+
export const MAX_TAGS = 8;
|
|
20
|
+
export const MAX_TAG_VALUE_LENGTH = 64;
|
|
21
|
+
const KEY = /^[a-z0-9_.-]{1,32}$/;
|
|
22
|
+
/**
|
|
23
|
+
* No `,` `=` `%` or `/` survives decoding: `,` and `=` are the separators, and
|
|
24
|
+
* a `/` in a stored value would be ambiguous in the path form. Spaces are
|
|
25
|
+
* allowed for human labels ("Platform team"); the URL form percent-encodes them.
|
|
26
|
+
*/
|
|
27
|
+
const VALUE = /^[A-Za-z0-9 ._:@-]+$/;
|
|
28
|
+
function decode(s) {
|
|
29
|
+
try {
|
|
30
|
+
return decodeURIComponent(s);
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* `team=platform,project=fest` → pairs, in order, duplicates kept (last wins
|
|
38
|
+
* when folded). Each half is URL-decoded. Invalid pairs are dropped silently:
|
|
39
|
+
* a malformed tag must never fail the request it rides on.
|
|
40
|
+
*
|
|
41
|
+
* An empty value is kept here (the CLI uses `project=` to suppress a default);
|
|
42
|
+
* {@link parseTags} drops it.
|
|
43
|
+
*/
|
|
44
|
+
export function parseTagPairs(raw) {
|
|
45
|
+
const pairs = [];
|
|
46
|
+
for (const part of raw.split(",")) {
|
|
47
|
+
const eq = part.indexOf("=");
|
|
48
|
+
if (eq <= 0)
|
|
49
|
+
continue;
|
|
50
|
+
const key = decode(part.slice(0, eq).trim())?.trim().toLowerCase();
|
|
51
|
+
const value = decode(part.slice(eq + 1).trim())?.trim();
|
|
52
|
+
if (key === undefined || value === undefined)
|
|
53
|
+
continue;
|
|
54
|
+
if (!KEY.test(key))
|
|
55
|
+
continue;
|
|
56
|
+
if (value !== "" && (value.length > MAX_TAG_VALUE_LENGTH || !VALUE.test(value)))
|
|
57
|
+
continue;
|
|
58
|
+
pairs.push({ key, value });
|
|
59
|
+
}
|
|
60
|
+
return pairs;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Fold pairs into tags: last wins per key, and an empty value removes the key.
|
|
64
|
+
* Layers merge by concatenating their pairs lowest-precedence first, so a
|
|
65
|
+
* higher layer's `project=` suppresses a lower layer's `project=fest`.
|
|
66
|
+
*/
|
|
67
|
+
export function tagsFromPairs(pairs) {
|
|
68
|
+
const out = {};
|
|
69
|
+
for (const { key, value } of pairs) {
|
|
70
|
+
if (value === "") {
|
|
71
|
+
delete out[key];
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
if (!(key in out) && Object.keys(out).length >= MAX_TAGS)
|
|
75
|
+
continue;
|
|
76
|
+
out[key] = value;
|
|
77
|
+
}
|
|
78
|
+
return out;
|
|
79
|
+
}
|
|
80
|
+
export function parseTags(raw) {
|
|
81
|
+
return tagsFromPairs(parseTagPairs(raw));
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* The one stored form: a JSON object with sorted keys, or `""` for no tags.
|
|
85
|
+
*
|
|
86
|
+
* Sorted, because the rollup keys on this string — `{"a":"1","b":"2"}` and
|
|
87
|
+
* `{"b":"2","a":"1"}` must land in the same bucket.
|
|
88
|
+
*/
|
|
89
|
+
export function canonicalTags(tags) {
|
|
90
|
+
if (tags === null || tags === undefined)
|
|
91
|
+
return "";
|
|
92
|
+
const keys = Object.keys(tags).sort();
|
|
93
|
+
if (keys.length === 0)
|
|
94
|
+
return "";
|
|
95
|
+
const sorted = {};
|
|
96
|
+
for (const k of keys)
|
|
97
|
+
sorted[k] = tags[k];
|
|
98
|
+
return JSON.stringify(sorted);
|
|
99
|
+
}
|
|
100
|
+
/** Inverse of {@link canonicalTags}; anything unreadable is no tags. */
|
|
101
|
+
export function tagsFromCanonical(s) {
|
|
102
|
+
if (s === null || s === undefined || s === "")
|
|
103
|
+
return {};
|
|
104
|
+
try {
|
|
105
|
+
const parsed = JSON.parse(s);
|
|
106
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed))
|
|
107
|
+
return {};
|
|
108
|
+
const out = {};
|
|
109
|
+
for (const [k, v] of Object.entries(parsed))
|
|
110
|
+
if (typeof v === "string")
|
|
111
|
+
out[k] = v;
|
|
112
|
+
return out;
|
|
113
|
+
}
|
|
114
|
+
catch {
|
|
115
|
+
return {};
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
/** `team=platform,project=fest` with each half URL-encoded — the wire form. */
|
|
119
|
+
export function formatTags(tags) {
|
|
120
|
+
return Object.keys(tags)
|
|
121
|
+
.sort()
|
|
122
|
+
.map((k) => `${encodeURIComponent(k)}=${encodeURIComponent(tags[k])}`)
|
|
123
|
+
.join(",");
|
|
124
|
+
}
|
|
125
|
+
/** The path segment, `~team=platform,project=fest`, or `""` for no tags. */
|
|
126
|
+
export function tagPathSegment(tags) {
|
|
127
|
+
const s = formatTags(tags);
|
|
128
|
+
return s === "" ? "" : `~${s}`;
|
|
129
|
+
}
|
|
130
|
+
/** `["team:platform", "project:fest"]` → filter. Invalid entries dropped. */
|
|
131
|
+
export function parseTagFilter(values) {
|
|
132
|
+
const out = {};
|
|
133
|
+
for (const v of values) {
|
|
134
|
+
const colon = v.indexOf(":");
|
|
135
|
+
if (colon <= 0)
|
|
136
|
+
continue;
|
|
137
|
+
const key = v.slice(0, colon).trim().toLowerCase();
|
|
138
|
+
const value = v.slice(colon + 1).trim();
|
|
139
|
+
if (!KEY.test(key) || value === "" || value.length > MAX_TAG_VALUE_LENGTH || !VALUE.test(value))
|
|
140
|
+
continue;
|
|
141
|
+
if (!(key in out) && Object.keys(out).length >= MAX_TAGS)
|
|
142
|
+
continue;
|
|
143
|
+
out[key] = value;
|
|
144
|
+
}
|
|
145
|
+
return out;
|
|
146
|
+
}
|
|
147
|
+
export function formatTagFilter(filter) {
|
|
148
|
+
return Object.keys(filter)
|
|
149
|
+
.sort()
|
|
150
|
+
.map((k) => `${k}:${filter[k]}`);
|
|
151
|
+
}
|
|
152
|
+
export function isValidTagKey(key) {
|
|
153
|
+
return KEY.test(key);
|
|
154
|
+
}
|
|
155
|
+
//# sourceMappingURL=tags.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tags.js","sourceRoot":"","sources":["../../shared/tags.ts"],"names":[],"mappings":"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"}
|