@r_masseater/ops-harbor 0.1.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/README.md +11 -0
- package/dist/cli.js +19186 -0
- package/dist/client/assets/index-0Ytuj-W0.css +1 -0
- package/dist/client/assets/index-DyzvRtB8.js +51 -0
- package/dist/client/index.html +26 -0
- package/dist/control-plane.js +3573 -0
- package/dist/mcp-server.js +13967 -0
- package/package.json +72 -0
|
@@ -0,0 +1,3573 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
// @bun
|
|
3
|
+
var __require = import.meta.require;
|
|
4
|
+
|
|
5
|
+
// ../../node_modules/.bun/valibot@1.3.1+8e24a2f921b8d7be/node_modules/valibot/dist/index.mjs
|
|
6
|
+
var store$4;
|
|
7
|
+
function getGlobalConfig(config$1) {
|
|
8
|
+
return {
|
|
9
|
+
lang: config$1?.lang ?? store$4?.lang,
|
|
10
|
+
message: config$1?.message,
|
|
11
|
+
abortEarly: config$1?.abortEarly ?? store$4?.abortEarly,
|
|
12
|
+
abortPipeEarly: config$1?.abortPipeEarly ?? store$4?.abortPipeEarly
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
var store$3;
|
|
16
|
+
function getGlobalMessage(lang) {
|
|
17
|
+
return store$3?.get(lang);
|
|
18
|
+
}
|
|
19
|
+
var store$2;
|
|
20
|
+
function getSchemaMessage(lang) {
|
|
21
|
+
return store$2?.get(lang);
|
|
22
|
+
}
|
|
23
|
+
var store$1;
|
|
24
|
+
function getSpecificMessage(reference, lang) {
|
|
25
|
+
return store$1?.get(reference)?.get(lang);
|
|
26
|
+
}
|
|
27
|
+
function _stringify(input) {
|
|
28
|
+
const type = typeof input;
|
|
29
|
+
if (type === "string")
|
|
30
|
+
return `"${input}"`;
|
|
31
|
+
if (type === "number" || type === "bigint" || type === "boolean")
|
|
32
|
+
return `${input}`;
|
|
33
|
+
if (type === "object" || type === "function")
|
|
34
|
+
return (input && Object.getPrototypeOf(input)?.constructor?.name) ?? "null";
|
|
35
|
+
return type;
|
|
36
|
+
}
|
|
37
|
+
function _addIssue(context, label, dataset, config$1, other) {
|
|
38
|
+
const input = other && "input" in other ? other.input : dataset.value;
|
|
39
|
+
const expected = other?.expected ?? context.expects ?? null;
|
|
40
|
+
const received = other?.received ?? /* @__PURE__ */ _stringify(input);
|
|
41
|
+
const issue = {
|
|
42
|
+
kind: context.kind,
|
|
43
|
+
type: context.type,
|
|
44
|
+
input,
|
|
45
|
+
expected,
|
|
46
|
+
received,
|
|
47
|
+
message: `Invalid ${label}: ${expected ? `Expected ${expected} but r` : "R"}eceived ${received}`,
|
|
48
|
+
requirement: context.requirement,
|
|
49
|
+
path: other?.path,
|
|
50
|
+
issues: other?.issues,
|
|
51
|
+
lang: config$1.lang,
|
|
52
|
+
abortEarly: config$1.abortEarly,
|
|
53
|
+
abortPipeEarly: config$1.abortPipeEarly
|
|
54
|
+
};
|
|
55
|
+
const isSchema = context.kind === "schema";
|
|
56
|
+
const message$1 = other?.message ?? context.message ?? /* @__PURE__ */ getSpecificMessage(context.reference, issue.lang) ?? (isSchema ? /* @__PURE__ */ getSchemaMessage(issue.lang) : null) ?? config$1.message ?? /* @__PURE__ */ getGlobalMessage(issue.lang);
|
|
57
|
+
if (message$1 !== undefined)
|
|
58
|
+
issue.message = typeof message$1 === "function" ? message$1(issue) : message$1;
|
|
59
|
+
if (isSchema)
|
|
60
|
+
dataset.typed = false;
|
|
61
|
+
if (dataset.issues)
|
|
62
|
+
dataset.issues.push(issue);
|
|
63
|
+
else
|
|
64
|
+
dataset.issues = [issue];
|
|
65
|
+
}
|
|
66
|
+
function _getStandardProps(context) {
|
|
67
|
+
return {
|
|
68
|
+
version: 1,
|
|
69
|
+
vendor: "valibot",
|
|
70
|
+
validate(value$1) {
|
|
71
|
+
return context["~run"]({ value: value$1 }, /* @__PURE__ */ getGlobalConfig());
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
var ValiError = class extends Error {
|
|
76
|
+
constructor(issues) {
|
|
77
|
+
super(issues[0].message);
|
|
78
|
+
this.name = "ValiError";
|
|
79
|
+
this.issues = issues;
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
function integer(message$1) {
|
|
83
|
+
return {
|
|
84
|
+
kind: "validation",
|
|
85
|
+
type: "integer",
|
|
86
|
+
reference: integer,
|
|
87
|
+
async: false,
|
|
88
|
+
expects: null,
|
|
89
|
+
requirement: Number.isInteger,
|
|
90
|
+
message: message$1,
|
|
91
|
+
"~run"(dataset, config$1) {
|
|
92
|
+
if (dataset.typed && !this.requirement(dataset.value))
|
|
93
|
+
_addIssue(this, "integer", dataset, config$1);
|
|
94
|
+
return dataset;
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
function maxValue(requirement, message$1) {
|
|
99
|
+
return {
|
|
100
|
+
kind: "validation",
|
|
101
|
+
type: "max_value",
|
|
102
|
+
reference: maxValue,
|
|
103
|
+
async: false,
|
|
104
|
+
expects: `<=${requirement instanceof Date ? requirement.toJSON() : /* @__PURE__ */ _stringify(requirement)}`,
|
|
105
|
+
requirement,
|
|
106
|
+
message: message$1,
|
|
107
|
+
"~run"(dataset, config$1) {
|
|
108
|
+
if (dataset.typed && !(dataset.value <= this.requirement))
|
|
109
|
+
_addIssue(this, "value", dataset, config$1, { received: dataset.value instanceof Date ? dataset.value.toJSON() : /* @__PURE__ */ _stringify(dataset.value) });
|
|
110
|
+
return dataset;
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
function minLength(requirement, message$1) {
|
|
115
|
+
return {
|
|
116
|
+
kind: "validation",
|
|
117
|
+
type: "min_length",
|
|
118
|
+
reference: minLength,
|
|
119
|
+
async: false,
|
|
120
|
+
expects: `>=${requirement}`,
|
|
121
|
+
requirement,
|
|
122
|
+
message: message$1,
|
|
123
|
+
"~run"(dataset, config$1) {
|
|
124
|
+
if (dataset.typed && dataset.value.length < this.requirement)
|
|
125
|
+
_addIssue(this, "length", dataset, config$1, { received: `${dataset.value.length}` });
|
|
126
|
+
return dataset;
|
|
127
|
+
}
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
function minValue(requirement, message$1) {
|
|
131
|
+
return {
|
|
132
|
+
kind: "validation",
|
|
133
|
+
type: "min_value",
|
|
134
|
+
reference: minValue,
|
|
135
|
+
async: false,
|
|
136
|
+
expects: `>=${requirement instanceof Date ? requirement.toJSON() : /* @__PURE__ */ _stringify(requirement)}`,
|
|
137
|
+
requirement,
|
|
138
|
+
message: message$1,
|
|
139
|
+
"~run"(dataset, config$1) {
|
|
140
|
+
if (dataset.typed && !(dataset.value >= this.requirement))
|
|
141
|
+
_addIssue(this, "value", dataset, config$1, { received: dataset.value instanceof Date ? dataset.value.toJSON() : /* @__PURE__ */ _stringify(dataset.value) });
|
|
142
|
+
return dataset;
|
|
143
|
+
}
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
var _LruCache = class {
|
|
147
|
+
constructor(config$1) {
|
|
148
|
+
this.refCount = 0;
|
|
149
|
+
this.maxSize = config$1?.maxSize ?? 1000;
|
|
150
|
+
this.maxAge = config$1?.maxAge ?? Infinity;
|
|
151
|
+
this.hasMaxAge = isFinite(this.maxAge);
|
|
152
|
+
}
|
|
153
|
+
#stringify(input) {
|
|
154
|
+
const type = typeof input;
|
|
155
|
+
if (type === "string")
|
|
156
|
+
return `"${input}"`;
|
|
157
|
+
if (type === "number" || type === "boolean")
|
|
158
|
+
return `${input}`;
|
|
159
|
+
if (type === "bigint")
|
|
160
|
+
return `${input}n`;
|
|
161
|
+
if (type === "object" || type === "function") {
|
|
162
|
+
if (input) {
|
|
163
|
+
this.refIds ??= /* @__PURE__ */ new WeakMap;
|
|
164
|
+
let id = this.refIds.get(input);
|
|
165
|
+
if (!id) {
|
|
166
|
+
id = ++this.refCount;
|
|
167
|
+
this.refIds.set(input, id);
|
|
168
|
+
}
|
|
169
|
+
return `#${id}`;
|
|
170
|
+
}
|
|
171
|
+
return "null";
|
|
172
|
+
}
|
|
173
|
+
return type;
|
|
174
|
+
}
|
|
175
|
+
key(input, config$1 = {}) {
|
|
176
|
+
return `${this.#stringify(input)}|${this.#stringify(config$1.lang)}|${this.#stringify(config$1.message)}|${this.#stringify(config$1.abortEarly)}|${this.#stringify(config$1.abortPipeEarly)}`;
|
|
177
|
+
}
|
|
178
|
+
get(key) {
|
|
179
|
+
if (!this.store)
|
|
180
|
+
return;
|
|
181
|
+
const entry = this.store.get(key);
|
|
182
|
+
if (!entry)
|
|
183
|
+
return;
|
|
184
|
+
if (this.hasMaxAge && Date.now() - entry[1] > this.maxAge) {
|
|
185
|
+
this.store.delete(key);
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
this.store.delete(key);
|
|
189
|
+
this.store.set(key, entry);
|
|
190
|
+
return entry[0];
|
|
191
|
+
}
|
|
192
|
+
set(key, value$1) {
|
|
193
|
+
this.store ??= /* @__PURE__ */ new Map;
|
|
194
|
+
this.store.delete(key);
|
|
195
|
+
const timestamp = this.hasMaxAge ? Date.now() : 0;
|
|
196
|
+
this.store.set(key, [value$1, timestamp]);
|
|
197
|
+
if (this.store.size > this.maxSize)
|
|
198
|
+
this.store.delete(this.store.keys().next().value);
|
|
199
|
+
}
|
|
200
|
+
clear() {
|
|
201
|
+
this.store?.clear();
|
|
202
|
+
}
|
|
203
|
+
};
|
|
204
|
+
function getFallback(schema, dataset, config$1) {
|
|
205
|
+
return typeof schema.fallback === "function" ? schema.fallback(dataset, config$1) : schema.fallback;
|
|
206
|
+
}
|
|
207
|
+
function getDefault(schema, dataset, config$1) {
|
|
208
|
+
return typeof schema.default === "function" ? schema.default(dataset, config$1) : schema.default;
|
|
209
|
+
}
|
|
210
|
+
function boolean(message$1) {
|
|
211
|
+
return {
|
|
212
|
+
kind: "schema",
|
|
213
|
+
type: "boolean",
|
|
214
|
+
reference: boolean,
|
|
215
|
+
expects: "boolean",
|
|
216
|
+
async: false,
|
|
217
|
+
message: message$1,
|
|
218
|
+
get "~standard"() {
|
|
219
|
+
return /* @__PURE__ */ _getStandardProps(this);
|
|
220
|
+
},
|
|
221
|
+
"~run"(dataset, config$1) {
|
|
222
|
+
if (typeof dataset.value === "boolean")
|
|
223
|
+
dataset.typed = true;
|
|
224
|
+
else
|
|
225
|
+
_addIssue(this, "type", dataset, config$1);
|
|
226
|
+
return dataset;
|
|
227
|
+
}
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
function number(message$1) {
|
|
231
|
+
return {
|
|
232
|
+
kind: "schema",
|
|
233
|
+
type: "number",
|
|
234
|
+
reference: number,
|
|
235
|
+
expects: "number",
|
|
236
|
+
async: false,
|
|
237
|
+
message: message$1,
|
|
238
|
+
get "~standard"() {
|
|
239
|
+
return /* @__PURE__ */ _getStandardProps(this);
|
|
240
|
+
},
|
|
241
|
+
"~run"(dataset, config$1) {
|
|
242
|
+
if (typeof dataset.value === "number" && !isNaN(dataset.value))
|
|
243
|
+
dataset.typed = true;
|
|
244
|
+
else
|
|
245
|
+
_addIssue(this, "type", dataset, config$1);
|
|
246
|
+
return dataset;
|
|
247
|
+
}
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
function object(entries$1, message$1) {
|
|
251
|
+
return {
|
|
252
|
+
kind: "schema",
|
|
253
|
+
type: "object",
|
|
254
|
+
reference: object,
|
|
255
|
+
expects: "Object",
|
|
256
|
+
async: false,
|
|
257
|
+
entries: entries$1,
|
|
258
|
+
message: message$1,
|
|
259
|
+
get "~standard"() {
|
|
260
|
+
return /* @__PURE__ */ _getStandardProps(this);
|
|
261
|
+
},
|
|
262
|
+
"~run"(dataset, config$1) {
|
|
263
|
+
const input = dataset.value;
|
|
264
|
+
if (input && typeof input === "object") {
|
|
265
|
+
dataset.typed = true;
|
|
266
|
+
dataset.value = {};
|
|
267
|
+
for (const key in this.entries) {
|
|
268
|
+
const valueSchema = this.entries[key];
|
|
269
|
+
if (key in input || (valueSchema.type === "exact_optional" || valueSchema.type === "optional" || valueSchema.type === "nullish") && valueSchema.default !== undefined) {
|
|
270
|
+
const value$1 = key in input ? input[key] : /* @__PURE__ */ getDefault(valueSchema);
|
|
271
|
+
const valueDataset = valueSchema["~run"]({ value: value$1 }, config$1);
|
|
272
|
+
if (valueDataset.issues) {
|
|
273
|
+
const pathItem = {
|
|
274
|
+
type: "object",
|
|
275
|
+
origin: "value",
|
|
276
|
+
input,
|
|
277
|
+
key,
|
|
278
|
+
value: value$1
|
|
279
|
+
};
|
|
280
|
+
for (const issue of valueDataset.issues) {
|
|
281
|
+
if (issue.path)
|
|
282
|
+
issue.path.unshift(pathItem);
|
|
283
|
+
else
|
|
284
|
+
issue.path = [pathItem];
|
|
285
|
+
dataset.issues?.push(issue);
|
|
286
|
+
}
|
|
287
|
+
if (!dataset.issues)
|
|
288
|
+
dataset.issues = valueDataset.issues;
|
|
289
|
+
if (config$1.abortEarly) {
|
|
290
|
+
dataset.typed = false;
|
|
291
|
+
break;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
if (!valueDataset.typed)
|
|
295
|
+
dataset.typed = false;
|
|
296
|
+
dataset.value[key] = valueDataset.value;
|
|
297
|
+
} else if (valueSchema.fallback !== undefined)
|
|
298
|
+
dataset.value[key] = /* @__PURE__ */ getFallback(valueSchema);
|
|
299
|
+
else if (valueSchema.type !== "exact_optional" && valueSchema.type !== "optional" && valueSchema.type !== "nullish") {
|
|
300
|
+
_addIssue(this, "key", dataset, config$1, {
|
|
301
|
+
input: undefined,
|
|
302
|
+
expected: `"${key}"`,
|
|
303
|
+
path: [{
|
|
304
|
+
type: "object",
|
|
305
|
+
origin: "key",
|
|
306
|
+
input,
|
|
307
|
+
key,
|
|
308
|
+
value: input[key]
|
|
309
|
+
}]
|
|
310
|
+
});
|
|
311
|
+
if (config$1.abortEarly)
|
|
312
|
+
break;
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
} else
|
|
316
|
+
_addIssue(this, "type", dataset, config$1);
|
|
317
|
+
return dataset;
|
|
318
|
+
}
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
function optional(wrapped, default_) {
|
|
322
|
+
return {
|
|
323
|
+
kind: "schema",
|
|
324
|
+
type: "optional",
|
|
325
|
+
reference: optional,
|
|
326
|
+
expects: `(${wrapped.expects} | undefined)`,
|
|
327
|
+
async: false,
|
|
328
|
+
wrapped,
|
|
329
|
+
default: default_,
|
|
330
|
+
get "~standard"() {
|
|
331
|
+
return /* @__PURE__ */ _getStandardProps(this);
|
|
332
|
+
},
|
|
333
|
+
"~run"(dataset, config$1) {
|
|
334
|
+
if (dataset.value === undefined) {
|
|
335
|
+
if (this.default !== undefined)
|
|
336
|
+
dataset.value = /* @__PURE__ */ getDefault(this, dataset, config$1);
|
|
337
|
+
if (dataset.value === undefined) {
|
|
338
|
+
dataset.typed = true;
|
|
339
|
+
return dataset;
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
return this.wrapped["~run"](dataset, config$1);
|
|
343
|
+
}
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
function string(message$1) {
|
|
347
|
+
return {
|
|
348
|
+
kind: "schema",
|
|
349
|
+
type: "string",
|
|
350
|
+
reference: string,
|
|
351
|
+
expects: "string",
|
|
352
|
+
async: false,
|
|
353
|
+
message: message$1,
|
|
354
|
+
get "~standard"() {
|
|
355
|
+
return /* @__PURE__ */ _getStandardProps(this);
|
|
356
|
+
},
|
|
357
|
+
"~run"(dataset, config$1) {
|
|
358
|
+
if (typeof dataset.value === "string")
|
|
359
|
+
dataset.typed = true;
|
|
360
|
+
else
|
|
361
|
+
_addIssue(this, "type", dataset, config$1);
|
|
362
|
+
return dataset;
|
|
363
|
+
}
|
|
364
|
+
};
|
|
365
|
+
}
|
|
366
|
+
function parse(schema, input, config$1) {
|
|
367
|
+
const dataset = schema["~run"]({ value: input }, /* @__PURE__ */ getGlobalConfig(config$1));
|
|
368
|
+
if (dataset.issues)
|
|
369
|
+
throw new ValiError(dataset.issues);
|
|
370
|
+
return dataset.value;
|
|
371
|
+
}
|
|
372
|
+
function pipe(...pipe$1) {
|
|
373
|
+
return {
|
|
374
|
+
...pipe$1[0],
|
|
375
|
+
pipe: pipe$1,
|
|
376
|
+
get "~standard"() {
|
|
377
|
+
return /* @__PURE__ */ _getStandardProps(this);
|
|
378
|
+
},
|
|
379
|
+
"~run"(dataset, config$1) {
|
|
380
|
+
for (const item of pipe$1)
|
|
381
|
+
if (item.kind !== "metadata") {
|
|
382
|
+
if (dataset.issues && (item.kind === "schema" || item.kind === "transformation")) {
|
|
383
|
+
dataset.typed = false;
|
|
384
|
+
break;
|
|
385
|
+
}
|
|
386
|
+
if (!dataset.issues || !config$1.abortEarly && !config$1.abortPipeEarly)
|
|
387
|
+
dataset = item["~run"](dataset, config$1);
|
|
388
|
+
}
|
|
389
|
+
return dataset;
|
|
390
|
+
}
|
|
391
|
+
};
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
// ../ops-harbor-control-plane/src/lib/config.ts
|
|
395
|
+
import { randomBytes } from "crypto";
|
|
396
|
+
import { existsSync, readFileSync } from "fs";
|
|
397
|
+
import { mkdir, writeFile } from "fs/promises";
|
|
398
|
+
import { homedir } from "os";
|
|
399
|
+
import { dirname, join } from "path";
|
|
400
|
+
var EnvSchema = object({
|
|
401
|
+
XDG_CONFIG_HOME: optional(pipe(string(), minLength(1))),
|
|
402
|
+
XDG_STATE_HOME: optional(pipe(string(), minLength(1)))
|
|
403
|
+
});
|
|
404
|
+
var StoredConfigSchema = object({
|
|
405
|
+
port: optional(pipe(number(), integer(), minValue(1), maxValue(65535))),
|
|
406
|
+
dbPath: optional(pipe(string(), minLength(1))),
|
|
407
|
+
githubAppId: optional(pipe(string(), minLength(1))),
|
|
408
|
+
githubPrivateKey: optional(pipe(string(), minLength(1))),
|
|
409
|
+
githubWebhookSecret: optional(pipe(string(), minLength(1))),
|
|
410
|
+
githubTunnelDisabled: optional(boolean()),
|
|
411
|
+
githubTunnelHost: optional(pipe(string(), minLength(1))),
|
|
412
|
+
internalApiToken: optional(pipe(string(), minLength(1))),
|
|
413
|
+
githubApiUrl: optional(pipe(string(), minLength(1))),
|
|
414
|
+
defaultAuthor: optional(pipe(string(), minLength(1)))
|
|
415
|
+
});
|
|
416
|
+
function readProcessEnv(env = process.env) {
|
|
417
|
+
return parse(EnvSchema, env);
|
|
418
|
+
}
|
|
419
|
+
function configPath(env) {
|
|
420
|
+
return join(env.XDG_CONFIG_HOME ?? join(homedir(), ".config"), "ops-harbor", "control-plane.json");
|
|
421
|
+
}
|
|
422
|
+
function normalizeStoredConfig(config) {
|
|
423
|
+
return {
|
|
424
|
+
...config.port !== undefined ? { port: config.port } : {},
|
|
425
|
+
...config.dbPath ? { dbPath: config.dbPath } : {},
|
|
426
|
+
...config.githubAppId ? { githubAppId: config.githubAppId } : {},
|
|
427
|
+
...config.githubPrivateKey ? { githubPrivateKey: config.githubPrivateKey } : {},
|
|
428
|
+
...config.githubWebhookSecret ? { githubWebhookSecret: config.githubWebhookSecret } : {},
|
|
429
|
+
...config.githubTunnelDisabled !== undefined ? { githubTunnelDisabled: config.githubTunnelDisabled } : {},
|
|
430
|
+
...config.githubTunnelHost ? { githubTunnelHost: config.githubTunnelHost } : {},
|
|
431
|
+
...config.internalApiToken ? { internalApiToken: config.internalApiToken } : {},
|
|
432
|
+
...config.githubApiUrl ? { githubApiUrl: config.githubApiUrl } : {},
|
|
433
|
+
...config.defaultAuthor ? { defaultAuthor: config.defaultAuthor } : {}
|
|
434
|
+
};
|
|
435
|
+
}
|
|
436
|
+
function loadStoredConfig(env = readProcessEnv()) {
|
|
437
|
+
const path = configPath(env);
|
|
438
|
+
if (!existsSync(path)) {
|
|
439
|
+
return {};
|
|
440
|
+
}
|
|
441
|
+
try {
|
|
442
|
+
return normalizeStoredConfig(parse(StoredConfigSchema, JSON.parse(readFileSync(path, "utf-8"))));
|
|
443
|
+
} catch {
|
|
444
|
+
return {};
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
function generateWebhookSecret() {
|
|
448
|
+
return randomBytes(32).toString("hex");
|
|
449
|
+
}
|
|
450
|
+
function shouldProvisionWebhookSecret(config) {
|
|
451
|
+
return Boolean(config.githubAppId && config.githubPrivateKey);
|
|
452
|
+
}
|
|
453
|
+
async function saveStoredConfig(config, env = readProcessEnv()) {
|
|
454
|
+
const current = loadStoredConfig(env);
|
|
455
|
+
const parsed = normalizeStoredConfig(parse(StoredConfigSchema, config));
|
|
456
|
+
const resolved = {
|
|
457
|
+
...parsed,
|
|
458
|
+
...shouldProvisionWebhookSecret(parsed) ? {
|
|
459
|
+
githubWebhookSecret: parsed.githubWebhookSecret ?? current.githubWebhookSecret ?? generateWebhookSecret()
|
|
460
|
+
} : {}
|
|
461
|
+
};
|
|
462
|
+
const path = configPath(env);
|
|
463
|
+
await mkdir(dirname(path), { recursive: true });
|
|
464
|
+
await writeFile(path, `${JSON.stringify(resolved, null, 2)}
|
|
465
|
+
`, {
|
|
466
|
+
encoding: "utf-8",
|
|
467
|
+
mode: 384
|
|
468
|
+
});
|
|
469
|
+
return resolved;
|
|
470
|
+
}
|
|
471
|
+
async function ensureStoredConfigSecrets(env = readProcessEnv()) {
|
|
472
|
+
const stored = loadStoredConfig(env);
|
|
473
|
+
if (!shouldProvisionWebhookSecret(stored) || stored.githubWebhookSecret) {
|
|
474
|
+
return stored;
|
|
475
|
+
}
|
|
476
|
+
return saveStoredConfig(stored, env);
|
|
477
|
+
}
|
|
478
|
+
function readConfig(env = readProcessEnv()) {
|
|
479
|
+
const stored = loadStoredConfig(env);
|
|
480
|
+
return {
|
|
481
|
+
port: stored.port ?? 4130,
|
|
482
|
+
dbPath: stored.dbPath ?? join(env.XDG_STATE_HOME ?? join(homedir(), ".local", "state"), "ops-harbor", "control-plane.db"),
|
|
483
|
+
githubApiUrl: stored.githubApiUrl ?? "https://api.github.com",
|
|
484
|
+
githubTunnelDisabled: stored.githubTunnelDisabled ?? false,
|
|
485
|
+
...stored.githubAppId ? { githubAppId: stored.githubAppId } : {},
|
|
486
|
+
...stored.githubPrivateKey ? { githubPrivateKey: stored.githubPrivateKey } : {},
|
|
487
|
+
...stored.githubWebhookSecret ? { githubWebhookSecret: stored.githubWebhookSecret } : {},
|
|
488
|
+
...stored.githubTunnelHost ? { githubTunnelHost: stored.githubTunnelHost } : {},
|
|
489
|
+
...stored.internalApiToken ? { internalApiToken: stored.internalApiToken } : {},
|
|
490
|
+
...stored.defaultAuthor ? { defaultAuthor: stored.defaultAuthor } : {}
|
|
491
|
+
};
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
// ../../packages/ops-harbor-core/src/alerts.ts
|
|
495
|
+
function inferCheckState(checks) {
|
|
496
|
+
if (checks.length === 0)
|
|
497
|
+
return "unknown";
|
|
498
|
+
let hasNeutral = false;
|
|
499
|
+
for (const check of checks) {
|
|
500
|
+
if (check.status !== "completed") {
|
|
501
|
+
return "pending";
|
|
502
|
+
}
|
|
503
|
+
const conclusion = (check.conclusion ?? "").toLowerCase();
|
|
504
|
+
if (["failure", "timed_out", "action_required", "cancelled"].includes(conclusion)) {
|
|
505
|
+
return "failure";
|
|
506
|
+
}
|
|
507
|
+
if (["neutral", "skipped"].includes(conclusion)) {
|
|
508
|
+
hasNeutral = true;
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
return hasNeutral ? "neutral" : "success";
|
|
512
|
+
}
|
|
513
|
+
function inferReviewState(reviews, reviewDecision) {
|
|
514
|
+
const normalizedDecision = (reviewDecision ?? "").toUpperCase();
|
|
515
|
+
if (normalizedDecision === "CHANGES_REQUESTED")
|
|
516
|
+
return "changes_requested";
|
|
517
|
+
if (normalizedDecision === "APPROVED")
|
|
518
|
+
return "approved";
|
|
519
|
+
const latest = reviews.toSorted((a, b) => b.submittedAt.localeCompare(a.submittedAt))[0];
|
|
520
|
+
return latest?.state ?? "none";
|
|
521
|
+
}
|
|
522
|
+
function inferMergeState(item) {
|
|
523
|
+
const state = item.mergeStateStatus.toUpperCase();
|
|
524
|
+
if (item.mergeable === "conflicting" || ["DIRTY", "CONFLICTING"].includes(state)) {
|
|
525
|
+
return "conflicting";
|
|
526
|
+
}
|
|
527
|
+
if (state === "BEHIND")
|
|
528
|
+
return "behind";
|
|
529
|
+
if (["BLOCKED", "DRAFT", "UNSTABLE"].includes(state))
|
|
530
|
+
return "blocked";
|
|
531
|
+
if (state === "CLEAN" || item.mergeable === "mergeable")
|
|
532
|
+
return "clean";
|
|
533
|
+
return "unknown";
|
|
534
|
+
}
|
|
535
|
+
function summarizeStatus(checks, reviews, item) {
|
|
536
|
+
const checkState = inferCheckState(checks);
|
|
537
|
+
const reviewState = inferReviewState(reviews, item.reviewDecision);
|
|
538
|
+
const mergeState = inferMergeState(item);
|
|
539
|
+
const overall = checkState === "unknown" && reviewState === "none" && mergeState === "unknown" ? "unknown" : checkState === "failure" || reviewState === "changes_requested" || mergeState !== "clean" && mergeState !== "unknown" ? "needs_attention" : checkState === "pending" ? "running" : "healthy";
|
|
540
|
+
return { overall, checkState, reviewState, mergeState };
|
|
541
|
+
}
|
|
542
|
+
function createAlert(type, severity, title, message, createdAt, source) {
|
|
543
|
+
return { type, severity, title, message, createdAt, source };
|
|
544
|
+
}
|
|
545
|
+
function deriveAlerts(item, recentEvents = []) {
|
|
546
|
+
const alerts = [];
|
|
547
|
+
const now = item.updatedAt;
|
|
548
|
+
if (item.statusSummary.checkState === "failure") {
|
|
549
|
+
alerts.push(createAlert("ci_failed", "critical", "CI failed", "One or more required checks failed on the latest head commit.", now, "state"));
|
|
550
|
+
}
|
|
551
|
+
if (item.statusSummary.mergeState === "conflicting") {
|
|
552
|
+
alerts.push(createAlert("conflicted", "critical", "Merge conflicts detected", `Branch ${item.headBranch} conflicts with ${item.baseBranch}.`, now, "state"));
|
|
553
|
+
}
|
|
554
|
+
if (item.statusSummary.mergeState === "behind") {
|
|
555
|
+
alerts.push(createAlert("base_behind", "warning", "Base branch moved ahead", `${item.headBranch} should be rebased or merged with ${item.baseBranch}.`, now, "state"));
|
|
556
|
+
}
|
|
557
|
+
if (item.statusSummary.reviewState === "changes_requested") {
|
|
558
|
+
alerts.push(createAlert("review_changes_requested", "critical", "Changes requested", "A reviewer requested follow-up changes on this work item.", now, "state"));
|
|
559
|
+
}
|
|
560
|
+
const reviewCommentEvent = recentEvents.filter((event) => event.type === "review_commented").toSorted((a, b) => b.createdAt.localeCompare(a.createdAt))[0];
|
|
561
|
+
if (reviewCommentEvent) {
|
|
562
|
+
alerts.push(createAlert("review_commented", "info", "Review comment received", reviewCommentEvent.message, reviewCommentEvent.createdAt, "event"));
|
|
563
|
+
}
|
|
564
|
+
const blockers = alerts.filter((alert) => alert.severity !== "info");
|
|
565
|
+
return { alerts, blockers };
|
|
566
|
+
}
|
|
567
|
+
function hasBlockingAlerts(item) {
|
|
568
|
+
return item.alerts.some((alert) => alert.severity !== "info");
|
|
569
|
+
}
|
|
570
|
+
// ../../packages/ops-harbor-core/src/db-helpers.ts
|
|
571
|
+
function mapWorkItemsToAlertSummaries(workItems) {
|
|
572
|
+
return workItems.map((item) => ({
|
|
573
|
+
workItemId: item.id,
|
|
574
|
+
repository: item.repository,
|
|
575
|
+
number: item.number,
|
|
576
|
+
title: item.title,
|
|
577
|
+
url: item.url,
|
|
578
|
+
alerts: item.alerts,
|
|
579
|
+
updatedAt: item.updatedAt
|
|
580
|
+
}));
|
|
581
|
+
}
|
|
582
|
+
function buildActivityWhereClause(options, columnMap = {
|
|
583
|
+
workItemId: "work_item_id",
|
|
584
|
+
repository: "repository"
|
|
585
|
+
}) {
|
|
586
|
+
const clauses = [];
|
|
587
|
+
const params = [];
|
|
588
|
+
if (options.workItemId) {
|
|
589
|
+
clauses.push(`${columnMap.workItemId} = ?`);
|
|
590
|
+
params.push(options.workItemId);
|
|
591
|
+
}
|
|
592
|
+
if (options.repository) {
|
|
593
|
+
clauses.push(`${columnMap.repository} = ?`);
|
|
594
|
+
params.push(options.repository);
|
|
595
|
+
}
|
|
596
|
+
const where = clauses.length > 0 ? `WHERE ${clauses.join(" AND ")}` : "";
|
|
597
|
+
return { where, params };
|
|
598
|
+
}
|
|
599
|
+
// ../../packages/ops-harbor-core/src/filters.ts
|
|
600
|
+
function filterWorkItems(items, filter) {
|
|
601
|
+
return items.filter((item) => {
|
|
602
|
+
if (filter.state && item.state !== filter.state)
|
|
603
|
+
return false;
|
|
604
|
+
if (filter.repository && item.repository !== filter.repository)
|
|
605
|
+
return false;
|
|
606
|
+
if (filter.hasBlocker !== undefined && hasBlockingAlerts(item) !== filter.hasBlocker) {
|
|
607
|
+
return false;
|
|
608
|
+
}
|
|
609
|
+
if (filter.updatedSince && item.updatedAt < filter.updatedSince)
|
|
610
|
+
return false;
|
|
611
|
+
return true;
|
|
612
|
+
});
|
|
613
|
+
}
|
|
614
|
+
function parseWorkItemFilterFromQuery(getParam) {
|
|
615
|
+
const filter = {};
|
|
616
|
+
const state = getParam("state");
|
|
617
|
+
if (state === "open" || state === "closed" || state === "merged") {
|
|
618
|
+
filter.state = state;
|
|
619
|
+
}
|
|
620
|
+
const repository = getParam("repository");
|
|
621
|
+
if (repository)
|
|
622
|
+
filter.repository = repository;
|
|
623
|
+
if (getParam("has_blocker") === "true") {
|
|
624
|
+
filter.hasBlocker = true;
|
|
625
|
+
} else if (getParam("has_blocker") === "false") {
|
|
626
|
+
filter.hasBlocker = false;
|
|
627
|
+
}
|
|
628
|
+
const updatedSince = getParam("updated_since");
|
|
629
|
+
if (updatedSince)
|
|
630
|
+
filter.updatedSince = updatedSince;
|
|
631
|
+
return filter;
|
|
632
|
+
}
|
|
633
|
+
// ../../packages/ops-harbor-core/src/prompt.ts
|
|
634
|
+
var TRIGGER_INSTRUCTIONS = {
|
|
635
|
+
ci_failed: "Investigate the failing checks, implement the minimum safe fix, and rerun local validation.",
|
|
636
|
+
conflicted: "Resolve merge conflicts with the base branch while preserving branch intent.",
|
|
637
|
+
base_behind: "Update the branch with the latest base branch changes and resolve follow-up issues.",
|
|
638
|
+
review_commented: "Review the latest reviewer feedback, apply the requested follow-up if it is actionable, and summarize the changes.",
|
|
639
|
+
review_changes_requested: "Address the requested review changes, validate locally, and prepare the branch for another review."
|
|
640
|
+
};
|
|
641
|
+
function buildAutomationPrompt(workItem, trigger) {
|
|
642
|
+
return [
|
|
643
|
+
`Provider: ${workItem.provider}`,
|
|
644
|
+
`Repository: ${workItem.repository}`,
|
|
645
|
+
`Work item: #${workItem.number} ${workItem.title}`,
|
|
646
|
+
`URL: ${workItem.url}`,
|
|
647
|
+
`Base branch: ${workItem.baseBranch}`,
|
|
648
|
+
`Head branch: ${workItem.headBranch}`,
|
|
649
|
+
`Trigger: ${trigger}`,
|
|
650
|
+
"",
|
|
651
|
+
TRIGGER_INSTRUCTIONS[trigger]
|
|
652
|
+
].join(`
|
|
653
|
+
`);
|
|
654
|
+
}
|
|
655
|
+
// ../../packages/ops-harbor-core/src/port-finder.ts
|
|
656
|
+
import { createServer } from "net";
|
|
657
|
+
var MAX_PORT = 65535;
|
|
658
|
+
async function findAvailablePort(startPort) {
|
|
659
|
+
for (let port = startPort;port <= MAX_PORT; port += 1) {
|
|
660
|
+
if (await isPortAvailable(port)) {
|
|
661
|
+
return port;
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
throw new Error(`No available port found between ${startPort} and ${MAX_PORT}`);
|
|
665
|
+
}
|
|
666
|
+
function isPortAvailable(port) {
|
|
667
|
+
return new Promise((resolve) => {
|
|
668
|
+
const server = createServer();
|
|
669
|
+
server.once("error", () => resolve(false));
|
|
670
|
+
server.once("listening", () => {
|
|
671
|
+
server.close(() => resolve(true));
|
|
672
|
+
});
|
|
673
|
+
server.listen(port, "127.0.0.1");
|
|
674
|
+
});
|
|
675
|
+
}
|
|
676
|
+
// ../../packages/ops-harbor-core/src/sqlite.ts
|
|
677
|
+
import { createRequire } from "module";
|
|
678
|
+
import { resolve } from "path";
|
|
679
|
+
var requireFromModule = createRequire(import.meta.url);
|
|
680
|
+
var requireFromWorkingDirectory = createRequire(resolve(process.cwd(), "package.json"));
|
|
681
|
+
function openSqliteDatabase(filename) {
|
|
682
|
+
const db = typeof Bun !== "undefined" ? new (requireFromModule("bun:sqlite")).Database(filename) : new (requireFromWorkingDirectory("better-sqlite3"))(filename);
|
|
683
|
+
db.exec("PRAGMA journal_mode = WAL");
|
|
684
|
+
return db;
|
|
685
|
+
}
|
|
686
|
+
function parseJson(value, fallback) {
|
|
687
|
+
if (!value)
|
|
688
|
+
return fallback;
|
|
689
|
+
try {
|
|
690
|
+
return JSON.parse(value);
|
|
691
|
+
} catch {
|
|
692
|
+
return fallback;
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
// ../ops-harbor-control-plane/src/lib/github.ts
|
|
696
|
+
import { createHmac, createSign } from "crypto";
|
|
697
|
+
var SEARCH_QUERY = `
|
|
698
|
+
query SearchPullRequests($query: String!, $first: Int!, $after: String) {
|
|
699
|
+
search(query: $query, type: ISSUE, first: $first, after: $after) {
|
|
700
|
+
pageInfo { hasNextPage endCursor }
|
|
701
|
+
nodes {
|
|
702
|
+
... on PullRequest {
|
|
703
|
+
id
|
|
704
|
+
number
|
|
705
|
+
title
|
|
706
|
+
url
|
|
707
|
+
state
|
|
708
|
+
isDraft
|
|
709
|
+
mergeable
|
|
710
|
+
mergeStateStatus
|
|
711
|
+
reviewDecision
|
|
712
|
+
updatedAt
|
|
713
|
+
repository { nameWithOwner url }
|
|
714
|
+
author { login }
|
|
715
|
+
baseRefName
|
|
716
|
+
headRefName
|
|
717
|
+
headRefOid
|
|
718
|
+
latestReviews(first: 20) {
|
|
719
|
+
nodes {
|
|
720
|
+
id
|
|
721
|
+
state
|
|
722
|
+
body
|
|
723
|
+
submittedAt
|
|
724
|
+
url
|
|
725
|
+
author { login }
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
commits(last: 1) {
|
|
729
|
+
nodes {
|
|
730
|
+
commit {
|
|
731
|
+
statusCheckRollup {
|
|
732
|
+
contexts(first: 50) {
|
|
733
|
+
nodes {
|
|
734
|
+
__typename
|
|
735
|
+
... on CheckRun {
|
|
736
|
+
name
|
|
737
|
+
status
|
|
738
|
+
conclusion
|
|
739
|
+
detailsUrl
|
|
740
|
+
startedAt
|
|
741
|
+
completedAt
|
|
742
|
+
}
|
|
743
|
+
... on StatusContext {
|
|
744
|
+
context
|
|
745
|
+
state
|
|
746
|
+
targetUrl
|
|
747
|
+
createdAt
|
|
748
|
+
description
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
`;
|
|
761
|
+
var PULL_REQUEST_QUERY = `
|
|
762
|
+
query PullRequestDetails($owner: String!, $repo: String!, $number: Int!) {
|
|
763
|
+
repository(owner: $owner, name: $repo) {
|
|
764
|
+
pullRequest(number: $number) {
|
|
765
|
+
id
|
|
766
|
+
number
|
|
767
|
+
title
|
|
768
|
+
url
|
|
769
|
+
state
|
|
770
|
+
isDraft
|
|
771
|
+
mergeable
|
|
772
|
+
mergeStateStatus
|
|
773
|
+
reviewDecision
|
|
774
|
+
updatedAt
|
|
775
|
+
repository { nameWithOwner url }
|
|
776
|
+
author { login }
|
|
777
|
+
baseRefName
|
|
778
|
+
headRefName
|
|
779
|
+
headRefOid
|
|
780
|
+
latestReviews(first: 20) {
|
|
781
|
+
nodes {
|
|
782
|
+
id
|
|
783
|
+
state
|
|
784
|
+
body
|
|
785
|
+
submittedAt
|
|
786
|
+
url
|
|
787
|
+
author { login }
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
commits(last: 1) {
|
|
791
|
+
nodes {
|
|
792
|
+
commit {
|
|
793
|
+
statusCheckRollup {
|
|
794
|
+
contexts(first: 50) {
|
|
795
|
+
nodes {
|
|
796
|
+
__typename
|
|
797
|
+
... on CheckRun {
|
|
798
|
+
name
|
|
799
|
+
status
|
|
800
|
+
conclusion
|
|
801
|
+
detailsUrl
|
|
802
|
+
startedAt
|
|
803
|
+
completedAt
|
|
804
|
+
}
|
|
805
|
+
... on StatusContext {
|
|
806
|
+
context
|
|
807
|
+
state
|
|
808
|
+
targetUrl
|
|
809
|
+
createdAt
|
|
810
|
+
description
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
`;
|
|
822
|
+
function toBase64Url(value) {
|
|
823
|
+
return Buffer.from(value).toString("base64").replaceAll("=", "").replaceAll("+", "-").replaceAll("/", "_");
|
|
824
|
+
}
|
|
825
|
+
function createAppJwt(appId, privateKeyPem) {
|
|
826
|
+
const now = Math.floor(Date.now() / 1000);
|
|
827
|
+
const header = { alg: "RS256", typ: "JWT" };
|
|
828
|
+
const payload = {
|
|
829
|
+
iat: now - 60,
|
|
830
|
+
exp: now + 9 * 60,
|
|
831
|
+
iss: appId
|
|
832
|
+
};
|
|
833
|
+
const encoded = `${toBase64Url(JSON.stringify(header))}.${toBase64Url(JSON.stringify(payload))}`;
|
|
834
|
+
const signer = createSign("RSA-SHA256");
|
|
835
|
+
signer.update(encoded);
|
|
836
|
+
const signature = signer.sign(privateKeyPem);
|
|
837
|
+
return `${encoded}.${toBase64Url(signature)}`;
|
|
838
|
+
}
|
|
839
|
+
async function githubRequest(config, path, init, scope) {
|
|
840
|
+
const response = await fetch(`${config.githubApiUrl}${path}`, {
|
|
841
|
+
...init,
|
|
842
|
+
headers: {
|
|
843
|
+
Accept: "application/vnd.github+json",
|
|
844
|
+
"User-Agent": "ops-harbor-control-plane",
|
|
845
|
+
...init.headers
|
|
846
|
+
}
|
|
847
|
+
});
|
|
848
|
+
if (!response.ok) {
|
|
849
|
+
throw new Error(`GitHub request failed (${scope}): ${response.status} ${response.statusText}`);
|
|
850
|
+
}
|
|
851
|
+
const data = await response.json();
|
|
852
|
+
return {
|
|
853
|
+
data,
|
|
854
|
+
rateLimit: {
|
|
855
|
+
remaining: Number.parseInt(response.headers.get("x-ratelimit-remaining") ?? "", 10),
|
|
856
|
+
resetAt: response.headers.get("x-ratelimit-reset") ? new Date(Number.parseInt(response.headers.get("x-ratelimit-reset") ?? "0", 10) * 1000).toISOString() : null
|
|
857
|
+
}
|
|
858
|
+
};
|
|
859
|
+
}
|
|
860
|
+
async function getAppAuthHeader(config) {
|
|
861
|
+
if (!config.githubAppId || !config.githubPrivateKey) {
|
|
862
|
+
throw new Error("GitHub App is not configured.");
|
|
863
|
+
}
|
|
864
|
+
return `Bearer ${createAppJwt(config.githubAppId, config.githubPrivateKey)}`;
|
|
865
|
+
}
|
|
866
|
+
function mergeRateLimitSnapshots(current, next) {
|
|
867
|
+
const remaining = current.remaining !== undefined && next.remaining !== undefined ? Math.min(current.remaining, next.remaining) : next.remaining ?? current.remaining;
|
|
868
|
+
const resetAt = next.resetAt ?? current.resetAt;
|
|
869
|
+
return {
|
|
870
|
+
...remaining !== undefined ? { remaining } : {},
|
|
871
|
+
...resetAt !== undefined ? { resetAt } : {}
|
|
872
|
+
};
|
|
873
|
+
}
|
|
874
|
+
async function listInstallations(config) {
|
|
875
|
+
const authHeader = await getAppAuthHeader(config);
|
|
876
|
+
const installations = [];
|
|
877
|
+
let rateLimit = {};
|
|
878
|
+
for (let page = 1;; page += 1) {
|
|
879
|
+
const { data, rateLimit: pageRateLimit } = await githubRequest(config, `/app/installations?per_page=100&page=${page}`, {
|
|
880
|
+
method: "GET",
|
|
881
|
+
headers: {
|
|
882
|
+
Authorization: authHeader
|
|
883
|
+
}
|
|
884
|
+
}, "app.installations");
|
|
885
|
+
const pageInstallations = Array.isArray(data) ? data : data.installations ?? [];
|
|
886
|
+
installations.push(...pageInstallations);
|
|
887
|
+
rateLimit = mergeRateLimitSnapshots(rateLimit, pageRateLimit);
|
|
888
|
+
if (pageInstallations.length < 100) {
|
|
889
|
+
break;
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
return { installations, rateLimit };
|
|
893
|
+
}
|
|
894
|
+
async function getGitHubAppWebhookConfig(config) {
|
|
895
|
+
const { data, rateLimit } = await githubRequest(config, "/app/hook/config", {
|
|
896
|
+
method: "GET",
|
|
897
|
+
headers: {
|
|
898
|
+
Authorization: await getAppAuthHeader(config)
|
|
899
|
+
}
|
|
900
|
+
}, "app.hook.config.get");
|
|
901
|
+
return {
|
|
902
|
+
config: {
|
|
903
|
+
url: data.url,
|
|
904
|
+
contentType: data.content_type ?? "json",
|
|
905
|
+
insecureSsl: data.insecure_ssl ?? "0"
|
|
906
|
+
},
|
|
907
|
+
rateLimit
|
|
908
|
+
};
|
|
909
|
+
}
|
|
910
|
+
async function updateGitHubAppWebhookConfig(config, update) {
|
|
911
|
+
const body = {
|
|
912
|
+
url: update.url
|
|
913
|
+
};
|
|
914
|
+
if (update.contentType) {
|
|
915
|
+
body.content_type = update.contentType;
|
|
916
|
+
}
|
|
917
|
+
if (update.insecureSsl) {
|
|
918
|
+
body.insecure_ssl = update.insecureSsl;
|
|
919
|
+
}
|
|
920
|
+
if (config.githubWebhookSecret) {
|
|
921
|
+
body.secret = config.githubWebhookSecret;
|
|
922
|
+
}
|
|
923
|
+
const { data, rateLimit } = await githubRequest(config, "/app/hook/config", {
|
|
924
|
+
method: "PATCH",
|
|
925
|
+
headers: {
|
|
926
|
+
Authorization: await getAppAuthHeader(config),
|
|
927
|
+
"Content-Type": "application/json"
|
|
928
|
+
},
|
|
929
|
+
body: JSON.stringify(body)
|
|
930
|
+
}, "app.hook.config.update");
|
|
931
|
+
return {
|
|
932
|
+
config: {
|
|
933
|
+
url: data.url,
|
|
934
|
+
contentType: data.content_type ?? update.contentType ?? "json",
|
|
935
|
+
insecureSsl: data.insecure_ssl ?? update.insecureSsl ?? "0"
|
|
936
|
+
},
|
|
937
|
+
rateLimit
|
|
938
|
+
};
|
|
939
|
+
}
|
|
940
|
+
async function ensureGitHubAppWebhookUrl(config, webhookUrl) {
|
|
941
|
+
const current = await getGitHubAppWebhookConfig(config);
|
|
942
|
+
if (current.config.url === webhookUrl && current.config.contentType === "json" && !config.githubWebhookSecret) {
|
|
943
|
+
return {
|
|
944
|
+
config: current.config,
|
|
945
|
+
updated: false,
|
|
946
|
+
rateLimit: current.rateLimit
|
|
947
|
+
};
|
|
948
|
+
}
|
|
949
|
+
const next = await updateGitHubAppWebhookConfig(config, {
|
|
950
|
+
url: webhookUrl,
|
|
951
|
+
contentType: "json",
|
|
952
|
+
insecureSsl: current.config.insecureSsl
|
|
953
|
+
});
|
|
954
|
+
return {
|
|
955
|
+
config: next.config,
|
|
956
|
+
updated: true,
|
|
957
|
+
rateLimit: mergeRateLimitSnapshots(current.rateLimit, next.rateLimit)
|
|
958
|
+
};
|
|
959
|
+
}
|
|
960
|
+
async function createInstallationToken(config, installationId) {
|
|
961
|
+
const { data, rateLimit } = await githubRequest(config, `/app/installations/${installationId}/access_tokens`, {
|
|
962
|
+
method: "POST",
|
|
963
|
+
headers: {
|
|
964
|
+
Authorization: await getAppAuthHeader(config)
|
|
965
|
+
}
|
|
966
|
+
}, `installation.${installationId}.token`);
|
|
967
|
+
return { token: data.token, rateLimit };
|
|
968
|
+
}
|
|
969
|
+
async function graphql(config, installationId, query, variables) {
|
|
970
|
+
const { token } = await createInstallationToken(config, installationId);
|
|
971
|
+
const response = await fetch(`${config.githubApiUrl}/graphql`, {
|
|
972
|
+
method: "POST",
|
|
973
|
+
headers: {
|
|
974
|
+
Accept: "application/vnd.github+json",
|
|
975
|
+
Authorization: `Bearer ${token}`,
|
|
976
|
+
"Content-Type": "application/json",
|
|
977
|
+
"User-Agent": "ops-harbor-control-plane"
|
|
978
|
+
},
|
|
979
|
+
body: JSON.stringify({ query, variables })
|
|
980
|
+
});
|
|
981
|
+
if (!response.ok) {
|
|
982
|
+
throw new Error(`GitHub GraphQL request failed: ${response.status} ${response.statusText}`);
|
|
983
|
+
}
|
|
984
|
+
const json = await response.json();
|
|
985
|
+
if (json.errors?.length) {
|
|
986
|
+
throw new Error(json.errors.map((error) => error.message).join("; "));
|
|
987
|
+
}
|
|
988
|
+
if (!json.data) {
|
|
989
|
+
throw new Error("GitHub GraphQL response did not contain data.");
|
|
990
|
+
}
|
|
991
|
+
return {
|
|
992
|
+
data: json.data,
|
|
993
|
+
rateLimit: {
|
|
994
|
+
remaining: Number.parseInt(response.headers.get("x-ratelimit-remaining") ?? "", 10),
|
|
995
|
+
resetAt: response.headers.get("x-ratelimit-reset") ? new Date(Number.parseInt(response.headers.get("x-ratelimit-reset") ?? "0", 10) * 1000).toISOString() : null
|
|
996
|
+
}
|
|
997
|
+
};
|
|
998
|
+
}
|
|
999
|
+
function mapChecks(node) {
|
|
1000
|
+
const contexts = node.commits.nodes[0]?.commit.statusCheckRollup?.contexts.nodes ?? [];
|
|
1001
|
+
return contexts.map((context) => {
|
|
1002
|
+
if (context.__typename === "CheckRun") {
|
|
1003
|
+
return {
|
|
1004
|
+
name: context.name,
|
|
1005
|
+
status: context.status,
|
|
1006
|
+
conclusion: context.conclusion,
|
|
1007
|
+
url: context.detailsUrl ?? null,
|
|
1008
|
+
startedAt: context.startedAt ?? null,
|
|
1009
|
+
completedAt: context.completedAt ?? null
|
|
1010
|
+
};
|
|
1011
|
+
}
|
|
1012
|
+
return {
|
|
1013
|
+
name: context.context,
|
|
1014
|
+
status: context.state === "PENDING" ? "pending" : "completed",
|
|
1015
|
+
conclusion: context.state.toLowerCase(),
|
|
1016
|
+
url: context.targetUrl ?? null,
|
|
1017
|
+
startedAt: context.createdAt ?? null,
|
|
1018
|
+
completedAt: context.createdAt ?? null
|
|
1019
|
+
};
|
|
1020
|
+
});
|
|
1021
|
+
}
|
|
1022
|
+
function mapReviews(node) {
|
|
1023
|
+
return node.latestReviews.nodes.map((review) => ({
|
|
1024
|
+
id: review.id,
|
|
1025
|
+
state: review.state.toLowerCase(),
|
|
1026
|
+
author: review.author?.login ?? "unknown",
|
|
1027
|
+
submittedAt: review.submittedAt,
|
|
1028
|
+
...review.body ? { bodySnippet: review.body.slice(0, 240) } : {},
|
|
1029
|
+
...review.url ? { url: review.url } : {}
|
|
1030
|
+
}));
|
|
1031
|
+
}
|
|
1032
|
+
function mapPullRequest(node, installationId) {
|
|
1033
|
+
const checks = mapChecks(node);
|
|
1034
|
+
const reviews = mapReviews(node);
|
|
1035
|
+
const statusSummary = summarizeStatus(checks, reviews, {
|
|
1036
|
+
mergeable: node.mergeable === "CONFLICTING" ? "conflicting" : node.mergeable === "MERGEABLE" ? "mergeable" : "unknown",
|
|
1037
|
+
mergeStateStatus: node.mergeStateStatus,
|
|
1038
|
+
reviewDecision: node.reviewDecision
|
|
1039
|
+
});
|
|
1040
|
+
return {
|
|
1041
|
+
id: node.id,
|
|
1042
|
+
provider: "github",
|
|
1043
|
+
kind: "pull_request",
|
|
1044
|
+
repository: node.repository.nameWithOwner,
|
|
1045
|
+
number: node.number,
|
|
1046
|
+
title: node.title,
|
|
1047
|
+
url: node.url,
|
|
1048
|
+
state: node.state.toLowerCase(),
|
|
1049
|
+
author: node.author?.login ?? "unknown",
|
|
1050
|
+
isDraft: node.isDraft,
|
|
1051
|
+
headBranch: node.headRefName,
|
|
1052
|
+
headSha: node.headRefOid,
|
|
1053
|
+
baseBranch: node.baseRefName,
|
|
1054
|
+
mergeable: node.mergeable === "CONFLICTING" ? "conflicting" : node.mergeable === "MERGEABLE" ? "mergeable" : "unknown",
|
|
1055
|
+
mergeStateStatus: node.mergeStateStatus,
|
|
1056
|
+
reviewDecision: node.reviewDecision,
|
|
1057
|
+
statusSummary,
|
|
1058
|
+
checks,
|
|
1059
|
+
reviews,
|
|
1060
|
+
alerts: [],
|
|
1061
|
+
lastActivityAt: node.updatedAt,
|
|
1062
|
+
updatedAt: node.updatedAt,
|
|
1063
|
+
providerPayload: {
|
|
1064
|
+
installationId,
|
|
1065
|
+
repositoryUrl: node.repository.url
|
|
1066
|
+
}
|
|
1067
|
+
};
|
|
1068
|
+
}
|
|
1069
|
+
async function fetchOpenPullRequestsForAuthor(config, installationId, author, limit = 100) {
|
|
1070
|
+
const items = [];
|
|
1071
|
+
let cursor = null;
|
|
1072
|
+
let lastRateLimit = {};
|
|
1073
|
+
while (items.length < limit) {
|
|
1074
|
+
const response = await graphql(config, installationId, SEARCH_QUERY, {
|
|
1075
|
+
query: `is:pr is:open author:${author} archived:false`,
|
|
1076
|
+
first: Math.min(50, limit - items.length),
|
|
1077
|
+
after: cursor
|
|
1078
|
+
});
|
|
1079
|
+
const data = response.data;
|
|
1080
|
+
lastRateLimit = response.rateLimit;
|
|
1081
|
+
items.push(...data.search.nodes.map((node) => mapPullRequest(node, installationId)));
|
|
1082
|
+
if (!data.search.pageInfo.hasNextPage)
|
|
1083
|
+
break;
|
|
1084
|
+
cursor = data.search.pageInfo.endCursor;
|
|
1085
|
+
}
|
|
1086
|
+
return { items, rateLimit: lastRateLimit };
|
|
1087
|
+
}
|
|
1088
|
+
async function hydratePullRequest(config, installationId, repository, number2) {
|
|
1089
|
+
const [owner, repo] = repository.split("/");
|
|
1090
|
+
if (!owner || !repo) {
|
|
1091
|
+
throw new Error(`Invalid repository name: ${repository}`);
|
|
1092
|
+
}
|
|
1093
|
+
const { data, rateLimit } = await graphql(config, installationId, PULL_REQUEST_QUERY, { owner, repo, number: number2 });
|
|
1094
|
+
return {
|
|
1095
|
+
item: data.repository.pullRequest ? mapPullRequest(data.repository.pullRequest, installationId) : null,
|
|
1096
|
+
rateLimit
|
|
1097
|
+
};
|
|
1098
|
+
}
|
|
1099
|
+
function verifyWebhookSignature(secret, rawBody, signatureHeader) {
|
|
1100
|
+
if (!signatureHeader?.startsWith("sha256="))
|
|
1101
|
+
return false;
|
|
1102
|
+
const expected = `sha256=${createHmac("sha256", secret).update(rawBody).digest("hex")}`;
|
|
1103
|
+
return expected === signatureHeader;
|
|
1104
|
+
}
|
|
1105
|
+
|
|
1106
|
+
// ../ops-harbor-control-plane/src/lib/tunnel.ts
|
|
1107
|
+
async function openTunnel(options) {
|
|
1108
|
+
const module = await import("localtunnel");
|
|
1109
|
+
const createTunnel = module.default;
|
|
1110
|
+
if (!createTunnel) {
|
|
1111
|
+
throw new Error("localtunnel did not expose a default factory.");
|
|
1112
|
+
}
|
|
1113
|
+
const tunnel = await createTunnel({
|
|
1114
|
+
port: options.port,
|
|
1115
|
+
...options.localHost ? { local_host: options.localHost } : {},
|
|
1116
|
+
...options.host ? { host: options.host } : {}
|
|
1117
|
+
});
|
|
1118
|
+
return {
|
|
1119
|
+
url: tunnel.url.replace(/\/$/, ""),
|
|
1120
|
+
async close() {
|
|
1121
|
+
await tunnel.close();
|
|
1122
|
+
}
|
|
1123
|
+
};
|
|
1124
|
+
}
|
|
1125
|
+
|
|
1126
|
+
// ../../node_modules/.bun/hono@4.12.10/node_modules/hono/dist/compose.js
|
|
1127
|
+
var compose = (middleware, onError, onNotFound) => {
|
|
1128
|
+
return (context, next) => {
|
|
1129
|
+
let index = -1;
|
|
1130
|
+
return dispatch(0);
|
|
1131
|
+
async function dispatch(i) {
|
|
1132
|
+
if (i <= index) {
|
|
1133
|
+
throw new Error("next() called multiple times");
|
|
1134
|
+
}
|
|
1135
|
+
index = i;
|
|
1136
|
+
let res;
|
|
1137
|
+
let isError = false;
|
|
1138
|
+
let handler;
|
|
1139
|
+
if (middleware[i]) {
|
|
1140
|
+
handler = middleware[i][0][0];
|
|
1141
|
+
context.req.routeIndex = i;
|
|
1142
|
+
} else {
|
|
1143
|
+
handler = i === middleware.length && next || undefined;
|
|
1144
|
+
}
|
|
1145
|
+
if (handler) {
|
|
1146
|
+
try {
|
|
1147
|
+
res = await handler(context, () => dispatch(i + 1));
|
|
1148
|
+
} catch (err) {
|
|
1149
|
+
if (err instanceof Error && onError) {
|
|
1150
|
+
context.error = err;
|
|
1151
|
+
res = await onError(err, context);
|
|
1152
|
+
isError = true;
|
|
1153
|
+
} else {
|
|
1154
|
+
throw err;
|
|
1155
|
+
}
|
|
1156
|
+
}
|
|
1157
|
+
} else {
|
|
1158
|
+
if (context.finalized === false && onNotFound) {
|
|
1159
|
+
res = await onNotFound(context);
|
|
1160
|
+
}
|
|
1161
|
+
}
|
|
1162
|
+
if (res && (context.finalized === false || isError)) {
|
|
1163
|
+
context.res = res;
|
|
1164
|
+
}
|
|
1165
|
+
return context;
|
|
1166
|
+
}
|
|
1167
|
+
};
|
|
1168
|
+
};
|
|
1169
|
+
|
|
1170
|
+
// ../../node_modules/.bun/hono@4.12.10/node_modules/hono/dist/request/constants.js
|
|
1171
|
+
var GET_MATCH_RESULT = /* @__PURE__ */ Symbol();
|
|
1172
|
+
|
|
1173
|
+
// ../../node_modules/.bun/hono@4.12.10/node_modules/hono/dist/utils/body.js
|
|
1174
|
+
var parseBody = async (request, options = /* @__PURE__ */ Object.create(null)) => {
|
|
1175
|
+
const { all = false, dot = false } = options;
|
|
1176
|
+
const headers = request instanceof HonoRequest ? request.raw.headers : request.headers;
|
|
1177
|
+
const contentType = headers.get("Content-Type");
|
|
1178
|
+
if (contentType?.startsWith("multipart/form-data") || contentType?.startsWith("application/x-www-form-urlencoded")) {
|
|
1179
|
+
return parseFormData(request, { all, dot });
|
|
1180
|
+
}
|
|
1181
|
+
return {};
|
|
1182
|
+
};
|
|
1183
|
+
async function parseFormData(request, options) {
|
|
1184
|
+
const formData = await request.formData();
|
|
1185
|
+
if (formData) {
|
|
1186
|
+
return convertFormDataToBodyData(formData, options);
|
|
1187
|
+
}
|
|
1188
|
+
return {};
|
|
1189
|
+
}
|
|
1190
|
+
function convertFormDataToBodyData(formData, options) {
|
|
1191
|
+
const form = /* @__PURE__ */ Object.create(null);
|
|
1192
|
+
formData.forEach((value, key) => {
|
|
1193
|
+
const shouldParseAllValues = options.all || key.endsWith("[]");
|
|
1194
|
+
if (!shouldParseAllValues) {
|
|
1195
|
+
form[key] = value;
|
|
1196
|
+
} else {
|
|
1197
|
+
handleParsingAllValues(form, key, value);
|
|
1198
|
+
}
|
|
1199
|
+
});
|
|
1200
|
+
if (options.dot) {
|
|
1201
|
+
Object.entries(form).forEach(([key, value]) => {
|
|
1202
|
+
const shouldParseDotValues = key.includes(".");
|
|
1203
|
+
if (shouldParseDotValues) {
|
|
1204
|
+
handleParsingNestedValues(form, key, value);
|
|
1205
|
+
delete form[key];
|
|
1206
|
+
}
|
|
1207
|
+
});
|
|
1208
|
+
}
|
|
1209
|
+
return form;
|
|
1210
|
+
}
|
|
1211
|
+
var handleParsingAllValues = (form, key, value) => {
|
|
1212
|
+
if (form[key] !== undefined) {
|
|
1213
|
+
if (Array.isArray(form[key])) {
|
|
1214
|
+
form[key].push(value);
|
|
1215
|
+
} else {
|
|
1216
|
+
form[key] = [form[key], value];
|
|
1217
|
+
}
|
|
1218
|
+
} else {
|
|
1219
|
+
if (!key.endsWith("[]")) {
|
|
1220
|
+
form[key] = value;
|
|
1221
|
+
} else {
|
|
1222
|
+
form[key] = [value];
|
|
1223
|
+
}
|
|
1224
|
+
}
|
|
1225
|
+
};
|
|
1226
|
+
var handleParsingNestedValues = (form, key, value) => {
|
|
1227
|
+
if (/(?:^|\.)__proto__\./.test(key)) {
|
|
1228
|
+
return;
|
|
1229
|
+
}
|
|
1230
|
+
let nestedForm = form;
|
|
1231
|
+
const keys = key.split(".");
|
|
1232
|
+
keys.forEach((key2, index) => {
|
|
1233
|
+
if (index === keys.length - 1) {
|
|
1234
|
+
nestedForm[key2] = value;
|
|
1235
|
+
} else {
|
|
1236
|
+
if (!nestedForm[key2] || typeof nestedForm[key2] !== "object" || Array.isArray(nestedForm[key2]) || nestedForm[key2] instanceof File) {
|
|
1237
|
+
nestedForm[key2] = /* @__PURE__ */ Object.create(null);
|
|
1238
|
+
}
|
|
1239
|
+
nestedForm = nestedForm[key2];
|
|
1240
|
+
}
|
|
1241
|
+
});
|
|
1242
|
+
};
|
|
1243
|
+
|
|
1244
|
+
// ../../node_modules/.bun/hono@4.12.10/node_modules/hono/dist/utils/url.js
|
|
1245
|
+
var splitPath = (path) => {
|
|
1246
|
+
const paths = path.split("/");
|
|
1247
|
+
if (paths[0] === "") {
|
|
1248
|
+
paths.shift();
|
|
1249
|
+
}
|
|
1250
|
+
return paths;
|
|
1251
|
+
};
|
|
1252
|
+
var splitRoutingPath = (routePath) => {
|
|
1253
|
+
const { groups, path } = extractGroupsFromPath(routePath);
|
|
1254
|
+
const paths = splitPath(path);
|
|
1255
|
+
return replaceGroupMarks(paths, groups);
|
|
1256
|
+
};
|
|
1257
|
+
var extractGroupsFromPath = (path) => {
|
|
1258
|
+
const groups = [];
|
|
1259
|
+
path = path.replace(/\{[^}]+\}/g, (match, index) => {
|
|
1260
|
+
const mark = `@${index}`;
|
|
1261
|
+
groups.push([mark, match]);
|
|
1262
|
+
return mark;
|
|
1263
|
+
});
|
|
1264
|
+
return { groups, path };
|
|
1265
|
+
};
|
|
1266
|
+
var replaceGroupMarks = (paths, groups) => {
|
|
1267
|
+
for (let i = groups.length - 1;i >= 0; i--) {
|
|
1268
|
+
const [mark] = groups[i];
|
|
1269
|
+
for (let j = paths.length - 1;j >= 0; j--) {
|
|
1270
|
+
if (paths[j].includes(mark)) {
|
|
1271
|
+
paths[j] = paths[j].replace(mark, groups[i][1]);
|
|
1272
|
+
break;
|
|
1273
|
+
}
|
|
1274
|
+
}
|
|
1275
|
+
}
|
|
1276
|
+
return paths;
|
|
1277
|
+
};
|
|
1278
|
+
var patternCache = {};
|
|
1279
|
+
var getPattern = (label, next) => {
|
|
1280
|
+
if (label === "*") {
|
|
1281
|
+
return "*";
|
|
1282
|
+
}
|
|
1283
|
+
const match = label.match(/^\:([^\{\}]+)(?:\{(.+)\})?$/);
|
|
1284
|
+
if (match) {
|
|
1285
|
+
const cacheKey = `${label}#${next}`;
|
|
1286
|
+
if (!patternCache[cacheKey]) {
|
|
1287
|
+
if (match[2]) {
|
|
1288
|
+
patternCache[cacheKey] = next && next[0] !== ":" && next[0] !== "*" ? [cacheKey, match[1], new RegExp(`^${match[2]}(?=/${next})`)] : [label, match[1], new RegExp(`^${match[2]}$`)];
|
|
1289
|
+
} else {
|
|
1290
|
+
patternCache[cacheKey] = [label, match[1], true];
|
|
1291
|
+
}
|
|
1292
|
+
}
|
|
1293
|
+
return patternCache[cacheKey];
|
|
1294
|
+
}
|
|
1295
|
+
return null;
|
|
1296
|
+
};
|
|
1297
|
+
var tryDecode = (str, decoder) => {
|
|
1298
|
+
try {
|
|
1299
|
+
return decoder(str);
|
|
1300
|
+
} catch {
|
|
1301
|
+
return str.replace(/(?:%[0-9A-Fa-f]{2})+/g, (match) => {
|
|
1302
|
+
try {
|
|
1303
|
+
return decoder(match);
|
|
1304
|
+
} catch {
|
|
1305
|
+
return match;
|
|
1306
|
+
}
|
|
1307
|
+
});
|
|
1308
|
+
}
|
|
1309
|
+
};
|
|
1310
|
+
var tryDecodeURI = (str) => tryDecode(str, decodeURI);
|
|
1311
|
+
var getPath = (request) => {
|
|
1312
|
+
const url = request.url;
|
|
1313
|
+
const start = url.indexOf("/", url.indexOf(":") + 4);
|
|
1314
|
+
let i = start;
|
|
1315
|
+
for (;i < url.length; i++) {
|
|
1316
|
+
const charCode = url.charCodeAt(i);
|
|
1317
|
+
if (charCode === 37) {
|
|
1318
|
+
const queryIndex = url.indexOf("?", i);
|
|
1319
|
+
const hashIndex = url.indexOf("#", i);
|
|
1320
|
+
const end = queryIndex === -1 ? hashIndex === -1 ? undefined : hashIndex : hashIndex === -1 ? queryIndex : Math.min(queryIndex, hashIndex);
|
|
1321
|
+
const path = url.slice(start, end);
|
|
1322
|
+
return tryDecodeURI(path.includes("%25") ? path.replace(/%25/g, "%2525") : path);
|
|
1323
|
+
} else if (charCode === 63 || charCode === 35) {
|
|
1324
|
+
break;
|
|
1325
|
+
}
|
|
1326
|
+
}
|
|
1327
|
+
return url.slice(start, i);
|
|
1328
|
+
};
|
|
1329
|
+
var getPathNoStrict = (request) => {
|
|
1330
|
+
const result = getPath(request);
|
|
1331
|
+
return result.length > 1 && result.at(-1) === "/" ? result.slice(0, -1) : result;
|
|
1332
|
+
};
|
|
1333
|
+
var mergePath = (base, sub, ...rest) => {
|
|
1334
|
+
if (rest.length) {
|
|
1335
|
+
sub = mergePath(sub, ...rest);
|
|
1336
|
+
}
|
|
1337
|
+
return `${base?.[0] === "/" ? "" : "/"}${base}${sub === "/" ? "" : `${base?.at(-1) === "/" ? "" : "/"}${sub?.[0] === "/" ? sub.slice(1) : sub}`}`;
|
|
1338
|
+
};
|
|
1339
|
+
var checkOptionalParameter = (path) => {
|
|
1340
|
+
if (path.charCodeAt(path.length - 1) !== 63 || !path.includes(":")) {
|
|
1341
|
+
return null;
|
|
1342
|
+
}
|
|
1343
|
+
const segments = path.split("/");
|
|
1344
|
+
const results = [];
|
|
1345
|
+
let basePath = "";
|
|
1346
|
+
segments.forEach((segment) => {
|
|
1347
|
+
if (segment !== "" && !/\:/.test(segment)) {
|
|
1348
|
+
basePath += "/" + segment;
|
|
1349
|
+
} else if (/\:/.test(segment)) {
|
|
1350
|
+
if (/\?/.test(segment)) {
|
|
1351
|
+
if (results.length === 0 && basePath === "") {
|
|
1352
|
+
results.push("/");
|
|
1353
|
+
} else {
|
|
1354
|
+
results.push(basePath);
|
|
1355
|
+
}
|
|
1356
|
+
const optionalSegment = segment.replace("?", "");
|
|
1357
|
+
basePath += "/" + optionalSegment;
|
|
1358
|
+
results.push(basePath);
|
|
1359
|
+
} else {
|
|
1360
|
+
basePath += "/" + segment;
|
|
1361
|
+
}
|
|
1362
|
+
}
|
|
1363
|
+
});
|
|
1364
|
+
return results.filter((v, i, a) => a.indexOf(v) === i);
|
|
1365
|
+
};
|
|
1366
|
+
var _decodeURI = (value) => {
|
|
1367
|
+
if (!/[%+]/.test(value)) {
|
|
1368
|
+
return value;
|
|
1369
|
+
}
|
|
1370
|
+
if (value.indexOf("+") !== -1) {
|
|
1371
|
+
value = value.replace(/\+/g, " ");
|
|
1372
|
+
}
|
|
1373
|
+
return value.indexOf("%") !== -1 ? tryDecode(value, decodeURIComponent_) : value;
|
|
1374
|
+
};
|
|
1375
|
+
var _getQueryParam = (url, key, multiple) => {
|
|
1376
|
+
let encoded;
|
|
1377
|
+
if (!multiple && key && !/[%+]/.test(key)) {
|
|
1378
|
+
let keyIndex2 = url.indexOf("?", 8);
|
|
1379
|
+
if (keyIndex2 === -1) {
|
|
1380
|
+
return;
|
|
1381
|
+
}
|
|
1382
|
+
if (!url.startsWith(key, keyIndex2 + 1)) {
|
|
1383
|
+
keyIndex2 = url.indexOf(`&${key}`, keyIndex2 + 1);
|
|
1384
|
+
}
|
|
1385
|
+
while (keyIndex2 !== -1) {
|
|
1386
|
+
const trailingKeyCode = url.charCodeAt(keyIndex2 + key.length + 1);
|
|
1387
|
+
if (trailingKeyCode === 61) {
|
|
1388
|
+
const valueIndex = keyIndex2 + key.length + 2;
|
|
1389
|
+
const endIndex = url.indexOf("&", valueIndex);
|
|
1390
|
+
return _decodeURI(url.slice(valueIndex, endIndex === -1 ? undefined : endIndex));
|
|
1391
|
+
} else if (trailingKeyCode == 38 || isNaN(trailingKeyCode)) {
|
|
1392
|
+
return "";
|
|
1393
|
+
}
|
|
1394
|
+
keyIndex2 = url.indexOf(`&${key}`, keyIndex2 + 1);
|
|
1395
|
+
}
|
|
1396
|
+
encoded = /[%+]/.test(url);
|
|
1397
|
+
if (!encoded) {
|
|
1398
|
+
return;
|
|
1399
|
+
}
|
|
1400
|
+
}
|
|
1401
|
+
const results = {};
|
|
1402
|
+
encoded ??= /[%+]/.test(url);
|
|
1403
|
+
let keyIndex = url.indexOf("?", 8);
|
|
1404
|
+
while (keyIndex !== -1) {
|
|
1405
|
+
const nextKeyIndex = url.indexOf("&", keyIndex + 1);
|
|
1406
|
+
let valueIndex = url.indexOf("=", keyIndex);
|
|
1407
|
+
if (valueIndex > nextKeyIndex && nextKeyIndex !== -1) {
|
|
1408
|
+
valueIndex = -1;
|
|
1409
|
+
}
|
|
1410
|
+
let name = url.slice(keyIndex + 1, valueIndex === -1 ? nextKeyIndex === -1 ? undefined : nextKeyIndex : valueIndex);
|
|
1411
|
+
if (encoded) {
|
|
1412
|
+
name = _decodeURI(name);
|
|
1413
|
+
}
|
|
1414
|
+
keyIndex = nextKeyIndex;
|
|
1415
|
+
if (name === "") {
|
|
1416
|
+
continue;
|
|
1417
|
+
}
|
|
1418
|
+
let value;
|
|
1419
|
+
if (valueIndex === -1) {
|
|
1420
|
+
value = "";
|
|
1421
|
+
} else {
|
|
1422
|
+
value = url.slice(valueIndex + 1, nextKeyIndex === -1 ? undefined : nextKeyIndex);
|
|
1423
|
+
if (encoded) {
|
|
1424
|
+
value = _decodeURI(value);
|
|
1425
|
+
}
|
|
1426
|
+
}
|
|
1427
|
+
if (multiple) {
|
|
1428
|
+
if (!(results[name] && Array.isArray(results[name]))) {
|
|
1429
|
+
results[name] = [];
|
|
1430
|
+
}
|
|
1431
|
+
results[name].push(value);
|
|
1432
|
+
} else {
|
|
1433
|
+
results[name] ??= value;
|
|
1434
|
+
}
|
|
1435
|
+
}
|
|
1436
|
+
return key ? results[key] : results;
|
|
1437
|
+
};
|
|
1438
|
+
var getQueryParam = _getQueryParam;
|
|
1439
|
+
var getQueryParams = (url, key) => {
|
|
1440
|
+
return _getQueryParam(url, key, true);
|
|
1441
|
+
};
|
|
1442
|
+
var decodeURIComponent_ = decodeURIComponent;
|
|
1443
|
+
|
|
1444
|
+
// ../../node_modules/.bun/hono@4.12.10/node_modules/hono/dist/request.js
|
|
1445
|
+
var tryDecodeURIComponent = (str) => tryDecode(str, decodeURIComponent_);
|
|
1446
|
+
var HonoRequest = class {
|
|
1447
|
+
raw;
|
|
1448
|
+
#validatedData;
|
|
1449
|
+
#matchResult;
|
|
1450
|
+
routeIndex = 0;
|
|
1451
|
+
path;
|
|
1452
|
+
bodyCache = {};
|
|
1453
|
+
constructor(request, path = "/", matchResult = [[]]) {
|
|
1454
|
+
this.raw = request;
|
|
1455
|
+
this.path = path;
|
|
1456
|
+
this.#matchResult = matchResult;
|
|
1457
|
+
this.#validatedData = {};
|
|
1458
|
+
}
|
|
1459
|
+
param(key) {
|
|
1460
|
+
return key ? this.#getDecodedParam(key) : this.#getAllDecodedParams();
|
|
1461
|
+
}
|
|
1462
|
+
#getDecodedParam(key) {
|
|
1463
|
+
const paramKey = this.#matchResult[0][this.routeIndex][1][key];
|
|
1464
|
+
const param = this.#getParamValue(paramKey);
|
|
1465
|
+
return param && /\%/.test(param) ? tryDecodeURIComponent(param) : param;
|
|
1466
|
+
}
|
|
1467
|
+
#getAllDecodedParams() {
|
|
1468
|
+
const decoded = {};
|
|
1469
|
+
const keys = Object.keys(this.#matchResult[0][this.routeIndex][1]);
|
|
1470
|
+
for (const key of keys) {
|
|
1471
|
+
const value = this.#getParamValue(this.#matchResult[0][this.routeIndex][1][key]);
|
|
1472
|
+
if (value !== undefined) {
|
|
1473
|
+
decoded[key] = /\%/.test(value) ? tryDecodeURIComponent(value) : value;
|
|
1474
|
+
}
|
|
1475
|
+
}
|
|
1476
|
+
return decoded;
|
|
1477
|
+
}
|
|
1478
|
+
#getParamValue(paramKey) {
|
|
1479
|
+
return this.#matchResult[1] ? this.#matchResult[1][paramKey] : paramKey;
|
|
1480
|
+
}
|
|
1481
|
+
query(key) {
|
|
1482
|
+
return getQueryParam(this.url, key);
|
|
1483
|
+
}
|
|
1484
|
+
queries(key) {
|
|
1485
|
+
return getQueryParams(this.url, key);
|
|
1486
|
+
}
|
|
1487
|
+
header(name) {
|
|
1488
|
+
if (name) {
|
|
1489
|
+
return this.raw.headers.get(name) ?? undefined;
|
|
1490
|
+
}
|
|
1491
|
+
const headerData = {};
|
|
1492
|
+
this.raw.headers.forEach((value, key) => {
|
|
1493
|
+
headerData[key] = value;
|
|
1494
|
+
});
|
|
1495
|
+
return headerData;
|
|
1496
|
+
}
|
|
1497
|
+
async parseBody(options) {
|
|
1498
|
+
return parseBody(this, options);
|
|
1499
|
+
}
|
|
1500
|
+
#cachedBody = (key) => {
|
|
1501
|
+
const { bodyCache, raw } = this;
|
|
1502
|
+
const cachedBody = bodyCache[key];
|
|
1503
|
+
if (cachedBody) {
|
|
1504
|
+
return cachedBody;
|
|
1505
|
+
}
|
|
1506
|
+
const anyCachedKey = Object.keys(bodyCache)[0];
|
|
1507
|
+
if (anyCachedKey) {
|
|
1508
|
+
return bodyCache[anyCachedKey].then((body) => {
|
|
1509
|
+
if (anyCachedKey === "json") {
|
|
1510
|
+
body = JSON.stringify(body);
|
|
1511
|
+
}
|
|
1512
|
+
return new Response(body)[key]();
|
|
1513
|
+
});
|
|
1514
|
+
}
|
|
1515
|
+
return bodyCache[key] = raw[key]();
|
|
1516
|
+
};
|
|
1517
|
+
json() {
|
|
1518
|
+
return this.#cachedBody("text").then((text) => JSON.parse(text));
|
|
1519
|
+
}
|
|
1520
|
+
text() {
|
|
1521
|
+
return this.#cachedBody("text");
|
|
1522
|
+
}
|
|
1523
|
+
arrayBuffer() {
|
|
1524
|
+
return this.#cachedBody("arrayBuffer");
|
|
1525
|
+
}
|
|
1526
|
+
blob() {
|
|
1527
|
+
return this.#cachedBody("blob");
|
|
1528
|
+
}
|
|
1529
|
+
formData() {
|
|
1530
|
+
return this.#cachedBody("formData");
|
|
1531
|
+
}
|
|
1532
|
+
addValidatedData(target, data) {
|
|
1533
|
+
this.#validatedData[target] = data;
|
|
1534
|
+
}
|
|
1535
|
+
valid(target) {
|
|
1536
|
+
return this.#validatedData[target];
|
|
1537
|
+
}
|
|
1538
|
+
get url() {
|
|
1539
|
+
return this.raw.url;
|
|
1540
|
+
}
|
|
1541
|
+
get method() {
|
|
1542
|
+
return this.raw.method;
|
|
1543
|
+
}
|
|
1544
|
+
get [GET_MATCH_RESULT]() {
|
|
1545
|
+
return this.#matchResult;
|
|
1546
|
+
}
|
|
1547
|
+
get matchedRoutes() {
|
|
1548
|
+
return this.#matchResult[0].map(([[, route]]) => route);
|
|
1549
|
+
}
|
|
1550
|
+
get routePath() {
|
|
1551
|
+
return this.#matchResult[0].map(([[, route]]) => route)[this.routeIndex].path;
|
|
1552
|
+
}
|
|
1553
|
+
};
|
|
1554
|
+
|
|
1555
|
+
// ../../node_modules/.bun/hono@4.12.10/node_modules/hono/dist/utils/html.js
|
|
1556
|
+
var HtmlEscapedCallbackPhase = {
|
|
1557
|
+
Stringify: 1,
|
|
1558
|
+
BeforeStream: 2,
|
|
1559
|
+
Stream: 3
|
|
1560
|
+
};
|
|
1561
|
+
var raw = (value, callbacks) => {
|
|
1562
|
+
const escapedString = new String(value);
|
|
1563
|
+
escapedString.isEscaped = true;
|
|
1564
|
+
escapedString.callbacks = callbacks;
|
|
1565
|
+
return escapedString;
|
|
1566
|
+
};
|
|
1567
|
+
var resolveCallback = async (str, phase, preserveCallbacks, context, buffer) => {
|
|
1568
|
+
if (typeof str === "object" && !(str instanceof String)) {
|
|
1569
|
+
if (!(str instanceof Promise)) {
|
|
1570
|
+
str = str.toString();
|
|
1571
|
+
}
|
|
1572
|
+
if (str instanceof Promise) {
|
|
1573
|
+
str = await str;
|
|
1574
|
+
}
|
|
1575
|
+
}
|
|
1576
|
+
const callbacks = str.callbacks;
|
|
1577
|
+
if (!callbacks?.length) {
|
|
1578
|
+
return Promise.resolve(str);
|
|
1579
|
+
}
|
|
1580
|
+
if (buffer) {
|
|
1581
|
+
buffer[0] += str;
|
|
1582
|
+
} else {
|
|
1583
|
+
buffer = [str];
|
|
1584
|
+
}
|
|
1585
|
+
const resStr = Promise.all(callbacks.map((c) => c({ phase, buffer, context }))).then((res) => Promise.all(res.filter(Boolean).map((str2) => resolveCallback(str2, phase, false, context, buffer))).then(() => buffer[0]));
|
|
1586
|
+
if (preserveCallbacks) {
|
|
1587
|
+
return raw(await resStr, callbacks);
|
|
1588
|
+
} else {
|
|
1589
|
+
return resStr;
|
|
1590
|
+
}
|
|
1591
|
+
};
|
|
1592
|
+
|
|
1593
|
+
// ../../node_modules/.bun/hono@4.12.10/node_modules/hono/dist/context.js
|
|
1594
|
+
var TEXT_PLAIN = "text/plain; charset=UTF-8";
|
|
1595
|
+
var setDefaultContentType = (contentType, headers) => {
|
|
1596
|
+
return {
|
|
1597
|
+
"Content-Type": contentType,
|
|
1598
|
+
...headers
|
|
1599
|
+
};
|
|
1600
|
+
};
|
|
1601
|
+
var createResponseInstance = (body, init) => new Response(body, init);
|
|
1602
|
+
var Context = class {
|
|
1603
|
+
#rawRequest;
|
|
1604
|
+
#req;
|
|
1605
|
+
env = {};
|
|
1606
|
+
#var;
|
|
1607
|
+
finalized = false;
|
|
1608
|
+
error;
|
|
1609
|
+
#status;
|
|
1610
|
+
#executionCtx;
|
|
1611
|
+
#res;
|
|
1612
|
+
#layout;
|
|
1613
|
+
#renderer;
|
|
1614
|
+
#notFoundHandler;
|
|
1615
|
+
#preparedHeaders;
|
|
1616
|
+
#matchResult;
|
|
1617
|
+
#path;
|
|
1618
|
+
constructor(req, options) {
|
|
1619
|
+
this.#rawRequest = req;
|
|
1620
|
+
if (options) {
|
|
1621
|
+
this.#executionCtx = options.executionCtx;
|
|
1622
|
+
this.env = options.env;
|
|
1623
|
+
this.#notFoundHandler = options.notFoundHandler;
|
|
1624
|
+
this.#path = options.path;
|
|
1625
|
+
this.#matchResult = options.matchResult;
|
|
1626
|
+
}
|
|
1627
|
+
}
|
|
1628
|
+
get req() {
|
|
1629
|
+
this.#req ??= new HonoRequest(this.#rawRequest, this.#path, this.#matchResult);
|
|
1630
|
+
return this.#req;
|
|
1631
|
+
}
|
|
1632
|
+
get event() {
|
|
1633
|
+
if (this.#executionCtx && "respondWith" in this.#executionCtx) {
|
|
1634
|
+
return this.#executionCtx;
|
|
1635
|
+
} else {
|
|
1636
|
+
throw Error("This context has no FetchEvent");
|
|
1637
|
+
}
|
|
1638
|
+
}
|
|
1639
|
+
get executionCtx() {
|
|
1640
|
+
if (this.#executionCtx) {
|
|
1641
|
+
return this.#executionCtx;
|
|
1642
|
+
} else {
|
|
1643
|
+
throw Error("This context has no ExecutionContext");
|
|
1644
|
+
}
|
|
1645
|
+
}
|
|
1646
|
+
get res() {
|
|
1647
|
+
return this.#res ||= createResponseInstance(null, {
|
|
1648
|
+
headers: this.#preparedHeaders ??= new Headers
|
|
1649
|
+
});
|
|
1650
|
+
}
|
|
1651
|
+
set res(_res) {
|
|
1652
|
+
if (this.#res && _res) {
|
|
1653
|
+
_res = createResponseInstance(_res.body, _res);
|
|
1654
|
+
for (const [k, v] of this.#res.headers.entries()) {
|
|
1655
|
+
if (k === "content-type") {
|
|
1656
|
+
continue;
|
|
1657
|
+
}
|
|
1658
|
+
if (k === "set-cookie") {
|
|
1659
|
+
const cookies = this.#res.headers.getSetCookie();
|
|
1660
|
+
_res.headers.delete("set-cookie");
|
|
1661
|
+
for (const cookie of cookies) {
|
|
1662
|
+
_res.headers.append("set-cookie", cookie);
|
|
1663
|
+
}
|
|
1664
|
+
} else {
|
|
1665
|
+
_res.headers.set(k, v);
|
|
1666
|
+
}
|
|
1667
|
+
}
|
|
1668
|
+
}
|
|
1669
|
+
this.#res = _res;
|
|
1670
|
+
this.finalized = true;
|
|
1671
|
+
}
|
|
1672
|
+
render = (...args) => {
|
|
1673
|
+
this.#renderer ??= (content) => this.html(content);
|
|
1674
|
+
return this.#renderer(...args);
|
|
1675
|
+
};
|
|
1676
|
+
setLayout = (layout) => this.#layout = layout;
|
|
1677
|
+
getLayout = () => this.#layout;
|
|
1678
|
+
setRenderer = (renderer) => {
|
|
1679
|
+
this.#renderer = renderer;
|
|
1680
|
+
};
|
|
1681
|
+
header = (name, value, options) => {
|
|
1682
|
+
if (this.finalized) {
|
|
1683
|
+
this.#res = createResponseInstance(this.#res.body, this.#res);
|
|
1684
|
+
}
|
|
1685
|
+
const headers = this.#res ? this.#res.headers : this.#preparedHeaders ??= new Headers;
|
|
1686
|
+
if (value === undefined) {
|
|
1687
|
+
headers.delete(name);
|
|
1688
|
+
} else if (options?.append) {
|
|
1689
|
+
headers.append(name, value);
|
|
1690
|
+
} else {
|
|
1691
|
+
headers.set(name, value);
|
|
1692
|
+
}
|
|
1693
|
+
};
|
|
1694
|
+
status = (status) => {
|
|
1695
|
+
this.#status = status;
|
|
1696
|
+
};
|
|
1697
|
+
set = (key, value) => {
|
|
1698
|
+
this.#var ??= /* @__PURE__ */ new Map;
|
|
1699
|
+
this.#var.set(key, value);
|
|
1700
|
+
};
|
|
1701
|
+
get = (key) => {
|
|
1702
|
+
return this.#var ? this.#var.get(key) : undefined;
|
|
1703
|
+
};
|
|
1704
|
+
get var() {
|
|
1705
|
+
if (!this.#var) {
|
|
1706
|
+
return {};
|
|
1707
|
+
}
|
|
1708
|
+
return Object.fromEntries(this.#var);
|
|
1709
|
+
}
|
|
1710
|
+
#newResponse(data, arg, headers) {
|
|
1711
|
+
const responseHeaders = this.#res ? new Headers(this.#res.headers) : this.#preparedHeaders ?? new Headers;
|
|
1712
|
+
if (typeof arg === "object" && "headers" in arg) {
|
|
1713
|
+
const argHeaders = arg.headers instanceof Headers ? arg.headers : new Headers(arg.headers);
|
|
1714
|
+
for (const [key, value] of argHeaders) {
|
|
1715
|
+
if (key.toLowerCase() === "set-cookie") {
|
|
1716
|
+
responseHeaders.append(key, value);
|
|
1717
|
+
} else {
|
|
1718
|
+
responseHeaders.set(key, value);
|
|
1719
|
+
}
|
|
1720
|
+
}
|
|
1721
|
+
}
|
|
1722
|
+
if (headers) {
|
|
1723
|
+
for (const [k, v] of Object.entries(headers)) {
|
|
1724
|
+
if (typeof v === "string") {
|
|
1725
|
+
responseHeaders.set(k, v);
|
|
1726
|
+
} else {
|
|
1727
|
+
responseHeaders.delete(k);
|
|
1728
|
+
for (const v2 of v) {
|
|
1729
|
+
responseHeaders.append(k, v2);
|
|
1730
|
+
}
|
|
1731
|
+
}
|
|
1732
|
+
}
|
|
1733
|
+
}
|
|
1734
|
+
const status = typeof arg === "number" ? arg : arg?.status ?? this.#status;
|
|
1735
|
+
return createResponseInstance(data, { status, headers: responseHeaders });
|
|
1736
|
+
}
|
|
1737
|
+
newResponse = (...args) => this.#newResponse(...args);
|
|
1738
|
+
body = (data, arg, headers) => this.#newResponse(data, arg, headers);
|
|
1739
|
+
text = (text, arg, headers) => {
|
|
1740
|
+
return !this.#preparedHeaders && !this.#status && !arg && !headers && !this.finalized ? new Response(text) : this.#newResponse(text, arg, setDefaultContentType(TEXT_PLAIN, headers));
|
|
1741
|
+
};
|
|
1742
|
+
json = (object2, arg, headers) => {
|
|
1743
|
+
return this.#newResponse(JSON.stringify(object2), arg, setDefaultContentType("application/json", headers));
|
|
1744
|
+
};
|
|
1745
|
+
html = (html, arg, headers) => {
|
|
1746
|
+
const res = (html2) => this.#newResponse(html2, arg, setDefaultContentType("text/html; charset=UTF-8", headers));
|
|
1747
|
+
return typeof html === "object" ? resolveCallback(html, HtmlEscapedCallbackPhase.Stringify, false, {}).then(res) : res(html);
|
|
1748
|
+
};
|
|
1749
|
+
redirect = (location, status) => {
|
|
1750
|
+
const locationString = String(location);
|
|
1751
|
+
this.header("Location", !/[^\x00-\xFF]/.test(locationString) ? locationString : encodeURI(locationString));
|
|
1752
|
+
return this.newResponse(null, status ?? 302);
|
|
1753
|
+
};
|
|
1754
|
+
notFound = () => {
|
|
1755
|
+
this.#notFoundHandler ??= () => createResponseInstance();
|
|
1756
|
+
return this.#notFoundHandler(this);
|
|
1757
|
+
};
|
|
1758
|
+
};
|
|
1759
|
+
|
|
1760
|
+
// ../../node_modules/.bun/hono@4.12.10/node_modules/hono/dist/router.js
|
|
1761
|
+
var METHOD_NAME_ALL = "ALL";
|
|
1762
|
+
var METHOD_NAME_ALL_LOWERCASE = "all";
|
|
1763
|
+
var METHODS = ["get", "post", "put", "delete", "options", "patch"];
|
|
1764
|
+
var MESSAGE_MATCHER_IS_ALREADY_BUILT = "Can not add a route since the matcher is already built.";
|
|
1765
|
+
var UnsupportedPathError = class extends Error {
|
|
1766
|
+
};
|
|
1767
|
+
|
|
1768
|
+
// ../../node_modules/.bun/hono@4.12.10/node_modules/hono/dist/utils/constants.js
|
|
1769
|
+
var COMPOSED_HANDLER = "__COMPOSED_HANDLER";
|
|
1770
|
+
|
|
1771
|
+
// ../../node_modules/.bun/hono@4.12.10/node_modules/hono/dist/hono-base.js
|
|
1772
|
+
var notFoundHandler = (c) => {
|
|
1773
|
+
return c.text("404 Not Found", 404);
|
|
1774
|
+
};
|
|
1775
|
+
var errorHandler = (err, c) => {
|
|
1776
|
+
if ("getResponse" in err) {
|
|
1777
|
+
const res = err.getResponse();
|
|
1778
|
+
return c.newResponse(res.body, res);
|
|
1779
|
+
}
|
|
1780
|
+
console.error(err);
|
|
1781
|
+
return c.text("Internal Server Error", 500);
|
|
1782
|
+
};
|
|
1783
|
+
var Hono = class _Hono {
|
|
1784
|
+
get;
|
|
1785
|
+
post;
|
|
1786
|
+
put;
|
|
1787
|
+
delete;
|
|
1788
|
+
options;
|
|
1789
|
+
patch;
|
|
1790
|
+
all;
|
|
1791
|
+
on;
|
|
1792
|
+
use;
|
|
1793
|
+
router;
|
|
1794
|
+
getPath;
|
|
1795
|
+
_basePath = "/";
|
|
1796
|
+
#path = "/";
|
|
1797
|
+
routes = [];
|
|
1798
|
+
constructor(options = {}) {
|
|
1799
|
+
const allMethods = [...METHODS, METHOD_NAME_ALL_LOWERCASE];
|
|
1800
|
+
allMethods.forEach((method) => {
|
|
1801
|
+
this[method] = (args1, ...args) => {
|
|
1802
|
+
if (typeof args1 === "string") {
|
|
1803
|
+
this.#path = args1;
|
|
1804
|
+
} else {
|
|
1805
|
+
this.#addRoute(method, this.#path, args1);
|
|
1806
|
+
}
|
|
1807
|
+
args.forEach((handler) => {
|
|
1808
|
+
this.#addRoute(method, this.#path, handler);
|
|
1809
|
+
});
|
|
1810
|
+
return this;
|
|
1811
|
+
};
|
|
1812
|
+
});
|
|
1813
|
+
this.on = (method, path, ...handlers) => {
|
|
1814
|
+
for (const p of [path].flat()) {
|
|
1815
|
+
this.#path = p;
|
|
1816
|
+
for (const m of [method].flat()) {
|
|
1817
|
+
handlers.map((handler) => {
|
|
1818
|
+
this.#addRoute(m.toUpperCase(), this.#path, handler);
|
|
1819
|
+
});
|
|
1820
|
+
}
|
|
1821
|
+
}
|
|
1822
|
+
return this;
|
|
1823
|
+
};
|
|
1824
|
+
this.use = (arg1, ...handlers) => {
|
|
1825
|
+
if (typeof arg1 === "string") {
|
|
1826
|
+
this.#path = arg1;
|
|
1827
|
+
} else {
|
|
1828
|
+
this.#path = "*";
|
|
1829
|
+
handlers.unshift(arg1);
|
|
1830
|
+
}
|
|
1831
|
+
handlers.forEach((handler) => {
|
|
1832
|
+
this.#addRoute(METHOD_NAME_ALL, this.#path, handler);
|
|
1833
|
+
});
|
|
1834
|
+
return this;
|
|
1835
|
+
};
|
|
1836
|
+
const { strict, ...optionsWithoutStrict } = options;
|
|
1837
|
+
Object.assign(this, optionsWithoutStrict);
|
|
1838
|
+
this.getPath = strict ?? true ? options.getPath ?? getPath : getPathNoStrict;
|
|
1839
|
+
}
|
|
1840
|
+
#clone() {
|
|
1841
|
+
const clone = new _Hono({
|
|
1842
|
+
router: this.router,
|
|
1843
|
+
getPath: this.getPath
|
|
1844
|
+
});
|
|
1845
|
+
clone.errorHandler = this.errorHandler;
|
|
1846
|
+
clone.#notFoundHandler = this.#notFoundHandler;
|
|
1847
|
+
clone.routes = this.routes;
|
|
1848
|
+
return clone;
|
|
1849
|
+
}
|
|
1850
|
+
#notFoundHandler = notFoundHandler;
|
|
1851
|
+
errorHandler = errorHandler;
|
|
1852
|
+
route(path, app) {
|
|
1853
|
+
const subApp = this.basePath(path);
|
|
1854
|
+
app.routes.map((r) => {
|
|
1855
|
+
let handler;
|
|
1856
|
+
if (app.errorHandler === errorHandler) {
|
|
1857
|
+
handler = r.handler;
|
|
1858
|
+
} else {
|
|
1859
|
+
handler = async (c, next) => (await compose([], app.errorHandler)(c, () => r.handler(c, next))).res;
|
|
1860
|
+
handler[COMPOSED_HANDLER] = r.handler;
|
|
1861
|
+
}
|
|
1862
|
+
subApp.#addRoute(r.method, r.path, handler);
|
|
1863
|
+
});
|
|
1864
|
+
return this;
|
|
1865
|
+
}
|
|
1866
|
+
basePath(path) {
|
|
1867
|
+
const subApp = this.#clone();
|
|
1868
|
+
subApp._basePath = mergePath(this._basePath, path);
|
|
1869
|
+
return subApp;
|
|
1870
|
+
}
|
|
1871
|
+
onError = (handler) => {
|
|
1872
|
+
this.errorHandler = handler;
|
|
1873
|
+
return this;
|
|
1874
|
+
};
|
|
1875
|
+
notFound = (handler) => {
|
|
1876
|
+
this.#notFoundHandler = handler;
|
|
1877
|
+
return this;
|
|
1878
|
+
};
|
|
1879
|
+
mount(path, applicationHandler, options) {
|
|
1880
|
+
let replaceRequest;
|
|
1881
|
+
let optionHandler;
|
|
1882
|
+
if (options) {
|
|
1883
|
+
if (typeof options === "function") {
|
|
1884
|
+
optionHandler = options;
|
|
1885
|
+
} else {
|
|
1886
|
+
optionHandler = options.optionHandler;
|
|
1887
|
+
if (options.replaceRequest === false) {
|
|
1888
|
+
replaceRequest = (request) => request;
|
|
1889
|
+
} else {
|
|
1890
|
+
replaceRequest = options.replaceRequest;
|
|
1891
|
+
}
|
|
1892
|
+
}
|
|
1893
|
+
}
|
|
1894
|
+
const getOptions = optionHandler ? (c) => {
|
|
1895
|
+
const options2 = optionHandler(c);
|
|
1896
|
+
return Array.isArray(options2) ? options2 : [options2];
|
|
1897
|
+
} : (c) => {
|
|
1898
|
+
let executionContext = undefined;
|
|
1899
|
+
try {
|
|
1900
|
+
executionContext = c.executionCtx;
|
|
1901
|
+
} catch {}
|
|
1902
|
+
return [c.env, executionContext];
|
|
1903
|
+
};
|
|
1904
|
+
replaceRequest ||= (() => {
|
|
1905
|
+
const mergedPath = mergePath(this._basePath, path);
|
|
1906
|
+
const pathPrefixLength = mergedPath === "/" ? 0 : mergedPath.length;
|
|
1907
|
+
return (request) => {
|
|
1908
|
+
const url = new URL(request.url);
|
|
1909
|
+
url.pathname = url.pathname.slice(pathPrefixLength) || "/";
|
|
1910
|
+
return new Request(url, request);
|
|
1911
|
+
};
|
|
1912
|
+
})();
|
|
1913
|
+
const handler = async (c, next) => {
|
|
1914
|
+
const res = await applicationHandler(replaceRequest(c.req.raw), ...getOptions(c));
|
|
1915
|
+
if (res) {
|
|
1916
|
+
return res;
|
|
1917
|
+
}
|
|
1918
|
+
await next();
|
|
1919
|
+
};
|
|
1920
|
+
this.#addRoute(METHOD_NAME_ALL, mergePath(path, "*"), handler);
|
|
1921
|
+
return this;
|
|
1922
|
+
}
|
|
1923
|
+
#addRoute(method, path, handler) {
|
|
1924
|
+
method = method.toUpperCase();
|
|
1925
|
+
path = mergePath(this._basePath, path);
|
|
1926
|
+
const r = { basePath: this._basePath, path, method, handler };
|
|
1927
|
+
this.router.add(method, path, [handler, r]);
|
|
1928
|
+
this.routes.push(r);
|
|
1929
|
+
}
|
|
1930
|
+
#handleError(err, c) {
|
|
1931
|
+
if (err instanceof Error) {
|
|
1932
|
+
return this.errorHandler(err, c);
|
|
1933
|
+
}
|
|
1934
|
+
throw err;
|
|
1935
|
+
}
|
|
1936
|
+
#dispatch(request, executionCtx, env, method) {
|
|
1937
|
+
if (method === "HEAD") {
|
|
1938
|
+
return (async () => new Response(null, await this.#dispatch(request, executionCtx, env, "GET")))();
|
|
1939
|
+
}
|
|
1940
|
+
const path = this.getPath(request, { env });
|
|
1941
|
+
const matchResult = this.router.match(method, path);
|
|
1942
|
+
const c = new Context(request, {
|
|
1943
|
+
path,
|
|
1944
|
+
matchResult,
|
|
1945
|
+
env,
|
|
1946
|
+
executionCtx,
|
|
1947
|
+
notFoundHandler: this.#notFoundHandler
|
|
1948
|
+
});
|
|
1949
|
+
if (matchResult[0].length === 1) {
|
|
1950
|
+
let res;
|
|
1951
|
+
try {
|
|
1952
|
+
res = matchResult[0][0][0][0](c, async () => {
|
|
1953
|
+
c.res = await this.#notFoundHandler(c);
|
|
1954
|
+
});
|
|
1955
|
+
} catch (err) {
|
|
1956
|
+
return this.#handleError(err, c);
|
|
1957
|
+
}
|
|
1958
|
+
return res instanceof Promise ? res.then((resolved) => resolved || (c.finalized ? c.res : this.#notFoundHandler(c))).catch((err) => this.#handleError(err, c)) : res ?? this.#notFoundHandler(c);
|
|
1959
|
+
}
|
|
1960
|
+
const composed = compose(matchResult[0], this.errorHandler, this.#notFoundHandler);
|
|
1961
|
+
return (async () => {
|
|
1962
|
+
try {
|
|
1963
|
+
const context = await composed(c);
|
|
1964
|
+
if (!context.finalized) {
|
|
1965
|
+
throw new Error("Context is not finalized. Did you forget to return a Response object or `await next()`?");
|
|
1966
|
+
}
|
|
1967
|
+
return context.res;
|
|
1968
|
+
} catch (err) {
|
|
1969
|
+
return this.#handleError(err, c);
|
|
1970
|
+
}
|
|
1971
|
+
})();
|
|
1972
|
+
}
|
|
1973
|
+
fetch = (request, ...rest) => {
|
|
1974
|
+
return this.#dispatch(request, rest[1], rest[0], request.method);
|
|
1975
|
+
};
|
|
1976
|
+
request = (input, requestInit, Env, executionCtx) => {
|
|
1977
|
+
if (input instanceof Request) {
|
|
1978
|
+
return this.fetch(requestInit ? new Request(input, requestInit) : input, Env, executionCtx);
|
|
1979
|
+
}
|
|
1980
|
+
input = input.toString();
|
|
1981
|
+
return this.fetch(new Request(/^https?:\/\//.test(input) ? input : `http://localhost${mergePath("/", input)}`, requestInit), Env, executionCtx);
|
|
1982
|
+
};
|
|
1983
|
+
fire = () => {
|
|
1984
|
+
addEventListener("fetch", (event) => {
|
|
1985
|
+
event.respondWith(this.#dispatch(event.request, event, undefined, event.request.method));
|
|
1986
|
+
});
|
|
1987
|
+
};
|
|
1988
|
+
};
|
|
1989
|
+
|
|
1990
|
+
// ../../node_modules/.bun/hono@4.12.10/node_modules/hono/dist/router/reg-exp-router/matcher.js
|
|
1991
|
+
var emptyParam = [];
|
|
1992
|
+
function match(method, path) {
|
|
1993
|
+
const matchers = this.buildAllMatchers();
|
|
1994
|
+
const match2 = (method2, path2) => {
|
|
1995
|
+
const matcher = matchers[method2] || matchers[METHOD_NAME_ALL];
|
|
1996
|
+
const staticMatch = matcher[2][path2];
|
|
1997
|
+
if (staticMatch) {
|
|
1998
|
+
return staticMatch;
|
|
1999
|
+
}
|
|
2000
|
+
const match3 = path2.match(matcher[0]);
|
|
2001
|
+
if (!match3) {
|
|
2002
|
+
return [[], emptyParam];
|
|
2003
|
+
}
|
|
2004
|
+
const index = match3.indexOf("", 1);
|
|
2005
|
+
return [matcher[1][index], match3];
|
|
2006
|
+
};
|
|
2007
|
+
this.match = match2;
|
|
2008
|
+
return match2(method, path);
|
|
2009
|
+
}
|
|
2010
|
+
|
|
2011
|
+
// ../../node_modules/.bun/hono@4.12.10/node_modules/hono/dist/router/reg-exp-router/node.js
|
|
2012
|
+
var LABEL_REG_EXP_STR = "[^/]+";
|
|
2013
|
+
var ONLY_WILDCARD_REG_EXP_STR = ".*";
|
|
2014
|
+
var TAIL_WILDCARD_REG_EXP_STR = "(?:|/.*)";
|
|
2015
|
+
var PATH_ERROR = /* @__PURE__ */ Symbol();
|
|
2016
|
+
var regExpMetaChars = new Set(".\\+*[^]$()");
|
|
2017
|
+
function compareKey(a, b) {
|
|
2018
|
+
if (a.length === 1) {
|
|
2019
|
+
return b.length === 1 ? a < b ? -1 : 1 : -1;
|
|
2020
|
+
}
|
|
2021
|
+
if (b.length === 1) {
|
|
2022
|
+
return 1;
|
|
2023
|
+
}
|
|
2024
|
+
if (a === ONLY_WILDCARD_REG_EXP_STR || a === TAIL_WILDCARD_REG_EXP_STR) {
|
|
2025
|
+
return 1;
|
|
2026
|
+
} else if (b === ONLY_WILDCARD_REG_EXP_STR || b === TAIL_WILDCARD_REG_EXP_STR) {
|
|
2027
|
+
return -1;
|
|
2028
|
+
}
|
|
2029
|
+
if (a === LABEL_REG_EXP_STR) {
|
|
2030
|
+
return 1;
|
|
2031
|
+
} else if (b === LABEL_REG_EXP_STR) {
|
|
2032
|
+
return -1;
|
|
2033
|
+
}
|
|
2034
|
+
return a.length === b.length ? a < b ? -1 : 1 : b.length - a.length;
|
|
2035
|
+
}
|
|
2036
|
+
var Node = class _Node {
|
|
2037
|
+
#index;
|
|
2038
|
+
#varIndex;
|
|
2039
|
+
#children = /* @__PURE__ */ Object.create(null);
|
|
2040
|
+
insert(tokens, index, paramMap, context, pathErrorCheckOnly) {
|
|
2041
|
+
if (tokens.length === 0) {
|
|
2042
|
+
if (this.#index !== undefined) {
|
|
2043
|
+
throw PATH_ERROR;
|
|
2044
|
+
}
|
|
2045
|
+
if (pathErrorCheckOnly) {
|
|
2046
|
+
return;
|
|
2047
|
+
}
|
|
2048
|
+
this.#index = index;
|
|
2049
|
+
return;
|
|
2050
|
+
}
|
|
2051
|
+
const [token, ...restTokens] = tokens;
|
|
2052
|
+
const pattern = token === "*" ? restTokens.length === 0 ? ["", "", ONLY_WILDCARD_REG_EXP_STR] : ["", "", LABEL_REG_EXP_STR] : token === "/*" ? ["", "", TAIL_WILDCARD_REG_EXP_STR] : token.match(/^\:([^\{\}]+)(?:\{(.+)\})?$/);
|
|
2053
|
+
let node;
|
|
2054
|
+
if (pattern) {
|
|
2055
|
+
const name = pattern[1];
|
|
2056
|
+
let regexpStr = pattern[2] || LABEL_REG_EXP_STR;
|
|
2057
|
+
if (name && pattern[2]) {
|
|
2058
|
+
if (regexpStr === ".*") {
|
|
2059
|
+
throw PATH_ERROR;
|
|
2060
|
+
}
|
|
2061
|
+
regexpStr = regexpStr.replace(/^\((?!\?:)(?=[^)]+\)$)/, "(?:");
|
|
2062
|
+
if (/\((?!\?:)/.test(regexpStr)) {
|
|
2063
|
+
throw PATH_ERROR;
|
|
2064
|
+
}
|
|
2065
|
+
}
|
|
2066
|
+
node = this.#children[regexpStr];
|
|
2067
|
+
if (!node) {
|
|
2068
|
+
if (Object.keys(this.#children).some((k) => k !== ONLY_WILDCARD_REG_EXP_STR && k !== TAIL_WILDCARD_REG_EXP_STR)) {
|
|
2069
|
+
throw PATH_ERROR;
|
|
2070
|
+
}
|
|
2071
|
+
if (pathErrorCheckOnly) {
|
|
2072
|
+
return;
|
|
2073
|
+
}
|
|
2074
|
+
node = this.#children[regexpStr] = new _Node;
|
|
2075
|
+
if (name !== "") {
|
|
2076
|
+
node.#varIndex = context.varIndex++;
|
|
2077
|
+
}
|
|
2078
|
+
}
|
|
2079
|
+
if (!pathErrorCheckOnly && name !== "") {
|
|
2080
|
+
paramMap.push([name, node.#varIndex]);
|
|
2081
|
+
}
|
|
2082
|
+
} else {
|
|
2083
|
+
node = this.#children[token];
|
|
2084
|
+
if (!node) {
|
|
2085
|
+
if (Object.keys(this.#children).some((k) => k.length > 1 && k !== ONLY_WILDCARD_REG_EXP_STR && k !== TAIL_WILDCARD_REG_EXP_STR)) {
|
|
2086
|
+
throw PATH_ERROR;
|
|
2087
|
+
}
|
|
2088
|
+
if (pathErrorCheckOnly) {
|
|
2089
|
+
return;
|
|
2090
|
+
}
|
|
2091
|
+
node = this.#children[token] = new _Node;
|
|
2092
|
+
}
|
|
2093
|
+
}
|
|
2094
|
+
node.insert(restTokens, index, paramMap, context, pathErrorCheckOnly);
|
|
2095
|
+
}
|
|
2096
|
+
buildRegExpStr() {
|
|
2097
|
+
const childKeys = Object.keys(this.#children).sort(compareKey);
|
|
2098
|
+
const strList = childKeys.map((k) => {
|
|
2099
|
+
const c = this.#children[k];
|
|
2100
|
+
return (typeof c.#varIndex === "number" ? `(${k})@${c.#varIndex}` : regExpMetaChars.has(k) ? `\\${k}` : k) + c.buildRegExpStr();
|
|
2101
|
+
});
|
|
2102
|
+
if (typeof this.#index === "number") {
|
|
2103
|
+
strList.unshift(`#${this.#index}`);
|
|
2104
|
+
}
|
|
2105
|
+
if (strList.length === 0) {
|
|
2106
|
+
return "";
|
|
2107
|
+
}
|
|
2108
|
+
if (strList.length === 1) {
|
|
2109
|
+
return strList[0];
|
|
2110
|
+
}
|
|
2111
|
+
return "(?:" + strList.join("|") + ")";
|
|
2112
|
+
}
|
|
2113
|
+
};
|
|
2114
|
+
|
|
2115
|
+
// ../../node_modules/.bun/hono@4.12.10/node_modules/hono/dist/router/reg-exp-router/trie.js
|
|
2116
|
+
var Trie = class {
|
|
2117
|
+
#context = { varIndex: 0 };
|
|
2118
|
+
#root = new Node;
|
|
2119
|
+
insert(path, index, pathErrorCheckOnly) {
|
|
2120
|
+
const paramAssoc = [];
|
|
2121
|
+
const groups = [];
|
|
2122
|
+
for (let i = 0;; ) {
|
|
2123
|
+
let replaced = false;
|
|
2124
|
+
path = path.replace(/\{[^}]+\}/g, (m) => {
|
|
2125
|
+
const mark = `@\\${i}`;
|
|
2126
|
+
groups[i] = [mark, m];
|
|
2127
|
+
i++;
|
|
2128
|
+
replaced = true;
|
|
2129
|
+
return mark;
|
|
2130
|
+
});
|
|
2131
|
+
if (!replaced) {
|
|
2132
|
+
break;
|
|
2133
|
+
}
|
|
2134
|
+
}
|
|
2135
|
+
const tokens = path.match(/(?::[^\/]+)|(?:\/\*$)|./g) || [];
|
|
2136
|
+
for (let i = groups.length - 1;i >= 0; i--) {
|
|
2137
|
+
const [mark] = groups[i];
|
|
2138
|
+
for (let j = tokens.length - 1;j >= 0; j--) {
|
|
2139
|
+
if (tokens[j].indexOf(mark) !== -1) {
|
|
2140
|
+
tokens[j] = tokens[j].replace(mark, groups[i][1]);
|
|
2141
|
+
break;
|
|
2142
|
+
}
|
|
2143
|
+
}
|
|
2144
|
+
}
|
|
2145
|
+
this.#root.insert(tokens, index, paramAssoc, this.#context, pathErrorCheckOnly);
|
|
2146
|
+
return paramAssoc;
|
|
2147
|
+
}
|
|
2148
|
+
buildRegExp() {
|
|
2149
|
+
let regexp = this.#root.buildRegExpStr();
|
|
2150
|
+
if (regexp === "") {
|
|
2151
|
+
return [/^$/, [], []];
|
|
2152
|
+
}
|
|
2153
|
+
let captureIndex = 0;
|
|
2154
|
+
const indexReplacementMap = [];
|
|
2155
|
+
const paramReplacementMap = [];
|
|
2156
|
+
regexp = regexp.replace(/#(\d+)|@(\d+)|\.\*\$/g, (_, handlerIndex, paramIndex) => {
|
|
2157
|
+
if (handlerIndex !== undefined) {
|
|
2158
|
+
indexReplacementMap[++captureIndex] = Number(handlerIndex);
|
|
2159
|
+
return "$()";
|
|
2160
|
+
}
|
|
2161
|
+
if (paramIndex !== undefined) {
|
|
2162
|
+
paramReplacementMap[Number(paramIndex)] = ++captureIndex;
|
|
2163
|
+
return "";
|
|
2164
|
+
}
|
|
2165
|
+
return "";
|
|
2166
|
+
});
|
|
2167
|
+
return [new RegExp(`^${regexp}`), indexReplacementMap, paramReplacementMap];
|
|
2168
|
+
}
|
|
2169
|
+
};
|
|
2170
|
+
|
|
2171
|
+
// ../../node_modules/.bun/hono@4.12.10/node_modules/hono/dist/router/reg-exp-router/router.js
|
|
2172
|
+
var nullMatcher = [/^$/, [], /* @__PURE__ */ Object.create(null)];
|
|
2173
|
+
var wildcardRegExpCache = /* @__PURE__ */ Object.create(null);
|
|
2174
|
+
function buildWildcardRegExp(path) {
|
|
2175
|
+
return wildcardRegExpCache[path] ??= new RegExp(path === "*" ? "" : `^${path.replace(/\/\*$|([.\\+*[^\]$()])/g, (_, metaChar) => metaChar ? `\\${metaChar}` : "(?:|/.*)")}$`);
|
|
2176
|
+
}
|
|
2177
|
+
function clearWildcardRegExpCache() {
|
|
2178
|
+
wildcardRegExpCache = /* @__PURE__ */ Object.create(null);
|
|
2179
|
+
}
|
|
2180
|
+
function buildMatcherFromPreprocessedRoutes(routes) {
|
|
2181
|
+
const trie = new Trie;
|
|
2182
|
+
const handlerData = [];
|
|
2183
|
+
if (routes.length === 0) {
|
|
2184
|
+
return nullMatcher;
|
|
2185
|
+
}
|
|
2186
|
+
const routesWithStaticPathFlag = routes.map((route) => [!/\*|\/:/.test(route[0]), ...route]).sort(([isStaticA, pathA], [isStaticB, pathB]) => isStaticA ? 1 : isStaticB ? -1 : pathA.length - pathB.length);
|
|
2187
|
+
const staticMap = /* @__PURE__ */ Object.create(null);
|
|
2188
|
+
for (let i = 0, j = -1, len = routesWithStaticPathFlag.length;i < len; i++) {
|
|
2189
|
+
const [pathErrorCheckOnly, path, handlers] = routesWithStaticPathFlag[i];
|
|
2190
|
+
if (pathErrorCheckOnly) {
|
|
2191
|
+
staticMap[path] = [handlers.map(([h]) => [h, /* @__PURE__ */ Object.create(null)]), emptyParam];
|
|
2192
|
+
} else {
|
|
2193
|
+
j++;
|
|
2194
|
+
}
|
|
2195
|
+
let paramAssoc;
|
|
2196
|
+
try {
|
|
2197
|
+
paramAssoc = trie.insert(path, j, pathErrorCheckOnly);
|
|
2198
|
+
} catch (e) {
|
|
2199
|
+
throw e === PATH_ERROR ? new UnsupportedPathError(path) : e;
|
|
2200
|
+
}
|
|
2201
|
+
if (pathErrorCheckOnly) {
|
|
2202
|
+
continue;
|
|
2203
|
+
}
|
|
2204
|
+
handlerData[j] = handlers.map(([h, paramCount]) => {
|
|
2205
|
+
const paramIndexMap = /* @__PURE__ */ Object.create(null);
|
|
2206
|
+
paramCount -= 1;
|
|
2207
|
+
for (;paramCount >= 0; paramCount--) {
|
|
2208
|
+
const [key, value] = paramAssoc[paramCount];
|
|
2209
|
+
paramIndexMap[key] = value;
|
|
2210
|
+
}
|
|
2211
|
+
return [h, paramIndexMap];
|
|
2212
|
+
});
|
|
2213
|
+
}
|
|
2214
|
+
const [regexp, indexReplacementMap, paramReplacementMap] = trie.buildRegExp();
|
|
2215
|
+
for (let i = 0, len = handlerData.length;i < len; i++) {
|
|
2216
|
+
for (let j = 0, len2 = handlerData[i].length;j < len2; j++) {
|
|
2217
|
+
const map = handlerData[i][j]?.[1];
|
|
2218
|
+
if (!map) {
|
|
2219
|
+
continue;
|
|
2220
|
+
}
|
|
2221
|
+
const keys = Object.keys(map);
|
|
2222
|
+
for (let k = 0, len3 = keys.length;k < len3; k++) {
|
|
2223
|
+
map[keys[k]] = paramReplacementMap[map[keys[k]]];
|
|
2224
|
+
}
|
|
2225
|
+
}
|
|
2226
|
+
}
|
|
2227
|
+
const handlerMap = [];
|
|
2228
|
+
for (const i in indexReplacementMap) {
|
|
2229
|
+
handlerMap[i] = handlerData[indexReplacementMap[i]];
|
|
2230
|
+
}
|
|
2231
|
+
return [regexp, handlerMap, staticMap];
|
|
2232
|
+
}
|
|
2233
|
+
function findMiddleware(middleware, path) {
|
|
2234
|
+
if (!middleware) {
|
|
2235
|
+
return;
|
|
2236
|
+
}
|
|
2237
|
+
for (const k of Object.keys(middleware).sort((a, b) => b.length - a.length)) {
|
|
2238
|
+
if (buildWildcardRegExp(k).test(path)) {
|
|
2239
|
+
return [...middleware[k]];
|
|
2240
|
+
}
|
|
2241
|
+
}
|
|
2242
|
+
return;
|
|
2243
|
+
}
|
|
2244
|
+
var RegExpRouter = class {
|
|
2245
|
+
name = "RegExpRouter";
|
|
2246
|
+
#middleware;
|
|
2247
|
+
#routes;
|
|
2248
|
+
constructor() {
|
|
2249
|
+
this.#middleware = { [METHOD_NAME_ALL]: /* @__PURE__ */ Object.create(null) };
|
|
2250
|
+
this.#routes = { [METHOD_NAME_ALL]: /* @__PURE__ */ Object.create(null) };
|
|
2251
|
+
}
|
|
2252
|
+
add(method, path, handler) {
|
|
2253
|
+
const middleware = this.#middleware;
|
|
2254
|
+
const routes = this.#routes;
|
|
2255
|
+
if (!middleware || !routes) {
|
|
2256
|
+
throw new Error(MESSAGE_MATCHER_IS_ALREADY_BUILT);
|
|
2257
|
+
}
|
|
2258
|
+
if (!middleware[method]) {
|
|
2259
|
+
[middleware, routes].forEach((handlerMap) => {
|
|
2260
|
+
handlerMap[method] = /* @__PURE__ */ Object.create(null);
|
|
2261
|
+
Object.keys(handlerMap[METHOD_NAME_ALL]).forEach((p) => {
|
|
2262
|
+
handlerMap[method][p] = [...handlerMap[METHOD_NAME_ALL][p]];
|
|
2263
|
+
});
|
|
2264
|
+
});
|
|
2265
|
+
}
|
|
2266
|
+
if (path === "/*") {
|
|
2267
|
+
path = "*";
|
|
2268
|
+
}
|
|
2269
|
+
const paramCount = (path.match(/\/:/g) || []).length;
|
|
2270
|
+
if (/\*$/.test(path)) {
|
|
2271
|
+
const re = buildWildcardRegExp(path);
|
|
2272
|
+
if (method === METHOD_NAME_ALL) {
|
|
2273
|
+
Object.keys(middleware).forEach((m) => {
|
|
2274
|
+
middleware[m][path] ||= findMiddleware(middleware[m], path) || findMiddleware(middleware[METHOD_NAME_ALL], path) || [];
|
|
2275
|
+
});
|
|
2276
|
+
} else {
|
|
2277
|
+
middleware[method][path] ||= findMiddleware(middleware[method], path) || findMiddleware(middleware[METHOD_NAME_ALL], path) || [];
|
|
2278
|
+
}
|
|
2279
|
+
Object.keys(middleware).forEach((m) => {
|
|
2280
|
+
if (method === METHOD_NAME_ALL || method === m) {
|
|
2281
|
+
Object.keys(middleware[m]).forEach((p) => {
|
|
2282
|
+
re.test(p) && middleware[m][p].push([handler, paramCount]);
|
|
2283
|
+
});
|
|
2284
|
+
}
|
|
2285
|
+
});
|
|
2286
|
+
Object.keys(routes).forEach((m) => {
|
|
2287
|
+
if (method === METHOD_NAME_ALL || method === m) {
|
|
2288
|
+
Object.keys(routes[m]).forEach((p) => re.test(p) && routes[m][p].push([handler, paramCount]));
|
|
2289
|
+
}
|
|
2290
|
+
});
|
|
2291
|
+
return;
|
|
2292
|
+
}
|
|
2293
|
+
const paths = checkOptionalParameter(path) || [path];
|
|
2294
|
+
for (let i = 0, len = paths.length;i < len; i++) {
|
|
2295
|
+
const path2 = paths[i];
|
|
2296
|
+
Object.keys(routes).forEach((m) => {
|
|
2297
|
+
if (method === METHOD_NAME_ALL || method === m) {
|
|
2298
|
+
routes[m][path2] ||= [
|
|
2299
|
+
...findMiddleware(middleware[m], path2) || findMiddleware(middleware[METHOD_NAME_ALL], path2) || []
|
|
2300
|
+
];
|
|
2301
|
+
routes[m][path2].push([handler, paramCount - len + i + 1]);
|
|
2302
|
+
}
|
|
2303
|
+
});
|
|
2304
|
+
}
|
|
2305
|
+
}
|
|
2306
|
+
match = match;
|
|
2307
|
+
buildAllMatchers() {
|
|
2308
|
+
const matchers = /* @__PURE__ */ Object.create(null);
|
|
2309
|
+
Object.keys(this.#routes).concat(Object.keys(this.#middleware)).forEach((method) => {
|
|
2310
|
+
matchers[method] ||= this.#buildMatcher(method);
|
|
2311
|
+
});
|
|
2312
|
+
this.#middleware = this.#routes = undefined;
|
|
2313
|
+
clearWildcardRegExpCache();
|
|
2314
|
+
return matchers;
|
|
2315
|
+
}
|
|
2316
|
+
#buildMatcher(method) {
|
|
2317
|
+
const routes = [];
|
|
2318
|
+
let hasOwnRoute = method === METHOD_NAME_ALL;
|
|
2319
|
+
[this.#middleware, this.#routes].forEach((r) => {
|
|
2320
|
+
const ownRoute = r[method] ? Object.keys(r[method]).map((path) => [path, r[method][path]]) : [];
|
|
2321
|
+
if (ownRoute.length !== 0) {
|
|
2322
|
+
hasOwnRoute ||= true;
|
|
2323
|
+
routes.push(...ownRoute);
|
|
2324
|
+
} else if (method !== METHOD_NAME_ALL) {
|
|
2325
|
+
routes.push(...Object.keys(r[METHOD_NAME_ALL]).map((path) => [path, r[METHOD_NAME_ALL][path]]));
|
|
2326
|
+
}
|
|
2327
|
+
});
|
|
2328
|
+
if (!hasOwnRoute) {
|
|
2329
|
+
return null;
|
|
2330
|
+
} else {
|
|
2331
|
+
return buildMatcherFromPreprocessedRoutes(routes);
|
|
2332
|
+
}
|
|
2333
|
+
}
|
|
2334
|
+
};
|
|
2335
|
+
|
|
2336
|
+
// ../../node_modules/.bun/hono@4.12.10/node_modules/hono/dist/router/reg-exp-router/prepared-router.js
|
|
2337
|
+
var PreparedRegExpRouter = class {
|
|
2338
|
+
name = "PreparedRegExpRouter";
|
|
2339
|
+
#matchers;
|
|
2340
|
+
#relocateMap;
|
|
2341
|
+
constructor(matchers, relocateMap) {
|
|
2342
|
+
this.#matchers = matchers;
|
|
2343
|
+
this.#relocateMap = relocateMap;
|
|
2344
|
+
}
|
|
2345
|
+
#addWildcard(method, handlerData) {
|
|
2346
|
+
const matcher = this.#matchers[method];
|
|
2347
|
+
matcher[1].forEach((list) => list && list.push(handlerData));
|
|
2348
|
+
Object.values(matcher[2]).forEach((list) => list[0].push(handlerData));
|
|
2349
|
+
}
|
|
2350
|
+
#addPath(method, path, handler, indexes, map) {
|
|
2351
|
+
const matcher = this.#matchers[method];
|
|
2352
|
+
if (!map) {
|
|
2353
|
+
matcher[2][path][0].push([handler, {}]);
|
|
2354
|
+
} else {
|
|
2355
|
+
indexes.forEach((index) => {
|
|
2356
|
+
if (typeof index === "number") {
|
|
2357
|
+
matcher[1][index].push([handler, map]);
|
|
2358
|
+
} else {
|
|
2359
|
+
matcher[2][index || path][0].push([handler, map]);
|
|
2360
|
+
}
|
|
2361
|
+
});
|
|
2362
|
+
}
|
|
2363
|
+
}
|
|
2364
|
+
add(method, path, handler) {
|
|
2365
|
+
if (!this.#matchers[method]) {
|
|
2366
|
+
const all = this.#matchers[METHOD_NAME_ALL];
|
|
2367
|
+
const staticMap = {};
|
|
2368
|
+
for (const key in all[2]) {
|
|
2369
|
+
staticMap[key] = [all[2][key][0].slice(), emptyParam];
|
|
2370
|
+
}
|
|
2371
|
+
this.#matchers[method] = [
|
|
2372
|
+
all[0],
|
|
2373
|
+
all[1].map((list) => Array.isArray(list) ? list.slice() : 0),
|
|
2374
|
+
staticMap
|
|
2375
|
+
];
|
|
2376
|
+
}
|
|
2377
|
+
if (path === "/*" || path === "*") {
|
|
2378
|
+
const handlerData = [handler, {}];
|
|
2379
|
+
if (method === METHOD_NAME_ALL) {
|
|
2380
|
+
for (const m in this.#matchers) {
|
|
2381
|
+
this.#addWildcard(m, handlerData);
|
|
2382
|
+
}
|
|
2383
|
+
} else {
|
|
2384
|
+
this.#addWildcard(method, handlerData);
|
|
2385
|
+
}
|
|
2386
|
+
return;
|
|
2387
|
+
}
|
|
2388
|
+
const data = this.#relocateMap[path];
|
|
2389
|
+
if (!data) {
|
|
2390
|
+
throw new Error(`Path ${path} is not registered`);
|
|
2391
|
+
}
|
|
2392
|
+
for (const [indexes, map] of data) {
|
|
2393
|
+
if (method === METHOD_NAME_ALL) {
|
|
2394
|
+
for (const m in this.#matchers) {
|
|
2395
|
+
this.#addPath(m, path, handler, indexes, map);
|
|
2396
|
+
}
|
|
2397
|
+
} else {
|
|
2398
|
+
this.#addPath(method, path, handler, indexes, map);
|
|
2399
|
+
}
|
|
2400
|
+
}
|
|
2401
|
+
}
|
|
2402
|
+
buildAllMatchers() {
|
|
2403
|
+
return this.#matchers;
|
|
2404
|
+
}
|
|
2405
|
+
match = match;
|
|
2406
|
+
};
|
|
2407
|
+
|
|
2408
|
+
// ../../node_modules/.bun/hono@4.12.10/node_modules/hono/dist/router/smart-router/router.js
|
|
2409
|
+
var SmartRouter = class {
|
|
2410
|
+
name = "SmartRouter";
|
|
2411
|
+
#routers = [];
|
|
2412
|
+
#routes = [];
|
|
2413
|
+
constructor(init) {
|
|
2414
|
+
this.#routers = init.routers;
|
|
2415
|
+
}
|
|
2416
|
+
add(method, path, handler) {
|
|
2417
|
+
if (!this.#routes) {
|
|
2418
|
+
throw new Error(MESSAGE_MATCHER_IS_ALREADY_BUILT);
|
|
2419
|
+
}
|
|
2420
|
+
this.#routes.push([method, path, handler]);
|
|
2421
|
+
}
|
|
2422
|
+
match(method, path) {
|
|
2423
|
+
if (!this.#routes) {
|
|
2424
|
+
throw new Error("Fatal error");
|
|
2425
|
+
}
|
|
2426
|
+
const routers = this.#routers;
|
|
2427
|
+
const routes = this.#routes;
|
|
2428
|
+
const len = routers.length;
|
|
2429
|
+
let i = 0;
|
|
2430
|
+
let res;
|
|
2431
|
+
for (;i < len; i++) {
|
|
2432
|
+
const router = routers[i];
|
|
2433
|
+
try {
|
|
2434
|
+
for (let i2 = 0, len2 = routes.length;i2 < len2; i2++) {
|
|
2435
|
+
router.add(...routes[i2]);
|
|
2436
|
+
}
|
|
2437
|
+
res = router.match(method, path);
|
|
2438
|
+
} catch (e) {
|
|
2439
|
+
if (e instanceof UnsupportedPathError) {
|
|
2440
|
+
continue;
|
|
2441
|
+
}
|
|
2442
|
+
throw e;
|
|
2443
|
+
}
|
|
2444
|
+
this.match = router.match.bind(router);
|
|
2445
|
+
this.#routers = [router];
|
|
2446
|
+
this.#routes = undefined;
|
|
2447
|
+
break;
|
|
2448
|
+
}
|
|
2449
|
+
if (i === len) {
|
|
2450
|
+
throw new Error("Fatal error");
|
|
2451
|
+
}
|
|
2452
|
+
this.name = `SmartRouter + ${this.activeRouter.name}`;
|
|
2453
|
+
return res;
|
|
2454
|
+
}
|
|
2455
|
+
get activeRouter() {
|
|
2456
|
+
if (this.#routes || this.#routers.length !== 1) {
|
|
2457
|
+
throw new Error("No active router has been determined yet.");
|
|
2458
|
+
}
|
|
2459
|
+
return this.#routers[0];
|
|
2460
|
+
}
|
|
2461
|
+
};
|
|
2462
|
+
|
|
2463
|
+
// ../../node_modules/.bun/hono@4.12.10/node_modules/hono/dist/router/trie-router/node.js
|
|
2464
|
+
var emptyParams = /* @__PURE__ */ Object.create(null);
|
|
2465
|
+
var hasChildren = (children) => {
|
|
2466
|
+
for (const _ in children) {
|
|
2467
|
+
return true;
|
|
2468
|
+
}
|
|
2469
|
+
return false;
|
|
2470
|
+
};
|
|
2471
|
+
var Node2 = class _Node2 {
|
|
2472
|
+
#methods;
|
|
2473
|
+
#children;
|
|
2474
|
+
#patterns;
|
|
2475
|
+
#order = 0;
|
|
2476
|
+
#params = emptyParams;
|
|
2477
|
+
constructor(method, handler, children) {
|
|
2478
|
+
this.#children = children || /* @__PURE__ */ Object.create(null);
|
|
2479
|
+
this.#methods = [];
|
|
2480
|
+
if (method && handler) {
|
|
2481
|
+
const m = /* @__PURE__ */ Object.create(null);
|
|
2482
|
+
m[method] = { handler, possibleKeys: [], score: 0 };
|
|
2483
|
+
this.#methods = [m];
|
|
2484
|
+
}
|
|
2485
|
+
this.#patterns = [];
|
|
2486
|
+
}
|
|
2487
|
+
insert(method, path, handler) {
|
|
2488
|
+
this.#order = ++this.#order;
|
|
2489
|
+
let curNode = this;
|
|
2490
|
+
const parts = splitRoutingPath(path);
|
|
2491
|
+
const possibleKeys = [];
|
|
2492
|
+
for (let i = 0, len = parts.length;i < len; i++) {
|
|
2493
|
+
const p = parts[i];
|
|
2494
|
+
const nextP = parts[i + 1];
|
|
2495
|
+
const pattern = getPattern(p, nextP);
|
|
2496
|
+
const key = Array.isArray(pattern) ? pattern[0] : p;
|
|
2497
|
+
if (key in curNode.#children) {
|
|
2498
|
+
curNode = curNode.#children[key];
|
|
2499
|
+
if (pattern) {
|
|
2500
|
+
possibleKeys.push(pattern[1]);
|
|
2501
|
+
}
|
|
2502
|
+
continue;
|
|
2503
|
+
}
|
|
2504
|
+
curNode.#children[key] = new _Node2;
|
|
2505
|
+
if (pattern) {
|
|
2506
|
+
curNode.#patterns.push(pattern);
|
|
2507
|
+
possibleKeys.push(pattern[1]);
|
|
2508
|
+
}
|
|
2509
|
+
curNode = curNode.#children[key];
|
|
2510
|
+
}
|
|
2511
|
+
curNode.#methods.push({
|
|
2512
|
+
[method]: {
|
|
2513
|
+
handler,
|
|
2514
|
+
possibleKeys: possibleKeys.filter((v, i, a) => a.indexOf(v) === i),
|
|
2515
|
+
score: this.#order
|
|
2516
|
+
}
|
|
2517
|
+
});
|
|
2518
|
+
return curNode;
|
|
2519
|
+
}
|
|
2520
|
+
#pushHandlerSets(handlerSets, node, method, nodeParams, params) {
|
|
2521
|
+
for (let i = 0, len = node.#methods.length;i < len; i++) {
|
|
2522
|
+
const m = node.#methods[i];
|
|
2523
|
+
const handlerSet = m[method] || m[METHOD_NAME_ALL];
|
|
2524
|
+
const processedSet = {};
|
|
2525
|
+
if (handlerSet !== undefined) {
|
|
2526
|
+
handlerSet.params = /* @__PURE__ */ Object.create(null);
|
|
2527
|
+
handlerSets.push(handlerSet);
|
|
2528
|
+
if (nodeParams !== emptyParams || params && params !== emptyParams) {
|
|
2529
|
+
for (let i2 = 0, len2 = handlerSet.possibleKeys.length;i2 < len2; i2++) {
|
|
2530
|
+
const key = handlerSet.possibleKeys[i2];
|
|
2531
|
+
const processed = processedSet[handlerSet.score];
|
|
2532
|
+
handlerSet.params[key] = params?.[key] && !processed ? params[key] : nodeParams[key] ?? params?.[key];
|
|
2533
|
+
processedSet[handlerSet.score] = true;
|
|
2534
|
+
}
|
|
2535
|
+
}
|
|
2536
|
+
}
|
|
2537
|
+
}
|
|
2538
|
+
}
|
|
2539
|
+
search(method, path) {
|
|
2540
|
+
const handlerSets = [];
|
|
2541
|
+
this.#params = emptyParams;
|
|
2542
|
+
const curNode = this;
|
|
2543
|
+
let curNodes = [curNode];
|
|
2544
|
+
const parts = splitPath(path);
|
|
2545
|
+
const curNodesQueue = [];
|
|
2546
|
+
const len = parts.length;
|
|
2547
|
+
let partOffsets = null;
|
|
2548
|
+
for (let i = 0;i < len; i++) {
|
|
2549
|
+
const part = parts[i];
|
|
2550
|
+
const isLast = i === len - 1;
|
|
2551
|
+
const tempNodes = [];
|
|
2552
|
+
for (let j = 0, len2 = curNodes.length;j < len2; j++) {
|
|
2553
|
+
const node = curNodes[j];
|
|
2554
|
+
const nextNode = node.#children[part];
|
|
2555
|
+
if (nextNode) {
|
|
2556
|
+
nextNode.#params = node.#params;
|
|
2557
|
+
if (isLast) {
|
|
2558
|
+
if (nextNode.#children["*"]) {
|
|
2559
|
+
this.#pushHandlerSets(handlerSets, nextNode.#children["*"], method, node.#params);
|
|
2560
|
+
}
|
|
2561
|
+
this.#pushHandlerSets(handlerSets, nextNode, method, node.#params);
|
|
2562
|
+
} else {
|
|
2563
|
+
tempNodes.push(nextNode);
|
|
2564
|
+
}
|
|
2565
|
+
}
|
|
2566
|
+
for (let k = 0, len3 = node.#patterns.length;k < len3; k++) {
|
|
2567
|
+
const pattern = node.#patterns[k];
|
|
2568
|
+
const params = node.#params === emptyParams ? {} : { ...node.#params };
|
|
2569
|
+
if (pattern === "*") {
|
|
2570
|
+
const astNode = node.#children["*"];
|
|
2571
|
+
if (astNode) {
|
|
2572
|
+
this.#pushHandlerSets(handlerSets, astNode, method, node.#params);
|
|
2573
|
+
astNode.#params = params;
|
|
2574
|
+
tempNodes.push(astNode);
|
|
2575
|
+
}
|
|
2576
|
+
continue;
|
|
2577
|
+
}
|
|
2578
|
+
const [key, name, matcher] = pattern;
|
|
2579
|
+
if (!part && !(matcher instanceof RegExp)) {
|
|
2580
|
+
continue;
|
|
2581
|
+
}
|
|
2582
|
+
const child = node.#children[key];
|
|
2583
|
+
if (matcher instanceof RegExp) {
|
|
2584
|
+
if (partOffsets === null) {
|
|
2585
|
+
partOffsets = new Array(len);
|
|
2586
|
+
let offset = path[0] === "/" ? 1 : 0;
|
|
2587
|
+
for (let p = 0;p < len; p++) {
|
|
2588
|
+
partOffsets[p] = offset;
|
|
2589
|
+
offset += parts[p].length + 1;
|
|
2590
|
+
}
|
|
2591
|
+
}
|
|
2592
|
+
const restPathString = path.substring(partOffsets[i]);
|
|
2593
|
+
const m = matcher.exec(restPathString);
|
|
2594
|
+
if (m) {
|
|
2595
|
+
params[name] = m[0];
|
|
2596
|
+
this.#pushHandlerSets(handlerSets, child, method, node.#params, params);
|
|
2597
|
+
if (hasChildren(child.#children)) {
|
|
2598
|
+
child.#params = params;
|
|
2599
|
+
const componentCount = m[0].match(/\//)?.length ?? 0;
|
|
2600
|
+
const targetCurNodes = curNodesQueue[componentCount] ||= [];
|
|
2601
|
+
targetCurNodes.push(child);
|
|
2602
|
+
}
|
|
2603
|
+
continue;
|
|
2604
|
+
}
|
|
2605
|
+
}
|
|
2606
|
+
if (matcher === true || matcher.test(part)) {
|
|
2607
|
+
params[name] = part;
|
|
2608
|
+
if (isLast) {
|
|
2609
|
+
this.#pushHandlerSets(handlerSets, child, method, params, node.#params);
|
|
2610
|
+
if (child.#children["*"]) {
|
|
2611
|
+
this.#pushHandlerSets(handlerSets, child.#children["*"], method, params, node.#params);
|
|
2612
|
+
}
|
|
2613
|
+
} else {
|
|
2614
|
+
child.#params = params;
|
|
2615
|
+
tempNodes.push(child);
|
|
2616
|
+
}
|
|
2617
|
+
}
|
|
2618
|
+
}
|
|
2619
|
+
}
|
|
2620
|
+
const shifted = curNodesQueue.shift();
|
|
2621
|
+
curNodes = shifted ? tempNodes.concat(shifted) : tempNodes;
|
|
2622
|
+
}
|
|
2623
|
+
if (handlerSets.length > 1) {
|
|
2624
|
+
handlerSets.sort((a, b) => {
|
|
2625
|
+
return a.score - b.score;
|
|
2626
|
+
});
|
|
2627
|
+
}
|
|
2628
|
+
return [handlerSets.map(({ handler, params }) => [handler, params])];
|
|
2629
|
+
}
|
|
2630
|
+
};
|
|
2631
|
+
|
|
2632
|
+
// ../../node_modules/.bun/hono@4.12.10/node_modules/hono/dist/router/trie-router/router.js
|
|
2633
|
+
var TrieRouter = class {
|
|
2634
|
+
name = "TrieRouter";
|
|
2635
|
+
#node;
|
|
2636
|
+
constructor() {
|
|
2637
|
+
this.#node = new Node2;
|
|
2638
|
+
}
|
|
2639
|
+
add(method, path, handler) {
|
|
2640
|
+
const results = checkOptionalParameter(path);
|
|
2641
|
+
if (results) {
|
|
2642
|
+
for (let i = 0, len = results.length;i < len; i++) {
|
|
2643
|
+
this.#node.insert(method, results[i], handler);
|
|
2644
|
+
}
|
|
2645
|
+
return;
|
|
2646
|
+
}
|
|
2647
|
+
this.#node.insert(method, path, handler);
|
|
2648
|
+
}
|
|
2649
|
+
match(method, path) {
|
|
2650
|
+
return this.#node.search(method, path);
|
|
2651
|
+
}
|
|
2652
|
+
};
|
|
2653
|
+
|
|
2654
|
+
// ../../node_modules/.bun/hono@4.12.10/node_modules/hono/dist/hono.js
|
|
2655
|
+
var Hono2 = class extends Hono {
|
|
2656
|
+
constructor(options = {}) {
|
|
2657
|
+
super(options);
|
|
2658
|
+
this.router = options.router ?? new SmartRouter({
|
|
2659
|
+
routers: [new RegExpRouter, new TrieRouter]
|
|
2660
|
+
});
|
|
2661
|
+
}
|
|
2662
|
+
};
|
|
2663
|
+
|
|
2664
|
+
// ../../node_modules/.bun/hono@4.12.10/node_modules/hono/dist/middleware/cors/index.js
|
|
2665
|
+
var cors = (options) => {
|
|
2666
|
+
const defaults = {
|
|
2667
|
+
origin: "*",
|
|
2668
|
+
allowMethods: ["GET", "HEAD", "PUT", "POST", "DELETE", "PATCH"],
|
|
2669
|
+
allowHeaders: [],
|
|
2670
|
+
exposeHeaders: []
|
|
2671
|
+
};
|
|
2672
|
+
const opts = {
|
|
2673
|
+
...defaults,
|
|
2674
|
+
...options
|
|
2675
|
+
};
|
|
2676
|
+
const findAllowOrigin = ((optsOrigin) => {
|
|
2677
|
+
if (typeof optsOrigin === "string") {
|
|
2678
|
+
if (optsOrigin === "*") {
|
|
2679
|
+
if (opts.credentials) {
|
|
2680
|
+
return (origin) => origin || null;
|
|
2681
|
+
}
|
|
2682
|
+
return () => optsOrigin;
|
|
2683
|
+
} else {
|
|
2684
|
+
return (origin) => optsOrigin === origin ? origin : null;
|
|
2685
|
+
}
|
|
2686
|
+
} else if (typeof optsOrigin === "function") {
|
|
2687
|
+
return optsOrigin;
|
|
2688
|
+
} else {
|
|
2689
|
+
return (origin) => optsOrigin.includes(origin) ? origin : null;
|
|
2690
|
+
}
|
|
2691
|
+
})(opts.origin);
|
|
2692
|
+
const findAllowMethods = ((optsAllowMethods) => {
|
|
2693
|
+
if (typeof optsAllowMethods === "function") {
|
|
2694
|
+
return optsAllowMethods;
|
|
2695
|
+
} else if (Array.isArray(optsAllowMethods)) {
|
|
2696
|
+
return () => optsAllowMethods;
|
|
2697
|
+
} else {
|
|
2698
|
+
return () => [];
|
|
2699
|
+
}
|
|
2700
|
+
})(opts.allowMethods);
|
|
2701
|
+
return async function cors2(c, next) {
|
|
2702
|
+
function set(key, value) {
|
|
2703
|
+
c.res.headers.set(key, value);
|
|
2704
|
+
}
|
|
2705
|
+
const allowOrigin = await findAllowOrigin(c.req.header("origin") || "", c);
|
|
2706
|
+
if (allowOrigin) {
|
|
2707
|
+
set("Access-Control-Allow-Origin", allowOrigin);
|
|
2708
|
+
}
|
|
2709
|
+
if (opts.credentials) {
|
|
2710
|
+
set("Access-Control-Allow-Credentials", "true");
|
|
2711
|
+
}
|
|
2712
|
+
if (opts.exposeHeaders?.length) {
|
|
2713
|
+
set("Access-Control-Expose-Headers", opts.exposeHeaders.join(","));
|
|
2714
|
+
}
|
|
2715
|
+
if (c.req.method === "OPTIONS") {
|
|
2716
|
+
if (opts.origin !== "*" || opts.credentials) {
|
|
2717
|
+
set("Vary", "Origin");
|
|
2718
|
+
}
|
|
2719
|
+
if (opts.maxAge != null) {
|
|
2720
|
+
set("Access-Control-Max-Age", opts.maxAge.toString());
|
|
2721
|
+
}
|
|
2722
|
+
const allowMethods = await findAllowMethods(c.req.header("origin") || "", c);
|
|
2723
|
+
if (allowMethods.length) {
|
|
2724
|
+
set("Access-Control-Allow-Methods", allowMethods.join(","));
|
|
2725
|
+
}
|
|
2726
|
+
let headers = opts.allowHeaders;
|
|
2727
|
+
if (!headers?.length) {
|
|
2728
|
+
const requestHeaders = c.req.header("Access-Control-Request-Headers");
|
|
2729
|
+
if (requestHeaders) {
|
|
2730
|
+
headers = requestHeaders.split(/\s*,\s*/);
|
|
2731
|
+
}
|
|
2732
|
+
}
|
|
2733
|
+
if (headers?.length) {
|
|
2734
|
+
set("Access-Control-Allow-Headers", headers.join(","));
|
|
2735
|
+
c.res.headers.append("Vary", "Access-Control-Request-Headers");
|
|
2736
|
+
}
|
|
2737
|
+
c.res.headers.delete("Content-Length");
|
|
2738
|
+
c.res.headers.delete("Content-Type");
|
|
2739
|
+
return new Response(null, {
|
|
2740
|
+
headers: c.res.headers,
|
|
2741
|
+
status: 204,
|
|
2742
|
+
statusText: "No Content"
|
|
2743
|
+
});
|
|
2744
|
+
}
|
|
2745
|
+
await next();
|
|
2746
|
+
if (opts.origin !== "*" || opts.credentials) {
|
|
2747
|
+
c.header("Vary", "Origin", { append: true });
|
|
2748
|
+
}
|
|
2749
|
+
};
|
|
2750
|
+
};
|
|
2751
|
+
|
|
2752
|
+
// ../ops-harbor-control-plane/src/lib/db.ts
|
|
2753
|
+
import { mkdirSync } from "fs";
|
|
2754
|
+
import { dirname as dirname2 } from "path";
|
|
2755
|
+
function toIso(date = new Date) {
|
|
2756
|
+
return date.toISOString();
|
|
2757
|
+
}
|
|
2758
|
+
function ensureColumn(db, table, column, definition) {
|
|
2759
|
+
const columns = db.prepare(`PRAGMA table_info(${table})`).all().map((entry) => entry.name);
|
|
2760
|
+
if (!columns.includes(column)) {
|
|
2761
|
+
db.exec(`ALTER TABLE ${table} ADD COLUMN ${column} ${definition}`);
|
|
2762
|
+
}
|
|
2763
|
+
}
|
|
2764
|
+
function assembleWorkItem(row, checks, reviews) {
|
|
2765
|
+
return {
|
|
2766
|
+
id: row.id,
|
|
2767
|
+
provider: row.provider,
|
|
2768
|
+
kind: row.kind,
|
|
2769
|
+
repository: row.repository,
|
|
2770
|
+
number: row.number,
|
|
2771
|
+
title: row.title,
|
|
2772
|
+
url: row.url,
|
|
2773
|
+
state: row.state,
|
|
2774
|
+
author: row.author,
|
|
2775
|
+
isDraft: row.is_draft === 1,
|
|
2776
|
+
headBranch: row.head_branch,
|
|
2777
|
+
headSha: row.head_sha,
|
|
2778
|
+
baseBranch: row.base_branch,
|
|
2779
|
+
mergeable: row.mergeable,
|
|
2780
|
+
mergeStateStatus: row.merge_state_status,
|
|
2781
|
+
reviewDecision: row.review_decision,
|
|
2782
|
+
statusSummary: parseJson(row.status_summary_json, {
|
|
2783
|
+
overall: "unknown",
|
|
2784
|
+
checkState: "unknown",
|
|
2785
|
+
reviewState: "none",
|
|
2786
|
+
mergeState: "unknown"
|
|
2787
|
+
}),
|
|
2788
|
+
alerts: parseJson(row.alerts_json, []),
|
|
2789
|
+
checks,
|
|
2790
|
+
reviews,
|
|
2791
|
+
lastActivityAt: row.last_activity_at,
|
|
2792
|
+
updatedAt: row.updated_at,
|
|
2793
|
+
...row.provider_payload_json ? { providerPayload: parseJson(row.provider_payload_json, {}) } : {}
|
|
2794
|
+
};
|
|
2795
|
+
}
|
|
2796
|
+
function fromRow(row, db) {
|
|
2797
|
+
const checks = db.prepare(`SELECT work_item_id, name, status, conclusion, url, started_at, completed_at
|
|
2798
|
+
FROM work_item_checks
|
|
2799
|
+
WHERE work_item_id = ?
|
|
2800
|
+
ORDER BY rowid ASC`).all(row.id).map((check) => ({
|
|
2801
|
+
name: check.name,
|
|
2802
|
+
status: check.status,
|
|
2803
|
+
conclusion: check.conclusion,
|
|
2804
|
+
url: check.url,
|
|
2805
|
+
startedAt: check.started_at,
|
|
2806
|
+
completedAt: check.completed_at
|
|
2807
|
+
}));
|
|
2808
|
+
const reviews = db.prepare(`SELECT work_item_id, id, state, author, submitted_at, body_snippet, url
|
|
2809
|
+
FROM work_item_reviews
|
|
2810
|
+
WHERE work_item_id = ?
|
|
2811
|
+
ORDER BY submitted_at DESC`).all(row.id).map((review) => {
|
|
2812
|
+
const itemReview = {
|
|
2813
|
+
id: review.id,
|
|
2814
|
+
state: review.state,
|
|
2815
|
+
author: review.author,
|
|
2816
|
+
submittedAt: review.submitted_at
|
|
2817
|
+
};
|
|
2818
|
+
if (review.body_snippet) {
|
|
2819
|
+
itemReview.bodySnippet = review.body_snippet;
|
|
2820
|
+
}
|
|
2821
|
+
if (review.url) {
|
|
2822
|
+
itemReview.url = review.url;
|
|
2823
|
+
}
|
|
2824
|
+
return itemReview;
|
|
2825
|
+
});
|
|
2826
|
+
return assembleWorkItem(row, checks, reviews);
|
|
2827
|
+
}
|
|
2828
|
+
var CHUNK_SIZE = 500;
|
|
2829
|
+
function buildWorkItemsFromRows(db, rows) {
|
|
2830
|
+
if (rows.length === 0)
|
|
2831
|
+
return [];
|
|
2832
|
+
const ids = rows.map((r) => r.id);
|
|
2833
|
+
const allCheckRows = [];
|
|
2834
|
+
const allReviewRows = [];
|
|
2835
|
+
for (let i = 0;i < ids.length; i += CHUNK_SIZE) {
|
|
2836
|
+
const chunk = ids.slice(i, i + CHUNK_SIZE);
|
|
2837
|
+
const placeholders = chunk.map(() => "?").join(", ");
|
|
2838
|
+
allCheckRows.push(...db.prepare(`SELECT work_item_id, name, status, conclusion, url, started_at, completed_at
|
|
2839
|
+
FROM work_item_checks
|
|
2840
|
+
WHERE work_item_id IN (${placeholders})
|
|
2841
|
+
ORDER BY rowid ASC`).all(...chunk));
|
|
2842
|
+
allReviewRows.push(...db.prepare(`SELECT work_item_id, id, state, author, submitted_at, body_snippet, url
|
|
2843
|
+
FROM work_item_reviews
|
|
2844
|
+
WHERE work_item_id IN (${placeholders})
|
|
2845
|
+
ORDER BY submitted_at DESC`).all(...chunk));
|
|
2846
|
+
}
|
|
2847
|
+
const checksByItemId = new Map;
|
|
2848
|
+
for (const check of allCheckRows) {
|
|
2849
|
+
const list = checksByItemId.get(check.work_item_id) ?? [];
|
|
2850
|
+
list.push({
|
|
2851
|
+
name: check.name,
|
|
2852
|
+
status: check.status,
|
|
2853
|
+
conclusion: check.conclusion,
|
|
2854
|
+
url: check.url,
|
|
2855
|
+
startedAt: check.started_at,
|
|
2856
|
+
completedAt: check.completed_at
|
|
2857
|
+
});
|
|
2858
|
+
checksByItemId.set(check.work_item_id, list);
|
|
2859
|
+
}
|
|
2860
|
+
const reviewsByItemId = new Map;
|
|
2861
|
+
for (const review of allReviewRows) {
|
|
2862
|
+
const list = reviewsByItemId.get(review.work_item_id) ?? [];
|
|
2863
|
+
const itemReview = {
|
|
2864
|
+
id: review.id,
|
|
2865
|
+
state: review.state,
|
|
2866
|
+
author: review.author,
|
|
2867
|
+
submittedAt: review.submitted_at
|
|
2868
|
+
};
|
|
2869
|
+
if (review.body_snippet) {
|
|
2870
|
+
itemReview.bodySnippet = review.body_snippet;
|
|
2871
|
+
}
|
|
2872
|
+
if (review.url) {
|
|
2873
|
+
itemReview.url = review.url;
|
|
2874
|
+
}
|
|
2875
|
+
list.push(itemReview);
|
|
2876
|
+
reviewsByItemId.set(review.work_item_id, list);
|
|
2877
|
+
}
|
|
2878
|
+
return rows.map((row) => assembleWorkItem(row, checksByItemId.get(row.id) ?? [], reviewsByItemId.get(row.id) ?? []));
|
|
2879
|
+
}
|
|
2880
|
+
function activityFromRow(row) {
|
|
2881
|
+
return {
|
|
2882
|
+
id: row.id,
|
|
2883
|
+
repository: row.repository,
|
|
2884
|
+
type: row.type,
|
|
2885
|
+
title: row.title,
|
|
2886
|
+
message: row.message,
|
|
2887
|
+
createdAt: row.created_at,
|
|
2888
|
+
...row.work_item_id ? { workItemId: row.work_item_id } : {},
|
|
2889
|
+
...row.url ? { url: row.url } : {},
|
|
2890
|
+
...row.payload_json ? { payload: parseJson(row.payload_json, {}) } : {}
|
|
2891
|
+
};
|
|
2892
|
+
}
|
|
2893
|
+
function jobFromRow(row) {
|
|
2894
|
+
return {
|
|
2895
|
+
id: row.id,
|
|
2896
|
+
workItemId: row.work_item_id,
|
|
2897
|
+
repository: row.repository,
|
|
2898
|
+
triggerType: row.trigger_type,
|
|
2899
|
+
status: row.status,
|
|
2900
|
+
prompt: row.prompt,
|
|
2901
|
+
leaseOwner: row.lease_owner,
|
|
2902
|
+
leaseExpiresAt: row.lease_expires_at,
|
|
2903
|
+
createdAt: row.created_at,
|
|
2904
|
+
updatedAt: row.updated_at,
|
|
2905
|
+
resultSummary: row.result_summary
|
|
2906
|
+
};
|
|
2907
|
+
}
|
|
2908
|
+
function openControlPlaneDb(dbPath) {
|
|
2909
|
+
mkdirSync(dirname2(dbPath), { recursive: true });
|
|
2910
|
+
const db = openSqliteDatabase(dbPath);
|
|
2911
|
+
ensureSchema(db);
|
|
2912
|
+
return db;
|
|
2913
|
+
}
|
|
2914
|
+
function ensureSchema(db) {
|
|
2915
|
+
db.exec(`
|
|
2916
|
+
CREATE TABLE IF NOT EXISTS providers (
|
|
2917
|
+
provider TEXT PRIMARY KEY,
|
|
2918
|
+
settings_json TEXT,
|
|
2919
|
+
created_at TEXT NOT NULL
|
|
2920
|
+
);
|
|
2921
|
+
|
|
2922
|
+
CREATE TABLE IF NOT EXISTS installations (
|
|
2923
|
+
installation_id INTEGER PRIMARY KEY,
|
|
2924
|
+
provider TEXT NOT NULL,
|
|
2925
|
+
account_login TEXT,
|
|
2926
|
+
account_type TEXT,
|
|
2927
|
+
created_at TEXT NOT NULL,
|
|
2928
|
+
updated_at TEXT NOT NULL
|
|
2929
|
+
);
|
|
2930
|
+
|
|
2931
|
+
CREATE TABLE IF NOT EXISTS repositories (
|
|
2932
|
+
repository TEXT PRIMARY KEY,
|
|
2933
|
+
installation_id INTEGER,
|
|
2934
|
+
url TEXT,
|
|
2935
|
+
default_branch TEXT,
|
|
2936
|
+
updated_at TEXT NOT NULL
|
|
2937
|
+
);
|
|
2938
|
+
|
|
2939
|
+
CREATE TABLE IF NOT EXISTS work_items (
|
|
2940
|
+
id TEXT PRIMARY KEY,
|
|
2941
|
+
provider TEXT NOT NULL,
|
|
2942
|
+
kind TEXT NOT NULL,
|
|
2943
|
+
repository TEXT NOT NULL,
|
|
2944
|
+
number INTEGER NOT NULL,
|
|
2945
|
+
title TEXT NOT NULL,
|
|
2946
|
+
url TEXT NOT NULL,
|
|
2947
|
+
state TEXT NOT NULL,
|
|
2948
|
+
author TEXT NOT NULL,
|
|
2949
|
+
is_draft INTEGER NOT NULL,
|
|
2950
|
+
head_branch TEXT NOT NULL,
|
|
2951
|
+
head_sha TEXT NOT NULL,
|
|
2952
|
+
base_branch TEXT NOT NULL,
|
|
2953
|
+
mergeable TEXT NOT NULL,
|
|
2954
|
+
merge_state_status TEXT NOT NULL,
|
|
2955
|
+
review_decision TEXT,
|
|
2956
|
+
status_summary_json TEXT NOT NULL,
|
|
2957
|
+
alerts_json TEXT NOT NULL,
|
|
2958
|
+
last_activity_at TEXT NOT NULL,
|
|
2959
|
+
updated_at TEXT NOT NULL,
|
|
2960
|
+
provider_payload_json TEXT
|
|
2961
|
+
);
|
|
2962
|
+
CREATE UNIQUE INDEX IF NOT EXISTS idx_work_items_repo_number
|
|
2963
|
+
ON work_items(repository, number);
|
|
2964
|
+
|
|
2965
|
+
CREATE TABLE IF NOT EXISTS work_item_checks (
|
|
2966
|
+
work_item_id TEXT NOT NULL,
|
|
2967
|
+
name TEXT NOT NULL,
|
|
2968
|
+
status TEXT NOT NULL,
|
|
2969
|
+
conclusion TEXT,
|
|
2970
|
+
url TEXT,
|
|
2971
|
+
started_at TEXT,
|
|
2972
|
+
completed_at TEXT
|
|
2973
|
+
);
|
|
2974
|
+
CREATE INDEX IF NOT EXISTS idx_work_item_checks_work_item
|
|
2975
|
+
ON work_item_checks(work_item_id);
|
|
2976
|
+
|
|
2977
|
+
CREATE TABLE IF NOT EXISTS work_item_reviews (
|
|
2978
|
+
work_item_id TEXT NOT NULL,
|
|
2979
|
+
id TEXT NOT NULL,
|
|
2980
|
+
state TEXT NOT NULL,
|
|
2981
|
+
author TEXT NOT NULL,
|
|
2982
|
+
submitted_at TEXT NOT NULL,
|
|
2983
|
+
body_snippet TEXT,
|
|
2984
|
+
url TEXT
|
|
2985
|
+
);
|
|
2986
|
+
CREATE INDEX IF NOT EXISTS idx_work_item_reviews_work_item
|
|
2987
|
+
ON work_item_reviews(work_item_id);
|
|
2988
|
+
|
|
2989
|
+
CREATE TABLE IF NOT EXISTS work_item_events (
|
|
2990
|
+
id TEXT PRIMARY KEY,
|
|
2991
|
+
work_item_id TEXT,
|
|
2992
|
+
repository TEXT NOT NULL,
|
|
2993
|
+
type TEXT NOT NULL,
|
|
2994
|
+
title TEXT NOT NULL,
|
|
2995
|
+
message TEXT NOT NULL,
|
|
2996
|
+
created_at TEXT NOT NULL,
|
|
2997
|
+
url TEXT,
|
|
2998
|
+
payload_json TEXT
|
|
2999
|
+
);
|
|
3000
|
+
CREATE INDEX IF NOT EXISTS idx_work_item_events_lookup
|
|
3001
|
+
ON work_item_events(work_item_id, created_at DESC);
|
|
3002
|
+
|
|
3003
|
+
CREATE TABLE IF NOT EXISTS automation_jobs (
|
|
3004
|
+
id TEXT PRIMARY KEY,
|
|
3005
|
+
work_item_id TEXT NOT NULL,
|
|
3006
|
+
repository TEXT NOT NULL,
|
|
3007
|
+
trigger_type TEXT NOT NULL,
|
|
3008
|
+
status TEXT NOT NULL,
|
|
3009
|
+
prompt TEXT NOT NULL,
|
|
3010
|
+
lease_owner TEXT,
|
|
3011
|
+
lease_expires_at TEXT,
|
|
3012
|
+
created_at TEXT NOT NULL,
|
|
3013
|
+
updated_at TEXT NOT NULL,
|
|
3014
|
+
result_summary TEXT
|
|
3015
|
+
);
|
|
3016
|
+
CREATE INDEX IF NOT EXISTS idx_automation_jobs_status
|
|
3017
|
+
ON automation_jobs(status, created_at);
|
|
3018
|
+
|
|
3019
|
+
CREATE TABLE IF NOT EXISTS webhook_deliveries (
|
|
3020
|
+
delivery_id TEXT PRIMARY KEY,
|
|
3021
|
+
event_name TEXT NOT NULL,
|
|
3022
|
+
received_at TEXT NOT NULL,
|
|
3023
|
+
payload_json TEXT NOT NULL,
|
|
3024
|
+
processed_at TEXT
|
|
3025
|
+
);
|
|
3026
|
+
|
|
3027
|
+
CREATE TABLE IF NOT EXISTS rate_limit_snapshots (
|
|
3028
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
3029
|
+
provider TEXT NOT NULL,
|
|
3030
|
+
scope TEXT NOT NULL,
|
|
3031
|
+
remaining INTEGER,
|
|
3032
|
+
reset_at TEXT,
|
|
3033
|
+
recorded_at TEXT NOT NULL
|
|
3034
|
+
);
|
|
3035
|
+
`);
|
|
3036
|
+
const now = toIso();
|
|
3037
|
+
db.prepare(`INSERT OR IGNORE INTO providers (provider, settings_json, created_at) VALUES (?, ?, ?)`).run("github", "{}", now);
|
|
3038
|
+
ensureColumn(db, "webhook_deliveries", "processed_at", "TEXT");
|
|
3039
|
+
}
|
|
3040
|
+
function isWebhookDeliveryProcessed(db, deliveryId) {
|
|
3041
|
+
const row = db.prepare(`SELECT processed_at FROM webhook_deliveries WHERE delivery_id = ?`).get(deliveryId);
|
|
3042
|
+
return row?.processed_at != null;
|
|
3043
|
+
}
|
|
3044
|
+
function recordWebhookDeliveryReceived(db, deliveryId, eventName, payload) {
|
|
3045
|
+
db.prepare(`INSERT INTO webhook_deliveries (delivery_id, event_name, received_at, payload_json, processed_at)
|
|
3046
|
+
VALUES (?, ?, ?, ?, NULL)
|
|
3047
|
+
ON CONFLICT(delivery_id) DO UPDATE SET
|
|
3048
|
+
event_name = excluded.event_name,
|
|
3049
|
+
received_at = excluded.received_at,
|
|
3050
|
+
payload_json = excluded.payload_json
|
|
3051
|
+
WHERE webhook_deliveries.processed_at IS NULL`).run(deliveryId, eventName, toIso(), JSON.stringify(payload));
|
|
3052
|
+
}
|
|
3053
|
+
function markWebhookDeliveryProcessed(db, deliveryId) {
|
|
3054
|
+
db.prepare(`UPDATE webhook_deliveries
|
|
3055
|
+
SET processed_at = ?
|
|
3056
|
+
WHERE delivery_id = ?`).run(toIso(), deliveryId);
|
|
3057
|
+
}
|
|
3058
|
+
function upsertInstallation(db, installationId, accountLogin, accountType) {
|
|
3059
|
+
const now = toIso();
|
|
3060
|
+
db.prepare(`INSERT INTO installations (installation_id, provider, account_login, account_type, created_at, updated_at)
|
|
3061
|
+
VALUES (?, 'github', ?, ?, ?, ?)
|
|
3062
|
+
ON CONFLICT(installation_id) DO UPDATE SET
|
|
3063
|
+
account_login = excluded.account_login,
|
|
3064
|
+
account_type = excluded.account_type,
|
|
3065
|
+
updated_at = excluded.updated_at`).run(installationId, accountLogin ?? null, accountType ?? null, now, now);
|
|
3066
|
+
}
|
|
3067
|
+
function upsertRepository(db, repository, installationId, url, defaultBranch) {
|
|
3068
|
+
db.prepare(`INSERT INTO repositories (repository, installation_id, url, default_branch, updated_at)
|
|
3069
|
+
VALUES (?, ?, ?, ?, ?)
|
|
3070
|
+
ON CONFLICT(repository) DO UPDATE SET
|
|
3071
|
+
installation_id = excluded.installation_id,
|
|
3072
|
+
url = excluded.url,
|
|
3073
|
+
default_branch = excluded.default_branch,
|
|
3074
|
+
updated_at = excluded.updated_at`).run(repository, installationId, url, defaultBranch, toIso());
|
|
3075
|
+
}
|
|
3076
|
+
function upsertWorkItem(db, workItem) {
|
|
3077
|
+
const tx = db.transaction(() => {
|
|
3078
|
+
upsertRepository(db, workItem.repository, Number(workItem.providerPayload?.installationId ?? 0) || null, typeof workItem.providerPayload?.repositoryUrl === "string" ? workItem.providerPayload.repositoryUrl : workItem.url, workItem.baseBranch);
|
|
3079
|
+
db.prepare(`INSERT INTO work_items (
|
|
3080
|
+
id, provider, kind, repository, number, title, url, state, author, is_draft,
|
|
3081
|
+
head_branch, head_sha, base_branch, mergeable, merge_state_status, review_decision,
|
|
3082
|
+
status_summary_json, alerts_json, last_activity_at, updated_at, provider_payload_json
|
|
3083
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
3084
|
+
ON CONFLICT(id) DO UPDATE SET
|
|
3085
|
+
title = excluded.title,
|
|
3086
|
+
url = excluded.url,
|
|
3087
|
+
state = excluded.state,
|
|
3088
|
+
author = excluded.author,
|
|
3089
|
+
is_draft = excluded.is_draft,
|
|
3090
|
+
head_branch = excluded.head_branch,
|
|
3091
|
+
head_sha = excluded.head_sha,
|
|
3092
|
+
base_branch = excluded.base_branch,
|
|
3093
|
+
mergeable = excluded.mergeable,
|
|
3094
|
+
merge_state_status = excluded.merge_state_status,
|
|
3095
|
+
review_decision = excluded.review_decision,
|
|
3096
|
+
status_summary_json = excluded.status_summary_json,
|
|
3097
|
+
alerts_json = excluded.alerts_json,
|
|
3098
|
+
last_activity_at = excluded.last_activity_at,
|
|
3099
|
+
updated_at = excluded.updated_at,
|
|
3100
|
+
provider_payload_json = excluded.provider_payload_json`).run(workItem.id, workItem.provider, workItem.kind, workItem.repository, workItem.number, workItem.title, workItem.url, workItem.state, workItem.author, workItem.isDraft ? 1 : 0, workItem.headBranch, workItem.headSha, workItem.baseBranch, workItem.mergeable, workItem.mergeStateStatus, workItem.reviewDecision, JSON.stringify(workItem.statusSummary), JSON.stringify(workItem.alerts), workItem.lastActivityAt, workItem.updatedAt, JSON.stringify(workItem.providerPayload ?? {}));
|
|
3101
|
+
db.prepare(`DELETE FROM work_item_checks WHERE work_item_id = ?`).run(workItem.id);
|
|
3102
|
+
const insertCheckStmt = db.prepare(`INSERT INTO work_item_checks (
|
|
3103
|
+
work_item_id, name, status, conclusion, url, started_at, completed_at
|
|
3104
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?)`);
|
|
3105
|
+
for (const check of workItem.checks) {
|
|
3106
|
+
insertCheckStmt.run(workItem.id, check.name, check.status, check.conclusion, check.url ?? null, check.startedAt ?? null, check.completedAt ?? null);
|
|
3107
|
+
}
|
|
3108
|
+
db.prepare(`DELETE FROM work_item_reviews WHERE work_item_id = ?`).run(workItem.id);
|
|
3109
|
+
const insertReviewStmt = db.prepare(`INSERT INTO work_item_reviews (
|
|
3110
|
+
work_item_id, id, state, author, submitted_at, body_snippet, url
|
|
3111
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?)`);
|
|
3112
|
+
for (const review of workItem.reviews) {
|
|
3113
|
+
insertReviewStmt.run(workItem.id, review.id, review.state, review.author, review.submittedAt, review.bodySnippet ?? null, review.url ?? null);
|
|
3114
|
+
}
|
|
3115
|
+
});
|
|
3116
|
+
tx();
|
|
3117
|
+
}
|
|
3118
|
+
function insertActivityEvent(db, event) {
|
|
3119
|
+
db.prepare(`INSERT OR REPLACE INTO work_item_events (
|
|
3120
|
+
id, work_item_id, repository, type, title, message, created_at, url, payload_json
|
|
3121
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(event.id, event.workItemId ?? null, event.repository, event.type, event.title, event.message, event.createdAt, event.url ?? null, JSON.stringify(event.payload ?? {}));
|
|
3122
|
+
}
|
|
3123
|
+
function listActivity(db, options = {}) {
|
|
3124
|
+
const { where, params } = buildActivityWhereClause(options);
|
|
3125
|
+
const limit = options.limit ?? 50;
|
|
3126
|
+
const rows = db.prepare(`SELECT * FROM work_item_events ${where} ORDER BY created_at DESC LIMIT ?`).all(...params, limit);
|
|
3127
|
+
return rows.map(activityFromRow);
|
|
3128
|
+
}
|
|
3129
|
+
function getWorkItem(db, id) {
|
|
3130
|
+
const row = db.prepare(`SELECT * FROM work_items WHERE id = ?`).get(id);
|
|
3131
|
+
return row ? fromRow(row, db) : null;
|
|
3132
|
+
}
|
|
3133
|
+
function listWorkItems(db, filter = {}) {
|
|
3134
|
+
const clauses = [];
|
|
3135
|
+
const params = [];
|
|
3136
|
+
if (filter.state) {
|
|
3137
|
+
clauses.push("state = ?");
|
|
3138
|
+
params.push(filter.state);
|
|
3139
|
+
}
|
|
3140
|
+
if (filter.repository) {
|
|
3141
|
+
clauses.push("repository = ?");
|
|
3142
|
+
params.push(filter.repository);
|
|
3143
|
+
}
|
|
3144
|
+
if (filter.updatedSince) {
|
|
3145
|
+
clauses.push("updated_at >= ?");
|
|
3146
|
+
params.push(filter.updatedSince);
|
|
3147
|
+
}
|
|
3148
|
+
const where = clauses.length > 0 ? `WHERE ${clauses.join(" AND ")}` : "";
|
|
3149
|
+
const rows = db.prepare(`SELECT * FROM work_items ${where} ORDER BY updated_at DESC`).all(...params);
|
|
3150
|
+
const items = buildWorkItemsFromRows(db, rows);
|
|
3151
|
+
if (filter.hasBlocker !== undefined) {
|
|
3152
|
+
return filterWorkItems(items, { hasBlocker: filter.hasBlocker });
|
|
3153
|
+
}
|
|
3154
|
+
return items;
|
|
3155
|
+
}
|
|
3156
|
+
function listAlerts(db) {
|
|
3157
|
+
return mapWorkItemsToAlertSummaries(listWorkItems(db, { hasBlocker: true }));
|
|
3158
|
+
}
|
|
3159
|
+
function recordRateLimit(db, scope, snapshot) {
|
|
3160
|
+
db.prepare(`INSERT INTO rate_limit_snapshots (provider, scope, remaining, reset_at, recorded_at)
|
|
3161
|
+
VALUES ('github', ?, ?, ?, ?)`).run(scope, snapshot.remaining ?? null, snapshot.resetAt ?? null, toIso());
|
|
3162
|
+
}
|
|
3163
|
+
function hasRecentJob(db, workItemId, triggerType, createdAfter) {
|
|
3164
|
+
const row = db.prepare(`SELECT status FROM automation_jobs
|
|
3165
|
+
WHERE work_item_id = ? AND trigger_type = ? AND created_at >= ?
|
|
3166
|
+
ORDER BY created_at DESC
|
|
3167
|
+
LIMIT 1`).get(workItemId, triggerType, createdAfter);
|
|
3168
|
+
return Boolean(row);
|
|
3169
|
+
}
|
|
3170
|
+
function enqueueJobsForAlerts(db, workItem) {
|
|
3171
|
+
const queued = [];
|
|
3172
|
+
const threshold = new Date(Date.now() - 4 * 60 * 60 * 1000).toISOString();
|
|
3173
|
+
for (const alert of workItem.alerts) {
|
|
3174
|
+
if (hasRecentJob(db, workItem.id, alert.type, threshold)) {
|
|
3175
|
+
continue;
|
|
3176
|
+
}
|
|
3177
|
+
const now = toIso();
|
|
3178
|
+
const job = {
|
|
3179
|
+
id: `${workItem.id}:${alert.type}:${Date.now().toString(36)}`,
|
|
3180
|
+
workItemId: workItem.id,
|
|
3181
|
+
repository: workItem.repository,
|
|
3182
|
+
triggerType: alert.type,
|
|
3183
|
+
status: "pending",
|
|
3184
|
+
prompt: buildAutomationPrompt(workItem, alert.type),
|
|
3185
|
+
createdAt: now,
|
|
3186
|
+
updatedAt: now
|
|
3187
|
+
};
|
|
3188
|
+
db.prepare(`INSERT INTO automation_jobs (
|
|
3189
|
+
id, work_item_id, repository, trigger_type, status, prompt, created_at, updated_at
|
|
3190
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`).run(job.id, job.workItemId, job.repository, job.triggerType, job.status, job.prompt, job.createdAt, job.updatedAt);
|
|
3191
|
+
queued.push(job);
|
|
3192
|
+
}
|
|
3193
|
+
return queued;
|
|
3194
|
+
}
|
|
3195
|
+
function leaseNextAutomationJob(db, leaseOwner, leaseTtlMs = 5 * 60 * 1000) {
|
|
3196
|
+
const now = new Date;
|
|
3197
|
+
const nowIso = now.toISOString();
|
|
3198
|
+
const leasedAt = new Date(now.getTime() + leaseTtlMs).toISOString();
|
|
3199
|
+
const leasedRow = db.transaction(() => {
|
|
3200
|
+
const jobRow = db.prepare(`SELECT * FROM automation_jobs
|
|
3201
|
+
WHERE status = 'pending'
|
|
3202
|
+
OR (status = 'leased' AND (lease_expires_at IS NULL OR lease_expires_at < ?))
|
|
3203
|
+
ORDER BY created_at ASC
|
|
3204
|
+
LIMIT 1`).get(nowIso);
|
|
3205
|
+
if (!jobRow)
|
|
3206
|
+
return null;
|
|
3207
|
+
const result = db.prepare(`UPDATE automation_jobs
|
|
3208
|
+
SET status = 'leased', lease_owner = ?, lease_expires_at = ?, updated_at = ?
|
|
3209
|
+
WHERE id = ?
|
|
3210
|
+
AND (
|
|
3211
|
+
status = 'pending'
|
|
3212
|
+
OR (status = 'leased' AND (lease_expires_at IS NULL OR lease_expires_at < ?))
|
|
3213
|
+
)`).run(leaseOwner, leasedAt, nowIso, jobRow.id, nowIso);
|
|
3214
|
+
if (result.changes !== 1)
|
|
3215
|
+
return null;
|
|
3216
|
+
return db.prepare(`SELECT * FROM automation_jobs WHERE id = ?`).get(jobRow.id);
|
|
3217
|
+
})();
|
|
3218
|
+
if (!leasedRow)
|
|
3219
|
+
return null;
|
|
3220
|
+
const workItem = getWorkItem(db, leasedRow.work_item_id);
|
|
3221
|
+
if (!workItem) {
|
|
3222
|
+
return null;
|
|
3223
|
+
}
|
|
3224
|
+
return {
|
|
3225
|
+
job: jobFromRow(leasedRow),
|
|
3226
|
+
workItem
|
|
3227
|
+
};
|
|
3228
|
+
}
|
|
3229
|
+
function completeAutomationJob(db, jobId, status, resultSummary) {
|
|
3230
|
+
db.prepare(`UPDATE automation_jobs
|
|
3231
|
+
SET status = ?, result_summary = ?, lease_owner = NULL, lease_expires_at = NULL, updated_at = ?
|
|
3232
|
+
WHERE id = ?`).run(status, resultSummary ?? null, toIso(), jobId);
|
|
3233
|
+
}
|
|
3234
|
+
function listAutomationJobs(db, limit = 100) {
|
|
3235
|
+
const rows = db.prepare(`SELECT * FROM automation_jobs ORDER BY updated_at DESC LIMIT ?`).all(limit);
|
|
3236
|
+
return rows.map(jobFromRow);
|
|
3237
|
+
}
|
|
3238
|
+
|
|
3239
|
+
// ../ops-harbor-control-plane/src/lib/sync.ts
|
|
3240
|
+
import { randomUUID } from "crypto";
|
|
3241
|
+
function createEvent(repository, options) {
|
|
3242
|
+
return {
|
|
3243
|
+
id: randomUUID(),
|
|
3244
|
+
repository,
|
|
3245
|
+
type: options.type,
|
|
3246
|
+
title: options.title,
|
|
3247
|
+
message: options.message,
|
|
3248
|
+
...options.workItemId ? { workItemId: options.workItemId } : {},
|
|
3249
|
+
...options.url ? { url: options.url } : {},
|
|
3250
|
+
...options.payload ? { payload: options.payload } : {},
|
|
3251
|
+
createdAt: options.createdAt ?? new Date().toISOString()
|
|
3252
|
+
};
|
|
3253
|
+
}
|
|
3254
|
+
function hasWorkItemChanged(existing, incoming) {
|
|
3255
|
+
if (!existing)
|
|
3256
|
+
return true;
|
|
3257
|
+
return existing.updatedAt !== incoming.updatedAt || existing.headSha !== incoming.headSha;
|
|
3258
|
+
}
|
|
3259
|
+
async function saveHydratedWorkItem(db, item, event) {
|
|
3260
|
+
const persistedEvent = event ? {
|
|
3261
|
+
...event,
|
|
3262
|
+
workItemId: event.workItemId ?? item.id
|
|
3263
|
+
} : undefined;
|
|
3264
|
+
if (persistedEvent) {
|
|
3265
|
+
insertActivityEvent(db, persistedEvent);
|
|
3266
|
+
}
|
|
3267
|
+
const recentEvents = listActivity(db, { workItemId: item.id, limit: 20 });
|
|
3268
|
+
const snapshot = deriveAlerts(item, recentEvents);
|
|
3269
|
+
const enriched = { ...item, alerts: snapshot.alerts };
|
|
3270
|
+
upsertWorkItem(db, enriched);
|
|
3271
|
+
enqueueJobsForAlerts(db, enriched);
|
|
3272
|
+
}
|
|
3273
|
+
async function syncAuthorWorkItems(db, config, author) {
|
|
3274
|
+
const { installations, rateLimit } = await listInstallations(config);
|
|
3275
|
+
recordRateLimit(db, "app.installations", rateLimit);
|
|
3276
|
+
let synchronized = 0;
|
|
3277
|
+
let changed = 0;
|
|
3278
|
+
for (const installation of installations) {
|
|
3279
|
+
upsertInstallation(db, installation.id, installation.account?.login, installation.account?.type);
|
|
3280
|
+
const { items, rateLimit: searchRateLimit } = await fetchOpenPullRequestsForAuthor(config, installation.id, author);
|
|
3281
|
+
recordRateLimit(db, `installation.${installation.id}.search`, searchRateLimit);
|
|
3282
|
+
for (const item of items) {
|
|
3283
|
+
const existing = getWorkItem(db, item.id);
|
|
3284
|
+
const itemChanged = hasWorkItemChanged(existing, item);
|
|
3285
|
+
const event = itemChanged ? createEvent(item.repository, {
|
|
3286
|
+
workItemId: item.id,
|
|
3287
|
+
type: "synchronized",
|
|
3288
|
+
title: "Work item synchronized",
|
|
3289
|
+
message: `Synchronized ${item.repository}#${item.number}.`,
|
|
3290
|
+
url: item.url
|
|
3291
|
+
}) : undefined;
|
|
3292
|
+
await saveHydratedWorkItem(db, item, event);
|
|
3293
|
+
synchronized++;
|
|
3294
|
+
if (itemChanged)
|
|
3295
|
+
changed++;
|
|
3296
|
+
}
|
|
3297
|
+
}
|
|
3298
|
+
return { author, synchronized, changed, installations: installations.length };
|
|
3299
|
+
}
|
|
3300
|
+
function mapReviewStateToTrigger(state) {
|
|
3301
|
+
if (state === "COMMENTED")
|
|
3302
|
+
return "review_commented";
|
|
3303
|
+
if (state === "CHANGES_REQUESTED")
|
|
3304
|
+
return "review_changes_requested";
|
|
3305
|
+
return null;
|
|
3306
|
+
}
|
|
3307
|
+
async function hydrateAndPersist(db, config, installationId, repository, number2, event) {
|
|
3308
|
+
const { item, rateLimit } = await hydratePullRequest(config, installationId, repository, number2);
|
|
3309
|
+
recordRateLimit(db, `installation.${installationId}.hydrate`, rateLimit);
|
|
3310
|
+
if (!item)
|
|
3311
|
+
return;
|
|
3312
|
+
await saveHydratedWorkItem(db, item, event);
|
|
3313
|
+
}
|
|
3314
|
+
async function handleWebhookEvent(db, config, eventName, payload) {
|
|
3315
|
+
const installationId = payload.installation?.id;
|
|
3316
|
+
const repository = payload.repository?.full_name;
|
|
3317
|
+
if (!installationId || !repository)
|
|
3318
|
+
return;
|
|
3319
|
+
upsertInstallation(db, installationId, payload.installation?.account?.login, payload.installation?.account?.type);
|
|
3320
|
+
if (eventName === "pull_request") {
|
|
3321
|
+
const number2 = payload.number ?? payload.pull_request?.number;
|
|
3322
|
+
if (!number2)
|
|
3323
|
+
return;
|
|
3324
|
+
const event = createEvent(repository, {
|
|
3325
|
+
type: "webhook_received",
|
|
3326
|
+
title: `pull_request:${payload.action ?? "updated"}`,
|
|
3327
|
+
message: `pull_request ${payload.action ?? "updated"} on ${repository}#${number2}`,
|
|
3328
|
+
url: payload.pull_request?.html_url ?? null,
|
|
3329
|
+
payload: {
|
|
3330
|
+
action: payload.action
|
|
3331
|
+
}
|
|
3332
|
+
});
|
|
3333
|
+
await hydrateAndPersist(db, config, installationId, repository, number2, event);
|
|
3334
|
+
return;
|
|
3335
|
+
}
|
|
3336
|
+
if (eventName === "pull_request_review") {
|
|
3337
|
+
const number2 = payload.pull_request?.number ?? payload.number;
|
|
3338
|
+
if (!number2)
|
|
3339
|
+
return;
|
|
3340
|
+
const trigger = mapReviewStateToTrigger(payload.review?.state);
|
|
3341
|
+
const event = trigger ? createEvent(repository, {
|
|
3342
|
+
type: trigger,
|
|
3343
|
+
title: trigger === "review_changes_requested" ? "Changes requested" : "Review comment received",
|
|
3344
|
+
message: payload.review?.body?.slice(0, 240) || `Review ${payload.review?.state ?? "submitted"}`,
|
|
3345
|
+
url: payload.review?.html_url ?? payload.pull_request?.html_url ?? null,
|
|
3346
|
+
payload: {
|
|
3347
|
+
reviewer: payload.review?.user?.login,
|
|
3348
|
+
state: payload.review?.state
|
|
3349
|
+
},
|
|
3350
|
+
...payload.review?.submitted_at ? { createdAt: payload.review.submitted_at } : {}
|
|
3351
|
+
}) : undefined;
|
|
3352
|
+
await hydrateAndPersist(db, config, installationId, repository, number2, event);
|
|
3353
|
+
return;
|
|
3354
|
+
}
|
|
3355
|
+
if (eventName === "check_run") {
|
|
3356
|
+
for (const pullRequest of payload.check_run?.pull_requests ?? []) {
|
|
3357
|
+
if (!pullRequest.number)
|
|
3358
|
+
continue;
|
|
3359
|
+
await hydrateAndPersist(db, config, installationId, repository, pullRequest.number);
|
|
3360
|
+
}
|
|
3361
|
+
return;
|
|
3362
|
+
}
|
|
3363
|
+
if (eventName === "check_suite") {
|
|
3364
|
+
for (const pullRequest of payload.check_suite?.pull_requests ?? []) {
|
|
3365
|
+
if (!pullRequest.number)
|
|
3366
|
+
continue;
|
|
3367
|
+
await hydrateAndPersist(db, config, installationId, repository, pullRequest.number);
|
|
3368
|
+
}
|
|
3369
|
+
}
|
|
3370
|
+
}
|
|
3371
|
+
|
|
3372
|
+
// ../ops-harbor-control-plane/src/server.ts
|
|
3373
|
+
function fallbackRuntimeStatus() {
|
|
3374
|
+
return {
|
|
3375
|
+
tunnelEnabled: false,
|
|
3376
|
+
publicWebhookUrl: null,
|
|
3377
|
+
publicWebhookBaseUrl: null,
|
|
3378
|
+
webhookIngressPort: null
|
|
3379
|
+
};
|
|
3380
|
+
}
|
|
3381
|
+
function redactStoredConfig(config) {
|
|
3382
|
+
const { githubWebhookSecret: _githubWebhookSecret, ...rest } = config;
|
|
3383
|
+
return rest;
|
|
3384
|
+
}
|
|
3385
|
+
function createControlPlaneApp({ config, db, getRuntimeStatus }) {
|
|
3386
|
+
const app = new Hono2;
|
|
3387
|
+
const webhookHandler = createGitHubWebhookHandler({ config, db });
|
|
3388
|
+
app.use("/api/*", cors());
|
|
3389
|
+
app.use("/api/admin/*", async (c, next) => {
|
|
3390
|
+
if (config.internalApiToken && c.req.header("x-ops-harbor-token") !== config.internalApiToken) {
|
|
3391
|
+
return c.json({ error: "unauthorized" }, 401);
|
|
3392
|
+
}
|
|
3393
|
+
await next();
|
|
3394
|
+
});
|
|
3395
|
+
app.use("/api/internal/*", async (c, next) => {
|
|
3396
|
+
if (config.internalApiToken && c.req.header("x-ops-harbor-token") !== config.internalApiToken) {
|
|
3397
|
+
return c.json({ error: "unauthorized" }, 401);
|
|
3398
|
+
}
|
|
3399
|
+
await next();
|
|
3400
|
+
});
|
|
3401
|
+
app.get("/api/health", (c) => c.json({
|
|
3402
|
+
ok: true,
|
|
3403
|
+
provider: "github",
|
|
3404
|
+
githubConfigured: Boolean(config.githubAppId && config.githubPrivateKey),
|
|
3405
|
+
tunnel: getRuntimeStatus?.() ?? fallbackRuntimeStatus()
|
|
3406
|
+
}));
|
|
3407
|
+
app.get("/api/work-items", (c) => c.json(listWorkItems(db, parseWorkItemFilterFromQuery((key) => c.req.query(key)))));
|
|
3408
|
+
app.get("/api/config", (c) => c.json(redactStoredConfig(loadStoredConfig())));
|
|
3409
|
+
app.put("/api/config", async (c) => {
|
|
3410
|
+
const body = await c.req.json().catch(() => null);
|
|
3411
|
+
if (!body) {
|
|
3412
|
+
return c.json({ error: "invalid config payload" }, 400);
|
|
3413
|
+
}
|
|
3414
|
+
try {
|
|
3415
|
+
return c.json(redactStoredConfig(await saveStoredConfig(body)));
|
|
3416
|
+
} catch (error) {
|
|
3417
|
+
return c.json({ error: error instanceof Error ? error.message : String(error) }, 400);
|
|
3418
|
+
}
|
|
3419
|
+
});
|
|
3420
|
+
app.get("/api/work-items/:id", (c) => {
|
|
3421
|
+
const item = getWorkItem(db, c.req.param("id"));
|
|
3422
|
+
if (!item)
|
|
3423
|
+
return c.json({ error: "work item not found" }, 404);
|
|
3424
|
+
return c.json(item);
|
|
3425
|
+
});
|
|
3426
|
+
app.get("/api/activity", (c) => {
|
|
3427
|
+
const workItemId = c.req.query("work_item_id");
|
|
3428
|
+
const repository = c.req.query("repository");
|
|
3429
|
+
return c.json(listActivity(db, {
|
|
3430
|
+
...workItemId ? { workItemId } : {},
|
|
3431
|
+
...repository ? { repository } : {},
|
|
3432
|
+
limit: Number.parseInt(c.req.query("limit") ?? "100", 10)
|
|
3433
|
+
}));
|
|
3434
|
+
});
|
|
3435
|
+
app.get("/api/alerts", (c) => c.json(listAlerts(db)));
|
|
3436
|
+
app.get("/api/internal/jobs", (c) => c.json(listAutomationJobs(db, Number.parseInt(c.req.query("limit") ?? "100", 10))));
|
|
3437
|
+
app.post("/api/admin/sync", async (c) => {
|
|
3438
|
+
const body = await c.req.json().catch(() => ({}));
|
|
3439
|
+
const author = body.author ?? config.defaultAuthor;
|
|
3440
|
+
if (!author)
|
|
3441
|
+
return c.json({ error: "author is required" }, 400);
|
|
3442
|
+
try {
|
|
3443
|
+
const result = await syncAuthorWorkItems(db, config, author);
|
|
3444
|
+
return c.json(result);
|
|
3445
|
+
} catch (error) {
|
|
3446
|
+
return c.json({ error: error instanceof Error ? error.message : String(error) }, 500);
|
|
3447
|
+
}
|
|
3448
|
+
});
|
|
3449
|
+
app.post("/api/webhooks/github", webhookHandler);
|
|
3450
|
+
app.post("/api/internal/jobs/lease", async (c) => {
|
|
3451
|
+
const body = await c.req.json().catch(() => ({}));
|
|
3452
|
+
const workerId = body.worker_id ?? "default-worker";
|
|
3453
|
+
const leased = leaseNextAutomationJob(db, workerId);
|
|
3454
|
+
return c.json(leased ?? { job: null, workItem: null });
|
|
3455
|
+
});
|
|
3456
|
+
app.post("/api/internal/jobs/:id/complete", async (c) => {
|
|
3457
|
+
const body = await c.req.json().catch(() => ({}));
|
|
3458
|
+
const status = body.status ?? "completed";
|
|
3459
|
+
completeAutomationJob(db, c.req.param("id"), status, body.result_summary);
|
|
3460
|
+
return c.json({ ok: true });
|
|
3461
|
+
});
|
|
3462
|
+
return app;
|
|
3463
|
+
}
|
|
3464
|
+
function createGitHubWebhookHandler({ config, db }) {
|
|
3465
|
+
return async (c) => {
|
|
3466
|
+
const rawBody = await c.req.text();
|
|
3467
|
+
if (config.githubWebhookSecret) {
|
|
3468
|
+
const valid = verifyWebhookSignature(config.githubWebhookSecret, rawBody, c.req.header("x-hub-signature-256") ?? null);
|
|
3469
|
+
if (!valid)
|
|
3470
|
+
return c.json({ error: "invalid webhook signature" }, 401);
|
|
3471
|
+
}
|
|
3472
|
+
const deliveryId = c.req.header("x-github-delivery") ?? crypto.randomUUID();
|
|
3473
|
+
const eventName = c.req.header("x-github-event") ?? "unknown";
|
|
3474
|
+
let payload;
|
|
3475
|
+
try {
|
|
3476
|
+
payload = JSON.parse(rawBody);
|
|
3477
|
+
} catch {
|
|
3478
|
+
return c.json({ error: "invalid JSON body" }, 400);
|
|
3479
|
+
}
|
|
3480
|
+
if (isWebhookDeliveryProcessed(db, deliveryId)) {
|
|
3481
|
+
return c.json({ ok: true, deduplicated: true });
|
|
3482
|
+
}
|
|
3483
|
+
recordWebhookDeliveryReceived(db, deliveryId, eventName, payload);
|
|
3484
|
+
try {
|
|
3485
|
+
await handleWebhookEvent(db, config, eventName, payload);
|
|
3486
|
+
markWebhookDeliveryProcessed(db, deliveryId);
|
|
3487
|
+
return c.json({ ok: true });
|
|
3488
|
+
} catch (error) {
|
|
3489
|
+
return c.json({ error: error instanceof Error ? error.message : String(error) }, 500);
|
|
3490
|
+
}
|
|
3491
|
+
};
|
|
3492
|
+
}
|
|
3493
|
+
function createWebhookIngressApp({ config, db, getRuntimeStatus }) {
|
|
3494
|
+
const app = new Hono2;
|
|
3495
|
+
const webhookHandler = createGitHubWebhookHandler({ config, db });
|
|
3496
|
+
app.get("/api/health", (c) => c.json({
|
|
3497
|
+
ok: true,
|
|
3498
|
+
ingress: "github-webhooks",
|
|
3499
|
+
tunnel: getRuntimeStatus?.() ?? fallbackRuntimeStatus()
|
|
3500
|
+
}));
|
|
3501
|
+
app.post("/api/webhooks/github", webhookHandler);
|
|
3502
|
+
return app;
|
|
3503
|
+
}
|
|
3504
|
+
function openControlPlaneApps(config, getRuntimeStatus) {
|
|
3505
|
+
const db = openControlPlaneDb(config.dbPath);
|
|
3506
|
+
return {
|
|
3507
|
+
db,
|
|
3508
|
+
controlPlaneApp: createControlPlaneApp({ config, db, getRuntimeStatus }),
|
|
3509
|
+
webhookIngressApp: createWebhookIngressApp({ config, db, getRuntimeStatus })
|
|
3510
|
+
};
|
|
3511
|
+
}
|
|
3512
|
+
|
|
3513
|
+
// ../ops-harbor-control-plane/src/cli.ts
|
|
3514
|
+
function shouldStartGitHubWebhookTunnel(config) {
|
|
3515
|
+
return Boolean(config.githubAppId && config.githubPrivateKey && !config.githubTunnelDisabled);
|
|
3516
|
+
}
|
|
3517
|
+
async function main() {
|
|
3518
|
+
await ensureStoredConfigSecrets();
|
|
3519
|
+
const config = readConfig();
|
|
3520
|
+
const runtimeStatus = {
|
|
3521
|
+
tunnelEnabled: false,
|
|
3522
|
+
publicWebhookUrl: null,
|
|
3523
|
+
publicWebhookBaseUrl: null,
|
|
3524
|
+
webhookIngressPort: null
|
|
3525
|
+
};
|
|
3526
|
+
const { controlPlaneApp, webhookIngressApp } = openControlPlaneApps(config, () => runtimeStatus);
|
|
3527
|
+
Bun.serve({
|
|
3528
|
+
port: config.port,
|
|
3529
|
+
hostname: "127.0.0.1",
|
|
3530
|
+
fetch: controlPlaneApp.fetch
|
|
3531
|
+
});
|
|
3532
|
+
let tunnelSession = null;
|
|
3533
|
+
if (shouldStartGitHubWebhookTunnel(config)) {
|
|
3534
|
+
const webhookIngressPort = await findAvailablePort(config.port + 1);
|
|
3535
|
+
runtimeStatus.webhookIngressPort = webhookIngressPort;
|
|
3536
|
+
Bun.serve({
|
|
3537
|
+
port: webhookIngressPort,
|
|
3538
|
+
hostname: "127.0.0.1",
|
|
3539
|
+
fetch: webhookIngressApp.fetch
|
|
3540
|
+
});
|
|
3541
|
+
tunnelSession = await openTunnel({
|
|
3542
|
+
port: webhookIngressPort,
|
|
3543
|
+
localHost: "127.0.0.1",
|
|
3544
|
+
...config.githubTunnelHost ? { host: config.githubTunnelHost } : {}
|
|
3545
|
+
});
|
|
3546
|
+
runtimeStatus.publicWebhookBaseUrl = tunnelSession.url;
|
|
3547
|
+
runtimeStatus.publicWebhookUrl = new URL("/api/webhooks/github", tunnelSession.url).toString();
|
|
3548
|
+
await ensureGitHubAppWebhookUrl(config, runtimeStatus.publicWebhookUrl);
|
|
3549
|
+
runtimeStatus.tunnelEnabled = true;
|
|
3550
|
+
}
|
|
3551
|
+
if (tunnelSession) {
|
|
3552
|
+
const shutdown = async () => {
|
|
3553
|
+
await tunnelSession?.close().catch((error) => {
|
|
3554
|
+
console.error("failed to close GitHub webhook tunnel", error);
|
|
3555
|
+
});
|
|
3556
|
+
process.exit(0);
|
|
3557
|
+
};
|
|
3558
|
+
process.once("SIGINT", () => void shutdown());
|
|
3559
|
+
process.once("SIGTERM", () => void shutdown());
|
|
3560
|
+
}
|
|
3561
|
+
console.log(`
|
|
3562
|
+
\u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510
|
|
3563
|
+
\u2502 Ops Harbor Control Plane \u2502
|
|
3564
|
+
\u251C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524
|
|
3565
|
+
\u2502 Local: http://127.0.0.1:${String(config.port).padEnd(5)} \u2502
|
|
3566
|
+
\u2502 Webhook: ${String(runtimeStatus.publicWebhookUrl ?? "disabled").padEnd(29)} \u2502
|
|
3567
|
+
\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518
|
|
3568
|
+
`);
|
|
3569
|
+
}
|
|
3570
|
+
main().catch((error) => {
|
|
3571
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
3572
|
+
process.exit(1);
|
|
3573
|
+
});
|