@storyshelf/core 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +127 -0
- package/dist/adapter/auth.d.mts +50 -0
- package/dist/adapter/auth.mjs +10 -0
- package/dist/adapter/auth.mjs.map +1 -0
- package/dist/adapter/capture-runner.d.mts +70 -0
- package/dist/adapter/capture-runner.mjs +4 -0
- package/dist/adapter/database.d.mts +43 -0
- package/dist/adapter/database.mjs +4 -0
- package/dist/adapter/git-host/helpers.d.mts +12 -0
- package/dist/adapter/git-host/helpers.mjs +29 -0
- package/dist/adapter/git-host/helpers.mjs.map +1 -0
- package/dist/adapter/git-host.d.mts +46 -0
- package/dist/adapter/git-host.mjs +4 -0
- package/dist/adapter/storage.d.mts +22 -0
- package/dist/adapter/storage.mjs +4 -0
- package/dist/adapters/capture-queue.d.mts +49 -0
- package/dist/adapters/metadata.d.mts +24 -0
- package/dist/adapters/webhook-events.mjs +39 -0
- package/dist/adapters/webhook-events.mjs.map +1 -0
- package/dist/capture/adapter.d.mts +29 -0
- package/dist/capture/adapter.mjs +13 -0
- package/dist/capture/adapter.mjs.map +1 -0
- package/dist/capture/orchestrator.d.mts +23 -0
- package/dist/capture/orchestrator.mjs +103 -0
- package/dist/capture/orchestrator.mjs.map +1 -0
- package/dist/capture/pipeline.d.mts +41 -0
- package/dist/capture/pipeline.mjs +119 -0
- package/dist/capture/pipeline.mjs.map +1 -0
- package/dist/capture/queue.d.mts +35 -0
- package/dist/capture/queue.mjs +96 -0
- package/dist/capture/queue.mjs.map +1 -0
- package/dist/capture/status-fanout.mjs +66 -0
- package/dist/capture/status-fanout.mjs.map +1 -0
- package/dist/capture/storybook.d.mts +14 -0
- package/dist/capture/storybook.mjs +40 -0
- package/dist/capture/storybook.mjs.map +1 -0
- package/dist/config.d.mts +68 -0
- package/dist/config.mjs +69 -0
- package/dist/config.mjs.map +1 -0
- package/dist/ddl.d.mts +7 -0
- package/dist/ddl.mjs +155 -0
- package/dist/ddl.mjs.map +1 -0
- package/dist/diff/engine.d.mts +8 -0
- package/dist/diff/engine.mjs +79 -0
- package/dist/diff/engine.mjs.map +1 -0
- package/dist/diff/options.d.mts +40 -0
- package/dist/diff/options.mjs +15 -0
- package/dist/diff/options.mjs.map +1 -0
- package/dist/index.d.mts +48 -0
- package/dist/index.mjs +327 -0
- package/dist/index.mjs.map +1 -0
- package/dist/logger.d.mts +39 -0
- package/dist/logger.mjs +35 -0
- package/dist/logger.mjs.map +1 -0
- package/dist/middleware/csrf.mjs +39 -0
- package/dist/middleware/csrf.mjs.map +1 -0
- package/dist/middleware/rate-limit.mjs +38 -0
- package/dist/middleware/rate-limit.mjs.map +1 -0
- package/dist/models/baseline.d.mts +20 -0
- package/dist/models/baseline.mjs +89 -0
- package/dist/models/baseline.mjs.map +1 -0
- package/dist/models/build.d.mts +35 -0
- package/dist/models/build.mjs +77 -0
- package/dist/models/build.mjs.map +1 -0
- package/dist/models/comment.d.mts +20 -0
- package/dist/models/comment.mjs +42 -0
- package/dist/models/comment.mjs.map +1 -0
- package/dist/models/label.d.mts +43 -0
- package/dist/models/label.mjs +107 -0
- package/dist/models/label.mjs.map +1 -0
- package/dist/models/member.d.mts +27 -0
- package/dist/models/member.mjs +54 -0
- package/dist/models/member.mjs.map +1 -0
- package/dist/models/project.d.mts +42 -0
- package/dist/models/project.mjs +78 -0
- package/dist/models/project.mjs.map +1 -0
- package/dist/models/snapshot.d.mts +46 -0
- package/dist/models/snapshot.mjs +75 -0
- package/dist/models/snapshot.mjs.map +1 -0
- package/dist/models/status-config.mjs +61 -0
- package/dist/models/status-config.mjs.map +1 -0
- package/dist/models/token.d.mts +33 -0
- package/dist/models/token.mjs +60 -0
- package/dist/models/token.mjs.map +1 -0
- package/dist/models/webhook.d.mts +37 -0
- package/dist/models/webhook.mjs +67 -0
- package/dist/models/webhook.mjs.map +1 -0
- package/dist/pages/build-detail.mjs +333 -0
- package/dist/pages/build-detail.mjs.map +1 -0
- package/dist/pages/build-diff.mjs +437 -0
- package/dist/pages/build-diff.mjs.map +1 -0
- package/dist/pages/compute-jobs.mjs +196 -0
- package/dist/pages/compute-jobs.mjs.map +1 -0
- package/dist/pages/login.mjs +77 -0
- package/dist/pages/login.mjs.map +1 -0
- package/dist/pages/project-builds.mjs +228 -0
- package/dist/pages/project-builds.mjs.map +1 -0
- package/dist/pages/project-create.mjs +168 -0
- package/dist/pages/project-create.mjs.map +1 -0
- package/dist/pages/project-settings.mjs +111 -0
- package/dist/pages/project-settings.mjs.map +1 -0
- package/dist/pages/projects.mjs +178 -0
- package/dist/pages/projects.mjs.map +1 -0
- package/dist/pages/root.mjs +90 -0
- package/dist/pages/root.mjs.map +1 -0
- package/dist/pages/settings-general.mjs +191 -0
- package/dist/pages/settings-general.mjs.map +1 -0
- package/dist/pages/settings-labels.mjs +149 -0
- package/dist/pages/settings-labels.mjs.map +1 -0
- package/dist/pages/settings-members.mjs +124 -0
- package/dist/pages/settings-members.mjs.map +1 -0
- package/dist/pages/settings-status.mjs +150 -0
- package/dist/pages/settings-status.mjs.map +1 -0
- package/dist/pages/settings-tokens.mjs +95 -0
- package/dist/pages/settings-tokens.mjs.map +1 -0
- package/dist/pages/settings-webhooks.mjs +149 -0
- package/dist/pages/settings-webhooks.mjs.map +1 -0
- package/dist/retention/purge.d.mts +28 -0
- package/dist/retention/purge.mjs +66 -0
- package/dist/retention/purge.mjs.map +1 -0
- package/dist/routers/admin.mjs +41 -0
- package/dist/routers/admin.mjs.map +1 -0
- package/dist/routers/assets.mjs +24 -0
- package/dist/routers/assets.mjs.map +1 -0
- package/dist/routers/auth.mjs +77 -0
- package/dist/routers/auth.mjs.map +1 -0
- package/dist/routers/builds.handlers.mjs +87 -0
- package/dist/routers/builds.handlers.mjs.map +1 -0
- package/dist/routers/builds.mjs +162 -0
- package/dist/routers/builds.mjs.map +1 -0
- package/dist/routers/comments.mjs +89 -0
- package/dist/routers/comments.mjs.map +1 -0
- package/dist/routers/helpers.mjs +76 -0
- package/dist/routers/helpers.mjs.map +1 -0
- package/dist/routers/htmx.mjs +18 -0
- package/dist/routers/htmx.mjs.map +1 -0
- package/dist/routers/labels.mjs +77 -0
- package/dist/routers/labels.mjs.map +1 -0
- package/dist/routers/media.mjs +54 -0
- package/dist/routers/media.mjs.map +1 -0
- package/dist/routers/members.mjs +101 -0
- package/dist/routers/members.mjs.map +1 -0
- package/dist/routers/projects.mjs +121 -0
- package/dist/routers/projects.mjs.map +1 -0
- package/dist/routers/schemas.mjs +188 -0
- package/dist/routers/schemas.mjs.map +1 -0
- package/dist/routers/settings.mjs +235 -0
- package/dist/routers/settings.mjs.map +1 -0
- package/dist/routers/snapshots.mjs +143 -0
- package/dist/routers/snapshots.mjs.map +1 -0
- package/dist/routers/status-configs.mjs +100 -0
- package/dist/routers/status-configs.mjs.map +1 -0
- package/dist/routers/tokens.mjs +80 -0
- package/dist/routers/tokens.mjs.map +1 -0
- package/dist/routers/ui.mjs +142 -0
- package/dist/routers/ui.mjs.map +1 -0
- package/dist/routers/webhooks.mjs +89 -0
- package/dist/routers/webhooks.mjs.map +1 -0
- package/dist/schema.d.mts +4372 -0
- package/dist/schema.mjs +158 -0
- package/dist/schema.mjs.map +1 -0
- package/dist/store.mjs +18 -0
- package/dist/store.mjs.map +1 -0
- package/dist/types.d.mts +18 -0
- package/dist/types.mjs +49 -0
- package/dist/types.mjs.map +1 -0
- package/dist/ui/components.mjs +127 -0
- package/dist/ui/components.mjs.map +1 -0
- package/dist/ui/document.d.mts +8 -0
- package/dist/ui/document.mjs +338 -0
- package/dist/ui/document.mjs.map +1 -0
- package/dist/ui/styles.mjs +173 -0
- package/dist/ui/styles.mjs.map +1 -0
- package/dist/ui/theme.mjs +42 -0
- package/dist/ui/theme.mjs.map +1 -0
- package/dist/urls.d.mts +19 -0
- package/dist/urls.mjs +23 -0
- package/dist/urls.mjs.map +1 -0
- package/dist/utils/encrypt.mjs +49 -0
- package/dist/utils/encrypt.mjs.map +1 -0
- package/dist/utils/hash.mjs +22 -0
- package/dist/utils/hash.mjs.map +1 -0
- package/dist/utils/paths.d.mts +11 -0
- package/dist/utils/paths.mjs +23 -0
- package/dist/utils/paths.mjs.map +1 -0
- package/dist/utils/ulid.d.mts +8 -0
- package/dist/utils/ulid.mjs +41 -0
- package/dist/utils/ulid.mjs.map +1 -0
- package/package.json +162 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"settings-status.mjs","names":[],"sources":["../../src/pages/settings-status.tsx"],"sourcesContent":["import type { FC } from \"hono/jsx\";\nimport type { GitHostProvider } from \"../adapters/git-host/index.ts\";\nimport type { Project } from \"../schema.ts\";\nimport { Badge, Field, SelectField, TextareaField } from \"../ui/components.tsx\";\n\nexport interface SettingsStatusConfig {\n id: string;\n provider: string;\n config: Record<string, unknown>;\n hasToken: boolean;\n createdAt: string;\n updatedAt: string;\n}\n\nexport interface StatusFormState {\n errors?: Record<string, string>;\n globalError?: string;\n}\n\n/* eslint-disable promise-function-async -- JSX components return HtmlEscapedString | Promise<HtmlEscapedString> */\n\nfunction providerOptions(providers: GitHostProvider[]): { value: string; label: string }[] {\n return providers.map((provider) => ({ value: provider.metadata.kind, label: provider.metadata.name }));\n}\n\nconst StatusConfigRow: FC<{\n config: SettingsStatusConfig;\n provider: GitHostProvider | undefined;\n project: Project;\n isAdmin: boolean;\n}> = ({ config, provider, project, isAdmin }) => {\n const rendered = JSON.stringify(config.config);\n return (\n <tr key={config.id}>\n <td>{provider?.metadata.name ?? config.provider}</td>\n <td style=\"max-width:32ch; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;\" title={rendered}>\n <code>{rendered}</code>\n </td>\n <td>{config.hasToken ? <Badge tone=\"success\">configured</Badge> : <Badge tone=\"danger\">missing</Badge>}</td>\n <td>{new Date(config.createdAt).toLocaleDateString()}</td>\n <td>\n {isAdmin ? (\n <form method=\"post\" action={`/projects/${project.slug}/settings/status/${config.id}/delete`} hx-post={`/projects/${project.slug}/settings/status/${config.id}/delete`} hx-target=\"body\">\n <button class=\"btn btn--ghost\" type=\"submit\">\n Delete\n </button>\n </form>\n ) : null}\n </td>\n </tr>\n );\n};\n\nconst StatusCreateCard: FC<{\n project: Project;\n providers: GitHostProvider[];\n formState?: StatusFormState;\n}> = ({ project, providers, formState }) => {\n return (\n <div class=\"card card--padded\">\n <h3 style=\"margin:0 0 .5rem;\">Add git provider</h3>\n <form method=\"post\" action={`/projects/${project.slug}/settings/status`} hx-post={`/projects/${project.slug}/settings/status`} hx-target=\"body\">\n <SelectField label=\"Provider\" name=\"provider\" options={providerOptions(providers)} hint=\"Integration that posts commit statuses for this project.\" />\n <Field label=\"Token\" name=\"token\" type=\"password\" required placeholder=\"ghp_…\" error={formState?.errors?.[\"token\"]} hint=\"Scoped token for the provider (e.g. a GitHub PAT with repo:status).\" />\n <TextareaField\n label=\"Config\"\n name=\"config\"\n rows={6}\n placeholder='{\"owner\":\"my-org\",\"repo\":\"my-repo\"}'\n error={formState?.errors?.[\"config\"]}\n hint=\"JSON configuration for the provider. See the provider documentation for its fields.\"\n />\n <button class=\"btn btn--primary\" type=\"submit\">\n Add git provider\n </button>\n </form>\n </div>\n );\n};\n\nfunction renderCreateSection(project: Project, providers: GitHostProvider[], formState?: StatusFormState): unknown {\n if (providers.length === 0) {\n return <p class=\"field__hint\">No git providers registered on this server.</p>;\n }\n return <StatusCreateCard project={project} providers={providers} formState={formState} />;\n}\n\n// eslint-disable-next-line max-params -- matches sibling settings render functions\nexport function renderSettingsStatus(\n project: Project,\n statusConfigs: SettingsStatusConfig[],\n providers: GitHostProvider[],\n isAdmin: boolean,\n formState?: StatusFormState,\n): unknown {\n const byProvider = new Map(providers.map((provider) => [provider.metadata.kind, provider]));\n return (\n <div class=\"grid\" style=\"max-width: 880px;\">\n {formState?.globalError ? (\n <div class=\"alert alert--danger\" role=\"alert\">\n {formState.globalError}\n </div>\n ) : null}\n\n <div class=\"card card--padded\">\n <h2 style=\"margin:0 0 .3rem;\">Git status</h2>\n <p class=\"field__hint\">\n Post commit statuses to a git provider so visual tests show up in your PR checks. Status is reported for each configured provider: pending while\n capturing, success on approval, failure on rejection or capture errors.\n </p>\n <div class=\"table-wrap\" style=\"margin-top:.75rem;\">\n <table>\n <thead>\n <tr>\n <th>Provider</th>\n <th>Config</th>\n <th>Token</th>\n <th>Created</th>\n <th></th>\n </tr>\n </thead>\n <tbody>\n {statusConfigs.map((config) => (\n <StatusConfigRow key={config.id} config={config} provider={byProvider.get(config.provider)} project={project} isAdmin={isAdmin} />\n ))}\n </tbody>\n </table>\n </div>\n {statusConfigs.length === 0 ? <p class=\"field__hint\" style=\"margin-top:.5rem;\">No git providers configured for this project.</p> : null}\n </div>\n\n {isAdmin ? renderCreateSection(project, providers, formState) : null}\n </div>\n );\n}"],"mappings":"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"}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import "node:path";
|
|
2
|
+
import "node:url";
|
|
3
|
+
import.meta.url;
|
|
4
|
+
import { jsx, jsxs } from "hono/jsx/jsx-runtime";
|
|
5
|
+
//#region src/pages/settings-tokens.tsx
|
|
6
|
+
function renderSettingsTokens(project, tokens, isAdmin) {
|
|
7
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
8
|
+
class: "grid",
|
|
9
|
+
style: "max-width: 880px;",
|
|
10
|
+
children: [/* @__PURE__ */ jsxs("div", {
|
|
11
|
+
class: "card card--padded",
|
|
12
|
+
children: [
|
|
13
|
+
/* @__PURE__ */ jsx("h2", {
|
|
14
|
+
style: "margin:0 0 .3rem;",
|
|
15
|
+
children: "API tokens"
|
|
16
|
+
}),
|
|
17
|
+
/* @__PURE__ */ jsx("p", {
|
|
18
|
+
class: "field__hint",
|
|
19
|
+
children: "Tokens are used by the CLI to upload builds. They are scoped to this project."
|
|
20
|
+
}),
|
|
21
|
+
/* @__PURE__ */ jsx("div", {
|
|
22
|
+
class: "table-wrap",
|
|
23
|
+
style: "margin-top:.75rem;",
|
|
24
|
+
children: /* @__PURE__ */ jsxs("table", { children: [/* @__PURE__ */ jsx("thead", { children: /* @__PURE__ */ jsxs("tr", { children: [
|
|
25
|
+
/* @__PURE__ */ jsx("th", { children: "Name" }),
|
|
26
|
+
/* @__PURE__ */ jsx("th", { children: "Created" }),
|
|
27
|
+
/* @__PURE__ */ jsx("th", { children: "Last used" }),
|
|
28
|
+
/* @__PURE__ */ jsx("th", {})
|
|
29
|
+
] }) }), /* @__PURE__ */ jsx("tbody", { children: tokens.map((token) => /* @__PURE__ */ jsxs("tr", { children: [
|
|
30
|
+
/* @__PURE__ */ jsx("td", { children: token.name }),
|
|
31
|
+
/* @__PURE__ */ jsx("td", { children: new Date(token.createdAt).toLocaleDateString() }),
|
|
32
|
+
/* @__PURE__ */ jsx("td", { children: token.lastUsedAt ? new Date(token.lastUsedAt).toLocaleDateString() : "—" }),
|
|
33
|
+
/* @__PURE__ */ jsx("td", { children: isAdmin ? /* @__PURE__ */ jsx("form", {
|
|
34
|
+
method: "post",
|
|
35
|
+
action: `/projects/${project.slug}/settings/tokens/${token.id}/delete`,
|
|
36
|
+
"hx-post": `/projects/${project.slug}/settings/tokens/${token.id}/delete`,
|
|
37
|
+
"hx-target": "body",
|
|
38
|
+
children: /* @__PURE__ */ jsx("button", {
|
|
39
|
+
class: "btn btn--ghost",
|
|
40
|
+
type: "submit",
|
|
41
|
+
children: "Revoke"
|
|
42
|
+
})
|
|
43
|
+
}) : null })
|
|
44
|
+
] }, token.id)) })] })
|
|
45
|
+
}),
|
|
46
|
+
tokens.length === 0 ? /* @__PURE__ */ jsx("p", {
|
|
47
|
+
class: "field__hint",
|
|
48
|
+
style: "margin-top:.5rem;",
|
|
49
|
+
children: "No tokens yet. Create one for CI."
|
|
50
|
+
}) : null
|
|
51
|
+
]
|
|
52
|
+
}), isAdmin ? /* @__PURE__ */ jsxs("div", {
|
|
53
|
+
class: "card card--padded",
|
|
54
|
+
children: [
|
|
55
|
+
/* @__PURE__ */ jsx("h3", {
|
|
56
|
+
style: "margin:0 0 .5rem;",
|
|
57
|
+
children: "Create token"
|
|
58
|
+
}),
|
|
59
|
+
/* @__PURE__ */ jsxs("form", {
|
|
60
|
+
method: "post",
|
|
61
|
+
action: `/projects/${project.slug}/settings/tokens`,
|
|
62
|
+
"hx-post": `/projects/${project.slug}/settings/tokens`,
|
|
63
|
+
"hx-target": "body",
|
|
64
|
+
children: [/* @__PURE__ */ jsxs("div", {
|
|
65
|
+
class: "field",
|
|
66
|
+
children: [/* @__PURE__ */ jsx("label", {
|
|
67
|
+
class: "field__label",
|
|
68
|
+
for: "tokenName",
|
|
69
|
+
children: "Name"
|
|
70
|
+
}), /* @__PURE__ */ jsx("input", {
|
|
71
|
+
class: "field__input",
|
|
72
|
+
id: "tokenName",
|
|
73
|
+
name: "tokenName",
|
|
74
|
+
required: true,
|
|
75
|
+
placeholder: "ci"
|
|
76
|
+
})]
|
|
77
|
+
}), /* @__PURE__ */ jsx("button", {
|
|
78
|
+
class: "btn btn--primary",
|
|
79
|
+
type: "submit",
|
|
80
|
+
children: "Create token"
|
|
81
|
+
})]
|
|
82
|
+
}),
|
|
83
|
+
/* @__PURE__ */ jsx("p", {
|
|
84
|
+
class: "field__hint",
|
|
85
|
+
style: "margin-top:.5rem;",
|
|
86
|
+
children: "Token value is shown once after creation. Store it securely."
|
|
87
|
+
})
|
|
88
|
+
]
|
|
89
|
+
}) : null]
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
//#endregion
|
|
93
|
+
export { renderSettingsTokens };
|
|
94
|
+
|
|
95
|
+
//# sourceMappingURL=settings-tokens.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"settings-tokens.mjs","names":[],"sources":["../../src/pages/settings-tokens.tsx"],"sourcesContent":["import type { Project, Token } from \"../schema.ts\";\n\nexport function renderSettingsTokens(project: Project, tokens: Array<Omit<Token, \"hash\">>, isAdmin: boolean): unknown {\n return (\n <div class=\"grid\" style=\"max-width: 880px;\">\n <div class=\"card card--padded\">\n <h2 style=\"margin:0 0 .3rem;\">API tokens</h2>\n <p class=\"field__hint\">Tokens are used by the CLI to upload builds. They are scoped to this project.</p>\n <div class=\"table-wrap\" style=\"margin-top:.75rem;\">\n <table>\n <thead>\n <tr>\n <th>Name</th>\n <th>Created</th>\n <th>Last used</th>\n <th></th>\n </tr>\n </thead>\n <tbody>\n {tokens.map(\n // eslint-disable-next-line promise-function-async -- JSX.Element includes Promise<HtmlEscapedString>\n (token) => (\n <tr key={token.id}>\n <td>{token.name}</td>\n <td>{new Date(token.createdAt).toLocaleDateString()}</td>\n <td>{token.lastUsedAt ? new Date(token.lastUsedAt).toLocaleDateString() : \"—\"}</td>\n <td>\n {isAdmin ? (\n <form method=\"post\" action={`/projects/${project.slug}/settings/tokens/${token.id}/delete`} hx-post={`/projects/${project.slug}/settings/tokens/${token.id}/delete`} hx-target=\"body\">\n <button class=\"btn btn--ghost\" type=\"submit\">\n Revoke\n </button>\n </form>\n ) : null}\n </td>\n </tr>\n ),\n )}\n </tbody>\n </table>\n </div>\n {tokens.length === 0 ? <p class=\"field__hint\" style=\"margin-top:.5rem;\">No tokens yet. Create one for CI.</p> : null}\n </div>\n\n {isAdmin ? (\n <div class=\"card card--padded\">\n <h3 style=\"margin:0 0 .5rem;\">Create token</h3>\n <form method=\"post\" action={`/projects/${project.slug}/settings/tokens`} hx-post={`/projects/${project.slug}/settings/tokens`} hx-target=\"body\">\n <div class=\"field\">\n <label class=\"field__label\" for=\"tokenName\">\n Name\n </label>\n <input class=\"field__input\" id=\"tokenName\" name=\"tokenName\" required placeholder=\"ci\" />\n </div>\n <button class=\"btn btn--primary\" type=\"submit\">\n Create token\n </button>\n </form>\n <p class=\"field__hint\" style=\"margin-top:.5rem;\">Token value is shown once after creation. Store it securely.</p>\n </div>\n ) : null}\n </div>\n );\n}"],"mappings":";;;;;AAEA,SAAgB,qBAAqB,SAAkB,QAAoC,SAA2B;CACpH,OACE,qBAAC,OAAD;EAAK,OAAM;EAAO,OAAM;EAAxB,UAAA,CACE,qBAAC,OAAD;GAAK,OAAM;GAAX,UAAA;IACE,oBAAC,MAAD;KAAI,OAAM;KAAoB,UAAA;IAAc,CAAA;IAC5C,oBAAC,KAAD;KAAG,OAAM;KAAc,UAAA;IAAgF,CAAA;IACvG,oBAAC,OAAD;KAAK,OAAM;KAAa,OAAM;KAC5B,UAAA,qBAAC,SAAD,EAAA,UAAA,CACE,oBAAC,SAAD,EAAA,UACE,qBAAC,MAAD,EAAA,UAAA;MACE,oBAAC,MAAD,EAAA,UAAI,OAAQ,CAAA;MACZ,oBAAC,MAAD,EAAA,UAAI,UAAW,CAAA;MACf,oBAAC,MAAD,EAAA,UAAI,YAAa,CAAA;MACjB,oBAAC,MAAD,CAAQ,CAAA;KACN,EAAA,CAAA,EACC,CAAA,GACP,oBAAC,SAAD,EAAA,UACG,OAAO,KAEL,UACC,qBAAC,MAAD,EAAA,UAAA;MACE,oBAAC,MAAD,EAAA,UAAK,MAAM,KAAS,CAAA;MACpB,oBAAC,MAAD,EAAA,UAAK,IAAI,KAAK,MAAM,SAAS,CAAC,CAAC,mBAAmB,EAAM,CAAA;MACxD,oBAAC,MAAD,EAAA,UAAK,MAAM,aAAa,IAAI,KAAK,MAAM,UAAU,CAAC,CAAC,mBAAmB,IAAI,IAAQ,CAAA;MAClF,oBAAC,MAAD,EAAA,UACG,UACC,oBAAC,QAAD;OAAM,QAAO;OAAO,QAAQ,aAAa,QAAQ,KAAK,mBAAmB,MAAM,GAAG;OAAU,WAAS,aAAa,QAAQ,KAAK,mBAAmB,MAAM,GAAG;OAAU,aAAU;OAC7K,UAAA,oBAAC,UAAD;QAAQ,OAAM;QAAiB,MAAK;QAAS,UAAA;OAErC,CAAA;MACJ,CAAA,IACJ,KACF,CAAA;KACF,EAAA,GAbK,MAAM,EAaX,CAER,EACK,CAAA,CACF,EAAA,CAAA;IACJ,CAAA;IACJ,OAAO,WAAW,IAAI,oBAAC,KAAD;KAAG,OAAM;KAAc,OAAM;KAAoB,UAAA;IAAoC,CAAA,IAAI;GAC7G;EAEJ,CAAA,GAAA,UACC,qBAAC,OAAD;GAAK,OAAM;GAAX,UAAA;IACE,oBAAC,MAAD;KAAI,OAAM;KAAoB,UAAA;IAAgB,CAAA;IAC9C,qBAAC,QAAD;KAAM,QAAO;KAAO,QAAQ,aAAa,QAAQ,KAAK;KAAmB,WAAS,aAAa,QAAQ,KAAK;KAAmB,aAAU;KAAzI,UAAA,CACE,qBAAC,OAAD;MAAK,OAAM;MAAX,UAAA,CACE,oBAAC,SAAD;OAAO,OAAM;OAAe,KAAI;OAAY,UAAA;MAErC,CAAA,GACP,oBAAC,SAAD;OAAO,OAAM;OAAe,IAAG;OAAY,MAAK;OAAY,UAAA;OAAS,aAAY;MAAM,CAAA,CACpF;KACL,CAAA,GAAA,oBAAC,UAAD;MAAQ,OAAM;MAAmB,MAAK;MAAS,UAAA;KAEvC,CAAA,CACJ;;IACN,oBAAC,KAAD;KAAG,OAAM;KAAc,OAAM;KAAoB,UAAA;IAA+D,CAAA;GAC7G;EACH,CAAA,IAAA,IACD;;AAET"}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import "node:path";
|
|
2
|
+
import "node:url";
|
|
3
|
+
import.meta.url;
|
|
4
|
+
import { jsx, jsxs } from "hono/jsx/jsx-runtime";
|
|
5
|
+
//#region src/pages/settings-webhooks.tsx
|
|
6
|
+
function renderSettingsWebhooks(project, webhooks, isAdmin, formState) {
|
|
7
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
8
|
+
class: "grid",
|
|
9
|
+
style: "max-width: 880px;",
|
|
10
|
+
children: [
|
|
11
|
+
formState?.secret ? /* @__PURE__ */ jsxs("div", {
|
|
12
|
+
class: "alert alert--success",
|
|
13
|
+
role: "alert",
|
|
14
|
+
children: [/* @__PURE__ */ jsx("strong", {
|
|
15
|
+
class: "alert__title",
|
|
16
|
+
children: "Webhook created"
|
|
17
|
+
}), /* @__PURE__ */ jsxs("div", {
|
|
18
|
+
class: "alert__body",
|
|
19
|
+
children: ["Copy the signing secret now — shown once: ", /* @__PURE__ */ jsx("code", { children: formState.secret })]
|
|
20
|
+
})]
|
|
21
|
+
}) : null,
|
|
22
|
+
formState?.globalError ? /* @__PURE__ */ jsx("div", {
|
|
23
|
+
class: "alert alert--danger",
|
|
24
|
+
role: "alert",
|
|
25
|
+
children: formState.globalError
|
|
26
|
+
}) : null,
|
|
27
|
+
/* @__PURE__ */ jsxs("div", {
|
|
28
|
+
class: "card card--padded",
|
|
29
|
+
children: [
|
|
30
|
+
/* @__PURE__ */ jsx("h2", {
|
|
31
|
+
style: "margin:0 0 .3rem;",
|
|
32
|
+
children: "Webhooks"
|
|
33
|
+
}),
|
|
34
|
+
/* @__PURE__ */ jsx("p", {
|
|
35
|
+
class: "field__hint",
|
|
36
|
+
children: "Notify external services when builds are created, updated, approved or rejected. Payloads are POSTed as JSON and signed with the webhook secret."
|
|
37
|
+
}),
|
|
38
|
+
/* @__PURE__ */ jsx("div", {
|
|
39
|
+
class: "table-wrap",
|
|
40
|
+
style: "margin-top:.75rem;",
|
|
41
|
+
children: /* @__PURE__ */ jsxs("table", { children: [/* @__PURE__ */ jsx("thead", { children: /* @__PURE__ */ jsxs("tr", { children: [
|
|
42
|
+
/* @__PURE__ */ jsx("th", { children: "URL" }),
|
|
43
|
+
/* @__PURE__ */ jsx("th", { children: "Events" }),
|
|
44
|
+
/* @__PURE__ */ jsx("th", {})
|
|
45
|
+
] }) }), /* @__PURE__ */ jsx("tbody", { children: webhooks.map((webhook) => /* @__PURE__ */ jsxs("tr", { children: [
|
|
46
|
+
/* @__PURE__ */ jsx("td", {
|
|
47
|
+
style: "max-width:36ch; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;",
|
|
48
|
+
title: webhook.url,
|
|
49
|
+
children: webhook.url
|
|
50
|
+
}),
|
|
51
|
+
/* @__PURE__ */ jsx("td", { children: webhook.events.length === 0 ? /* @__PURE__ */ jsx("span", {
|
|
52
|
+
class: "field__hint",
|
|
53
|
+
children: "all events"
|
|
54
|
+
}) : /* @__PURE__ */ jsx("div", {
|
|
55
|
+
style: "display:flex; gap:.25rem; flex-wrap:wrap;",
|
|
56
|
+
children: webhook.events.map((event) => /* @__PURE__ */ jsx("span", {
|
|
57
|
+
class: "badge badge--neutral",
|
|
58
|
+
children: event
|
|
59
|
+
}, event))
|
|
60
|
+
}) }),
|
|
61
|
+
/* @__PURE__ */ jsx("td", { children: isAdmin ? /* @__PURE__ */ jsx("form", {
|
|
62
|
+
method: "post",
|
|
63
|
+
action: `/projects/${project.slug}/settings/webhooks/${webhook.id}/delete`,
|
|
64
|
+
"hx-post": `/projects/${project.slug}/settings/webhooks/${webhook.id}/delete`,
|
|
65
|
+
"hx-target": "body",
|
|
66
|
+
children: /* @__PURE__ */ jsx("button", {
|
|
67
|
+
class: "btn btn--ghost",
|
|
68
|
+
type: "submit",
|
|
69
|
+
children: "Delete"
|
|
70
|
+
})
|
|
71
|
+
}) : null })
|
|
72
|
+
] }, webhook.id)) })] })
|
|
73
|
+
}),
|
|
74
|
+
webhooks.length === 0 ? /* @__PURE__ */ jsx("p", {
|
|
75
|
+
class: "field__hint",
|
|
76
|
+
style: "margin-top:.5rem;",
|
|
77
|
+
children: "No webhooks configured."
|
|
78
|
+
}) : null
|
|
79
|
+
]
|
|
80
|
+
}),
|
|
81
|
+
isAdmin ? /* @__PURE__ */ jsxs("div", {
|
|
82
|
+
class: "card card--padded",
|
|
83
|
+
children: [/* @__PURE__ */ jsx("h3", {
|
|
84
|
+
style: "margin:0 0 .5rem;",
|
|
85
|
+
children: "Create webhook"
|
|
86
|
+
}), /* @__PURE__ */ jsxs("form", {
|
|
87
|
+
method: "post",
|
|
88
|
+
action: `/projects/${project.slug}/settings/webhooks`,
|
|
89
|
+
"hx-post": `/projects/${project.slug}/settings/webhooks`,
|
|
90
|
+
"hx-target": "body",
|
|
91
|
+
children: [
|
|
92
|
+
/* @__PURE__ */ jsxs("div", {
|
|
93
|
+
class: "field",
|
|
94
|
+
children: [
|
|
95
|
+
/* @__PURE__ */ jsx("label", {
|
|
96
|
+
class: "field__label",
|
|
97
|
+
for: "url",
|
|
98
|
+
children: "URL"
|
|
99
|
+
}),
|
|
100
|
+
/* @__PURE__ */ jsx("input", {
|
|
101
|
+
class: `field__input ${formState?.errors?.["url"] ? "field__input--error" : ""}`,
|
|
102
|
+
id: "url",
|
|
103
|
+
name: "url",
|
|
104
|
+
type: "url",
|
|
105
|
+
required: true,
|
|
106
|
+
placeholder: "https://example.com/hooks/storyshelf"
|
|
107
|
+
}),
|
|
108
|
+
formState?.errors?.["url"] ? /* @__PURE__ */ jsx("p", {
|
|
109
|
+
class: "field__error",
|
|
110
|
+
role: "alert",
|
|
111
|
+
children: formState.errors["url"]
|
|
112
|
+
}) : null
|
|
113
|
+
]
|
|
114
|
+
}),
|
|
115
|
+
/* @__PURE__ */ jsxs("div", {
|
|
116
|
+
class: "field",
|
|
117
|
+
children: [
|
|
118
|
+
/* @__PURE__ */ jsx("label", {
|
|
119
|
+
class: "field__label",
|
|
120
|
+
for: "events",
|
|
121
|
+
children: "Events"
|
|
122
|
+
}),
|
|
123
|
+
/* @__PURE__ */ jsx("input", {
|
|
124
|
+
class: "field__input",
|
|
125
|
+
id: "events",
|
|
126
|
+
name: "events",
|
|
127
|
+
placeholder: "build.created, build.approved, snapshot.reviewed"
|
|
128
|
+
}),
|
|
129
|
+
/* @__PURE__ */ jsx("p", {
|
|
130
|
+
class: "field__hint",
|
|
131
|
+
children: "Comma-separated. Leave blank to receive all events."
|
|
132
|
+
})
|
|
133
|
+
]
|
|
134
|
+
}),
|
|
135
|
+
/* @__PURE__ */ jsx("button", {
|
|
136
|
+
class: "btn btn--primary",
|
|
137
|
+
type: "submit",
|
|
138
|
+
children: "Add webhook"
|
|
139
|
+
})
|
|
140
|
+
]
|
|
141
|
+
})]
|
|
142
|
+
}) : null
|
|
143
|
+
]
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
//#endregion
|
|
147
|
+
export { renderSettingsWebhooks };
|
|
148
|
+
|
|
149
|
+
//# sourceMappingURL=settings-webhooks.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"settings-webhooks.mjs","names":[],"sources":["../../src/pages/settings-webhooks.tsx"],"sourcesContent":["import type { Project } from \"../schema.ts\";\n\nexport interface SettingsWebhook {\n id: string;\n url: string;\n events: string[];\n}\n\nexport function renderSettingsWebhooks(project: Project, webhooks: SettingsWebhook[], isAdmin: boolean, formState?: { errors?: Record<string, string>; globalError?: string; secret?: string }): unknown {\n return (\n <div class=\"grid\" style=\"max-width: 880px;\">\n {formState?.secret ? (\n <div class=\"alert alert--success\" role=\"alert\">\n <strong class=\"alert__title\">Webhook created</strong>\n <div class=\"alert__body\">\n Copy the signing secret now — shown once: <code>{formState.secret}</code>\n </div>\n </div>\n ) : null}\n {formState?.globalError ? (\n <div class=\"alert alert--danger\" role=\"alert\">\n {formState.globalError}\n </div>\n ) : null}\n\n <div class=\"card card--padded\">\n <h2 style=\"margin:0 0 .3rem;\">Webhooks</h2>\n <p class=\"field__hint\">\n Notify external services when builds are created, updated, approved or rejected. Payloads are POSTed as JSON and signed with the webhook secret.\n </p>\n <div class=\"table-wrap\" style=\"margin-top:.75rem;\">\n <table>\n <thead>\n <tr>\n <th>URL</th>\n <th>Events</th>\n <th></th>\n </tr>\n </thead>\n <tbody>\n {webhooks.map(\n // eslint-disable-next-line promise-function-async -- JSX.Element includes Promise<HtmlEscapedString>\n (webhook) => (\n <tr key={webhook.id}>\n <td style=\"max-width:36ch; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;\" title={webhook.url}>\n {webhook.url}\n </td>\n <td>\n {webhook.events.length === 0 ? (\n <span class=\"field__hint\">all events</span>\n ) : (\n <div style=\"display:flex; gap:.25rem; flex-wrap:wrap;\">\n {webhook.events.map(\n // eslint-disable-next-line promise-function-async -- JSX.Element includes Promise<HtmlEscapedString>\n (event) => (\n <span key={event} class=\"badge badge--neutral\">\n {event}\n </span>\n ),\n )}\n </div>\n )}\n </td>\n <td>\n {isAdmin ? (\n <form method=\"post\" action={`/projects/${project.slug}/settings/webhooks/${webhook.id}/delete`} hx-post={`/projects/${project.slug}/settings/webhooks/${webhook.id}/delete`} hx-target=\"body\">\n <button class=\"btn btn--ghost\" type=\"submit\">\n Delete\n </button>\n </form>\n ) : null}\n </td>\n </tr>\n ),\n )}\n </tbody>\n </table>\n </div>\n {webhooks.length === 0 ? <p class=\"field__hint\" style=\"margin-top:.5rem;\">No webhooks configured.</p> : null}\n </div>\n\n {isAdmin ? (\n <div class=\"card card--padded\">\n <h3 style=\"margin:0 0 .5rem;\">Create webhook</h3>\n <form method=\"post\" action={`/projects/${project.slug}/settings/webhooks`} hx-post={`/projects/${project.slug}/settings/webhooks`} hx-target=\"body\">\n <div class=\"field\">\n <label class=\"field__label\" for=\"url\">\n URL\n </label>\n <input class={`field__input ${formState?.errors?.[\"url\"] ? \"field__input--error\" : \"\"}`} id=\"url\" name=\"url\" type=\"url\" required placeholder=\"https://example.com/hooks/storyshelf\" />\n {formState?.errors?.[\"url\"] ? (\n <p class=\"field__error\" role=\"alert\">\n {formState.errors[\"url\"]}\n </p>\n ) : null}\n </div>\n <div class=\"field\">\n <label class=\"field__label\" for=\"events\">\n Events\n </label>\n <input class=\"field__input\" id=\"events\" name=\"events\" placeholder=\"build.created, build.approved, snapshot.reviewed\" />\n <p class=\"field__hint\">Comma-separated. Leave blank to receive all events.</p>\n </div>\n <button class=\"btn btn--primary\" type=\"submit\">\n Add webhook\n </button>\n </form>\n </div>\n ) : null}\n </div>\n );\n}"],"mappings":"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"}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import __tsdown_shims_path from 'node:path';
|
|
2
|
+
import __tsdown_shims_url from 'node:url';
|
|
3
|
+
import { DatabaseAdapter } from "../adapter/database.mjs";
|
|
4
|
+
import { StorageAdapter } from "../adapter/storage.mjs";
|
|
5
|
+
import { Project } from "../schema.mjs";
|
|
6
|
+
import { Logger } from "pino";
|
|
7
|
+
//#region src/retention/purge.d.ts
|
|
8
|
+
interface PurgeOptions {
|
|
9
|
+
ttlDays: number;
|
|
10
|
+
keepLatestPerBranch: boolean;
|
|
11
|
+
}
|
|
12
|
+
interface PurgeResult {
|
|
13
|
+
removedBuilds: number;
|
|
14
|
+
removedFiles: number;
|
|
15
|
+
}
|
|
16
|
+
declare class Retention {
|
|
17
|
+
private readonly db;
|
|
18
|
+
private readonly storage;
|
|
19
|
+
private readonly logger?;
|
|
20
|
+
constructor(db: DatabaseAdapter, storage: StorageAdapter, logger?: Logger | undefined);
|
|
21
|
+
purge(project: Project, options: PurgeOptions): Promise<PurgeResult>;
|
|
22
|
+
private removeBuild;
|
|
23
|
+
private latestPerBranch;
|
|
24
|
+
private deleteBuildFiles;
|
|
25
|
+
}
|
|
26
|
+
//#endregion
|
|
27
|
+
export { PurgeOptions, PurgeResult, Retention };
|
|
28
|
+
//# sourceMappingURL=purge.d.mts.map
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import "node:path";
|
|
2
|
+
import "node:url";
|
|
3
|
+
import.meta.url;
|
|
4
|
+
import { builds } from "../schema.mjs";
|
|
5
|
+
import { BuildModel } from "../models/build.mjs";
|
|
6
|
+
import { TERMINAL_BUILD_STATUSES } from "../types.mjs";
|
|
7
|
+
import { LabelModel } from "../models/label.mjs";
|
|
8
|
+
import { and, eq, inArray, lt, sql } from "drizzle-orm";
|
|
9
|
+
//#region src/retention/purge.ts
|
|
10
|
+
var Retention = class {
|
|
11
|
+
db;
|
|
12
|
+
storage;
|
|
13
|
+
logger;
|
|
14
|
+
constructor(db, storage, logger) {
|
|
15
|
+
this.db = db;
|
|
16
|
+
this.storage = storage;
|
|
17
|
+
this.logger = logger;
|
|
18
|
+
}
|
|
19
|
+
async purge(project, options) {
|
|
20
|
+
const cutoff = (/* @__PURE__ */ new Date(Date.now() - options.ttlDays * 864e5)).toISOString();
|
|
21
|
+
const candidates = await this.db.list(builds, { where: and(eq(builds.projectId, project.id), inArray(builds.status, [...TERMINAL_BUILD_STATUSES]), lt(builds.updatedAt, cutoff)) });
|
|
22
|
+
const keep = options.keepLatestPerBranch ? await this.latestPerBranch(project.id) : /* @__PURE__ */ new Set();
|
|
23
|
+
const labelModel = new LabelModel(this.db);
|
|
24
|
+
const buildIds = (await Promise.all(candidates.map(async (build) => {
|
|
25
|
+
if (keep.has(build.id) || await labelModel.hasPersistent(project.id, build.id)) return null;
|
|
26
|
+
return build.id;
|
|
27
|
+
}))).filter((id) => id !== null);
|
|
28
|
+
const results = await Promise.all(buildIds.map(async (buildId) => ({
|
|
29
|
+
files: await this.deleteBuildFiles(project.id, buildId),
|
|
30
|
+
removed: await this.removeBuild(buildId)
|
|
31
|
+
})));
|
|
32
|
+
const removedBuilds = results.filter((r) => r.removed).length;
|
|
33
|
+
const removedFiles = results.reduce((sum, r) => sum + r.files, 0);
|
|
34
|
+
this.logger?.info({
|
|
35
|
+
projectId: project.id,
|
|
36
|
+
removedBuilds,
|
|
37
|
+
removedFiles
|
|
38
|
+
}, "build retention purge complete");
|
|
39
|
+
return {
|
|
40
|
+
removedBuilds,
|
|
41
|
+
removedFiles
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
async removeBuild(buildId) {
|
|
45
|
+
await new BuildModel(this.db).remove(buildId);
|
|
46
|
+
return true;
|
|
47
|
+
}
|
|
48
|
+
async latestPerBranch(projectId) {
|
|
49
|
+
const rows = await this.db.all(sql`SELECT id, gitBranch, createdAt FROM builds WHERE projectId = ${projectId} ORDER BY createdAt DESC`);
|
|
50
|
+
const latest = /* @__PURE__ */ new Map();
|
|
51
|
+
for (const row of rows) if (!latest.get(row.gitBranch)) latest.set(row.gitBranch, row.id);
|
|
52
|
+
return new Set(latest.values());
|
|
53
|
+
}
|
|
54
|
+
async deleteBuildFiles(projectId, buildId) {
|
|
55
|
+
const prefix = `${projectId}/builds/${buildId}/`;
|
|
56
|
+
const files = await this.storage.list(prefix);
|
|
57
|
+
await Promise.all(files.map(async (file) => {
|
|
58
|
+
await this.storage.delete(file);
|
|
59
|
+
}));
|
|
60
|
+
return files.length;
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
//#endregion
|
|
64
|
+
export { Retention };
|
|
65
|
+
|
|
66
|
+
//# sourceMappingURL=purge.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"purge.mjs","names":[],"sources":["../../src/retention/purge.ts"],"sourcesContent":["import { and, eq, inArray, lt, sql } from \"drizzle-orm\";\nimport type { Logger } from \"pino\";\n\nimport type { DatabaseAdapter } from \"../adapters/database.ts\";\nimport type { StorageAdapter } from \"../adapters/storage.ts\";\nimport { BuildModel } from \"../models/build.ts\";\nimport { LabelModel } from \"../models/label.ts\";\nimport { builds, type Project } from \"../schema.ts\";\nimport { TERMINAL_BUILD_STATUSES } from \"../types.ts\";\n\nexport interface PurgeOptions {\n ttlDays: number;\n keepLatestPerBranch: boolean;\n}\n\nexport interface PurgeResult {\n removedBuilds: number;\n removedFiles: number;\n}\nexport class Retention {\n constructor(\n private readonly db: DatabaseAdapter,\n private readonly storage: StorageAdapter,\n private readonly logger?: Logger,\n ) {}\n\n\n async purge(project: Project, options: PurgeOptions): Promise<PurgeResult> {\n const cutoff = new Date(Date.now() - options.ttlDays * 86_400_000).toISOString();\n const candidates = await this.db.list(builds, {\n where: and(eq(builds.projectId, project.id), inArray(builds.status, [...TERMINAL_BUILD_STATUSES]), lt(builds.updatedAt, cutoff)),\n });\n\n const keep = options.keepLatestPerBranch ? await this.latestPerBranch(project.id) : new Set<string>();\n const labelModel = new LabelModel(this.db);\n const target = await Promise.all(\n candidates.map(async (build) => {\n if (keep.has(build.id) || (await labelModel.hasPersistent(project.id, build.id))) {\n return null;\n }\n return build.id;\n }),\n );\n\n const buildIds = target.filter((id): id is string => id !== null);\n const results = await Promise.all(\n buildIds.map(async (buildId) => ({\n files: await this.deleteBuildFiles(project.id, buildId),\n removed: await this.removeBuild(buildId),\n })),\n );\n const removedBuilds = results.filter((r) => r.removed).length;\n const removedFiles = results.reduce((sum, r) => sum + r.files, 0);\n this.logger?.info({ projectId: project.id, removedBuilds, removedFiles }, \"build retention purge complete\");\n return { removedBuilds, removedFiles };\n }\n\n private async removeBuild(buildId: string): Promise<boolean> {\n await new BuildModel(this.db).remove(buildId);\n return true;\n }\n\n private async latestPerBranch(projectId: string): Promise<Set<string>> {\n const rows = await this.db.all<{ id: string; gitBranch: string }>(\n sql`SELECT id, gitBranch, createdAt FROM builds WHERE projectId = ${projectId} ORDER BY createdAt DESC`,\n );\n const latest = new Map<string, string>();\n for (const row of rows) {\n const current = latest.get(row.gitBranch);\n if (!current) {\n latest.set(row.gitBranch, row.id);\n }\n }\n return new Set(latest.values());\n }\n\n private async deleteBuildFiles(projectId: string, buildId: string): Promise<number> {\n const prefix = `${projectId}/builds/${buildId}/`;\n const files = await this.storage.list(prefix);\n await Promise.all(\n files.map(async (file) => {\n await this.storage.delete(file);\n }),\n );\n return files.length;\n }\n}\n"],"mappings":";;;;;;;;;AAmBA,IAAa,YAAb,MAAuB;CAEF;CACA;CACA;CAHnB,YACE,IACA,SACA,QACA;EAHiB,KAAA,KAAA;EACA,KAAA,UAAA;EACA,KAAA,SAAA;CAChB;CAGH,MAAM,MAAM,SAAkB,SAA6C;EACzE,MAAM,0BAAS,IAAI,KAAK,KAAK,IAAI,IAAI,QAAQ,UAAU,KAAU,EAAA,CAAE,YAAY;EAC/E,MAAM,aAAa,MAAM,KAAK,GAAG,KAAK,QAAQ,EAC5C,OAAO,IAAI,GAAG,OAAO,WAAW,QAAQ,EAAE,GAAG,QAAQ,OAAO,QAAQ,CAAC,GAAG,uBAAuB,CAAC,GAAG,GAAG,OAAO,WAAW,MAAM,CAAC,EACjI,CAAC;EAED,MAAM,OAAO,QAAQ,sBAAsB,MAAM,KAAK,gBAAgB,QAAQ,EAAE,oBAAI,IAAI,IAAY;EACpG,MAAM,aAAa,IAAI,WAAW,KAAK,EAAE;EAUzC,MAAM,YAAW,MATI,QAAQ,IAC3B,WAAW,IAAI,OAAO,UAAU;GAC9B,IAAI,KAAK,IAAI,MAAM,EAAE,KAAM,MAAM,WAAW,cAAc,QAAQ,IAAI,MAAM,EAAE,GAC5E,OAAO;GAET,OAAO,MAAM;EACf,CAAC,CACH,EAAA,CAEwB,QAAQ,OAAqB,OAAO,IAAI;EAChE,MAAM,UAAU,MAAM,QAAQ,IAC5B,SAAS,IAAI,OAAO,aAAa;GAC/B,OAAO,MAAM,KAAK,iBAAiB,QAAQ,IAAI,OAAO;GACtD,SAAS,MAAM,KAAK,YAAY,OAAO;EACzC,EAAE,CACJ;EACA,MAAM,gBAAgB,QAAQ,QAAQ,MAAM,EAAE,OAAO,CAAC,CAAC;EACvD,MAAM,eAAe,QAAQ,QAAQ,KAAK,MAAM,MAAM,EAAE,OAAO,CAAC;EAChE,KAAK,QAAQ,KAAK;GAAE,WAAW,QAAQ;GAAI;GAAe;EAAa,GAAG,gCAAgC;EAC1G,OAAO;GAAE;GAAe;EAAa;CACvC;CAEA,MAAc,YAAY,SAAmC;EAC3D,MAAM,IAAI,WAAW,KAAK,EAAE,CAAC,CAAC,OAAO,OAAO;EAC5C,OAAO;CACT;CAEA,MAAc,gBAAgB,WAAyC;EACrE,MAAM,OAAO,MAAM,KAAK,GAAG,IACzB,GAAG,iEAAiE,UAAU,yBAChF;EACA,MAAM,yBAAS,IAAI,IAAoB;EACvC,KAAK,MAAM,OAAO,MAEhB,IAAI,CADY,OAAO,IAAI,IAAI,SACpB,GACT,OAAO,IAAI,IAAI,WAAW,IAAI,EAAE;EAGpC,OAAO,IAAI,IAAI,OAAO,OAAO,CAAC;CAChC;CAEA,MAAc,iBAAiB,WAAmB,SAAkC;EAClF,MAAM,SAAS,GAAG,UAAU,UAAU,QAAQ;EAC9C,MAAM,QAAQ,MAAM,KAAK,QAAQ,KAAK,MAAM;EAC5C,MAAM,QAAQ,IACZ,MAAM,IAAI,OAAO,SAAS;GACxB,MAAM,KAAK,QAAQ,OAAO,IAAI;EAChC,CAAC,CACH;EACA,OAAO,MAAM;CACf;AACF"}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import "node:path";
|
|
2
|
+
import "node:url";
|
|
3
|
+
import.meta.url;
|
|
4
|
+
import { ProjectModel } from "../models/project.mjs";
|
|
5
|
+
import { Retention } from "../retention/purge.mjs";
|
|
6
|
+
import { getStore } from "../store.mjs";
|
|
7
|
+
import { requireSiteAdmin } from "./helpers.mjs";
|
|
8
|
+
import { forbidden, purgeInputSchema, purgeSchema } from "./schemas.mjs";
|
|
9
|
+
import { createRoute } from "@hono/zod-openapi";
|
|
10
|
+
//#region src/routers/admin.ts
|
|
11
|
+
const purgeRoute = createRoute({
|
|
12
|
+
method: "post",
|
|
13
|
+
path: "/api/v1/admin/purge",
|
|
14
|
+
request: { body: { content: { "application/json": { schema: purgeInputSchema } } } },
|
|
15
|
+
responses: {
|
|
16
|
+
200: {
|
|
17
|
+
content: { "application/json": { schema: purgeSchema } },
|
|
18
|
+
description: "Retention purge run"
|
|
19
|
+
},
|
|
20
|
+
...forbidden
|
|
21
|
+
}
|
|
22
|
+
});
|
|
23
|
+
function registerAdmin(app) {
|
|
24
|
+
app.openapi(purgeRoute, async (c) => {
|
|
25
|
+
requireSiteAdmin();
|
|
26
|
+
const ttlDays = c.req.valid("json").ttlDays ?? getStore().config.purgeTtlDays ?? 30;
|
|
27
|
+
const projects = await new ProjectModel(getStore().db).list();
|
|
28
|
+
const retention = new Retention(getStore().db, getStore().storage);
|
|
29
|
+
const removedBuilds = (await Promise.all(projects.map(async (project) => {
|
|
30
|
+
return await retention.purge(project, {
|
|
31
|
+
ttlDays,
|
|
32
|
+
keepLatestPerBranch: true
|
|
33
|
+
});
|
|
34
|
+
}))).reduce((sum, result) => sum + result.removedBuilds, 0);
|
|
35
|
+
return c.json({ removedBuilds });
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
//#endregion
|
|
39
|
+
export { registerAdmin };
|
|
40
|
+
|
|
41
|
+
//# sourceMappingURL=admin.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"admin.mjs","names":["forbiddenResponse"],"sources":["../../src/routers/admin.ts"],"sourcesContent":["import { createRoute } from \"@hono/zod-openapi\";\nimport type { ShelfApp } from \"../index.tsx\";\n\nimport { ProjectModel } from \"../models/project.ts\";\nimport { Retention } from \"../retention/purge.ts\";\nimport { getStore } from \"../store.ts\";\nimport { requireSiteAdmin } from \"./helpers.ts\";\nimport { forbidden as forbiddenResponse, purgeInputSchema, purgeSchema } from \"./schemas.ts\";\n\nconst purgeRoute = createRoute({\n method: \"post\",\n path: \"/api/v1/admin/purge\",\n request: { body: { content: { \"application/json\": { schema: purgeInputSchema } } } },\n responses: {\n 200: { content: { \"application/json\": { schema: purgeSchema } }, description: \"Retention purge run\" },\n ...forbiddenResponse,\n },\n});\n\nexport function registerAdmin(app: ShelfApp): void {\n app.openapi(purgeRoute, async (c) => {\n requireSiteAdmin();\n const body = c.req.valid(\"json\");\n const ttlDays = body.ttlDays ?? getStore().config.purgeTtlDays ?? 30;\n const projects = await new ProjectModel(getStore().db).list();\n const retention = new Retention(getStore().db, getStore().storage);\n const results = await Promise.all(\n projects.map(async (project) => {\n return await retention.purge(project, { ttlDays, keepLatestPerBranch: true });\n }),\n );\n const removedBuilds = results.reduce((sum, result) => sum + result.removedBuilds, 0);\n return c.json({ removedBuilds });\n });\n}"],"mappings":";;;;;;;;;;AASA,MAAM,aAAa,YAAY;CAC7B,QAAQ;CACR,MAAM;CACN,SAAS,EAAE,MAAM,EAAE,SAAS,EAAE,oBAAoB,EAAE,QAAQ,iBAAiB,EAAE,EAAE,EAAE;CACnF,WAAW;EACT,KAAK;GAAE,SAAS,EAAE,oBAAoB,EAAE,QAAQ,YAAY,EAAE;GAAG,aAAa;EAAsB;EACpG,GAAGA;CACL;AACF,CAAC;AAED,SAAgB,cAAc,KAAqB;CACjD,IAAI,QAAQ,YAAY,OAAO,MAAM;EACnC,iBAAiB;EAEjB,MAAM,UADO,EAAE,IAAI,MAAM,MACN,CAAC,CAAC,WAAW,SAAS,CAAC,CAAC,OAAO,gBAAgB;EAClE,MAAM,WAAW,MAAM,IAAI,aAAa,SAAS,CAAC,CAAC,EAAE,CAAC,CAAC,KAAK;EAC5D,MAAM,YAAY,IAAI,UAAU,SAAS,CAAC,CAAC,IAAI,SAAS,CAAC,CAAC,OAAO;EAMjE,MAAM,iBAAgB,MALA,QAAQ,IAC5B,SAAS,IAAI,OAAO,YAAY;GAC9B,OAAO,MAAM,UAAU,MAAM,SAAS;IAAE;IAAS,qBAAqB;GAAK,CAAC;EAC9E,CAAC,CACH,EAAA,CAC8B,QAAQ,KAAK,WAAW,MAAM,OAAO,eAAe,CAAC;EACnF,OAAO,EAAE,KAAK,EAAE,cAAc,CAAC;CACjC,CAAC;AACH"}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import "node:path";
|
|
2
|
+
import "node:url";
|
|
3
|
+
import.meta.url;
|
|
4
|
+
import { readFile } from "node:fs/promises";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
//#region src/routers/assets.ts
|
|
7
|
+
let htmxSource = null;
|
|
8
|
+
async function htmxScript() {
|
|
9
|
+
htmxSource ??= await readFile(fileURLToPath(new URL("../assets/htmx.min.js", import.meta.url)), "utf8");
|
|
10
|
+
return htmxSource;
|
|
11
|
+
}
|
|
12
|
+
function registerAssets(app) {
|
|
13
|
+
app.get("/assets/htmx.js", async (c) => {
|
|
14
|
+
const body = await htmxScript();
|
|
15
|
+
return c.body(body, 200, {
|
|
16
|
+
"content-type": "application/javascript",
|
|
17
|
+
"cache-control": "public, max-age=31536000, immutable"
|
|
18
|
+
});
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
//#endregion
|
|
22
|
+
export { registerAssets };
|
|
23
|
+
|
|
24
|
+
//# sourceMappingURL=assets.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"assets.mjs","names":[],"sources":["../../src/routers/assets.ts"],"sourcesContent":["import { readFile } from \"node:fs/promises\";\nimport { fileURLToPath } from \"node:url\";\n\nimport type { ShelfApp } from \"../index.tsx\";\n\nlet htmxSource: string | null = null;\n\nasync function htmxScript(): Promise<string> {\n htmxSource ??= await readFile(fileURLToPath(new URL(\"../assets/htmx.min.js\", import.meta.url)), \"utf8\");\n return htmxSource;\n}\n\nexport function registerAssets(app: ShelfApp): void {\n app.get(\"/assets/htmx.js\", async (c) => {\n const body = await htmxScript();\n return c.body(body, 200, {\n \"content-type\": \"application/javascript\",\n \"cache-control\": \"public, max-age=31536000, immutable\",\n });\n });\n}"],"mappings":";;;;;;AAKA,IAAI,aAA4B;AAEhC,eAAe,aAA8B;CAC3C,eAAe,MAAM,SAAS,cAAc,IAAI,IAAI,yBAAyB,YAAY,GAAG,CAAC,GAAG,MAAM;CACtG,OAAO;AACT;AAEA,SAAgB,eAAe,KAAqB;CAClD,IAAI,IAAI,mBAAmB,OAAO,MAAM;EACtC,MAAM,OAAO,MAAM,WAAW;EAC9B,OAAO,EAAE,KAAK,MAAM,KAAK;GACvB,gBAAgB;GAChB,iBAAiB;EACnB,CAAC;CACH,CAAC;AACH"}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import "node:path";
|
|
2
|
+
import "node:url";
|
|
3
|
+
import.meta.url;
|
|
4
|
+
import { randomToken } from "../utils/hash.mjs";
|
|
5
|
+
import { SESSION_COOKIE } from "../adapter/auth.mjs";
|
|
6
|
+
import { renderLoginPage } from "../pages/login.mjs";
|
|
7
|
+
import { hxRedirect } from "./htmx.mjs";
|
|
8
|
+
import { HTTPException } from "hono/http-exception";
|
|
9
|
+
//#region src/routers/auth.ts
|
|
10
|
+
const OAUTH_STATE_COOKIE = "storyshelf_oauth_state";
|
|
11
|
+
const SESSION_TTL_SECONDS = 604800;
|
|
12
|
+
const SHARED_USER = {
|
|
13
|
+
id: "shared",
|
|
14
|
+
email: "admin@local",
|
|
15
|
+
name: "Admin",
|
|
16
|
+
role: "admin"
|
|
17
|
+
};
|
|
18
|
+
function sessionCookieHeader(token) {
|
|
19
|
+
return `${SESSION_COOKIE}=${token}; Path=/; HttpOnly; SameSite=Lax; Max-Age=${SESSION_TTL_SECONDS}`;
|
|
20
|
+
}
|
|
21
|
+
function clearSessionCookieHeader() {
|
|
22
|
+
return `${SESSION_COOKIE}=; Path=/; HttpOnly; SameSite=Lax; Max-Age=0`;
|
|
23
|
+
}
|
|
24
|
+
function hasPasswordLogin(auth) {
|
|
25
|
+
return "login" in auth;
|
|
26
|
+
}
|
|
27
|
+
function hasSso(auth) {
|
|
28
|
+
return "loginUrl" in auth;
|
|
29
|
+
}
|
|
30
|
+
function buildSsoUrl(c, auth) {
|
|
31
|
+
const state = randomToken("shelf_").value;
|
|
32
|
+
c.header("set-cookie", `${OAUTH_STATE_COOKIE}=${state}; Path=/; HttpOnly; SameSite=Lax; Max-Age=600`);
|
|
33
|
+
return auth.loginUrl(state);
|
|
34
|
+
}
|
|
35
|
+
function registerAuth(app, auth) {
|
|
36
|
+
app.get("/auth/login", async (c) => {
|
|
37
|
+
if (hasSso(auth) && !hasPasswordLogin(auth)) return c.redirect(buildSsoUrl(c, auth));
|
|
38
|
+
const ssoUrl = hasSso(auth) ? buildSsoUrl(c, auth) : void 0;
|
|
39
|
+
return c.html(await renderLoginPage({ ssoUrl }));
|
|
40
|
+
});
|
|
41
|
+
app.post("/auth/login", async (c) => {
|
|
42
|
+
if (!hasPasswordLogin(auth)) throw new HTTPException(404, { message: "Password login is not configured" });
|
|
43
|
+
const raw = (await c.req.formData()).get("password");
|
|
44
|
+
const password = typeof raw === "string" ? raw : "";
|
|
45
|
+
try {
|
|
46
|
+
const token = await auth.login(password, SHARED_USER);
|
|
47
|
+
c.header("set-cookie", sessionCookieHeader(token));
|
|
48
|
+
return hxRedirect(c, "/");
|
|
49
|
+
} catch {
|
|
50
|
+
return c.html(await renderLoginPage({ error: "Invalid password" }), 401);
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
app.get("/auth/callback", async (c) => {
|
|
54
|
+
if (!auth.handleCallback) return c.notFound();
|
|
55
|
+
const code = c.req.query("code") ?? "";
|
|
56
|
+
const state = c.req.query("state") ?? "";
|
|
57
|
+
if (!code || !state) throw new HTTPException(400, { message: "Missing code or state" });
|
|
58
|
+
const user = await auth.handleCallback({
|
|
59
|
+
provider: "oidc",
|
|
60
|
+
code,
|
|
61
|
+
state
|
|
62
|
+
});
|
|
63
|
+
if (!user) return c.html(await renderLoginPage({ error: "SSO sign in failed" }), 401);
|
|
64
|
+
const token = await auth.createSession(user);
|
|
65
|
+
c.header("set-cookie", sessionCookieHeader(token));
|
|
66
|
+
return c.redirect("/");
|
|
67
|
+
});
|
|
68
|
+
app.post("/auth/logout", async (c) => {
|
|
69
|
+
await auth.destroySession("");
|
|
70
|
+
c.header("set-cookie", clearSessionCookieHeader());
|
|
71
|
+
return hxRedirect(c, "/auth/login");
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
//#endregion
|
|
75
|
+
export { registerAuth };
|
|
76
|
+
|
|
77
|
+
//# sourceMappingURL=auth.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"auth.mjs","names":[],"sources":["../../src/routers/auth.ts"],"sourcesContent":["import type { Context } from \"hono\";\nimport { HTTPException } from \"hono/http-exception\";\nimport type { ShelfApp } from \"../index.tsx\";\n\nimport { SESSION_COOKIE, type AuthAdapter, type AuthUser } from \"../adapters/auth.ts\";\nimport { renderLoginPage } from \"../pages/login.tsx\";\nimport { randomToken } from \"../utils/hash.ts\";\nimport { hxRedirect } from \"./htmx.ts\";\n\nconst OAUTH_STATE_COOKIE = \"storyshelf_oauth_state\";\nconst SESSION_TTL_SECONDS = 7 * 24 * 60 * 60;\n\nconst SHARED_USER: AuthUser = { id: \"shared\", email: \"admin@local\", name: \"Admin\", role: \"admin\" };\n\nfunction sessionCookieHeader(token: string): string {\n return `${SESSION_COOKIE}=${token}; Path=/; HttpOnly; SameSite=Lax; Max-Age=${SESSION_TTL_SECONDS}`;\n}\n\nfunction clearSessionCookieHeader(): string {\n return `${SESSION_COOKIE}=; Path=/; HttpOnly; SameSite=Lax; Max-Age=0`;\n}\n\ntype PasswordAuth = AuthAdapter & { login(password: string, user: AuthUser): Promise<string> };\ntype SsoAuth = AuthAdapter & { loginUrl(state: string): string };\n\nfunction hasPasswordLogin(auth: AuthAdapter): auth is PasswordAuth {\n return \"login\" in auth;\n}\n\nfunction hasSso(auth: AuthAdapter): auth is SsoAuth {\n return \"loginUrl\" in auth;\n}\n\nfunction buildSsoUrl(c: Context, auth: SsoAuth): string {\n const state = randomToken(\"shelf_\").value;\n c.header(\"set-cookie\", `${OAUTH_STATE_COOKIE}=${state}; Path=/; HttpOnly; SameSite=Lax; Max-Age=600`);\n return auth.loginUrl(state);\n}\n\nexport function registerAuth(app: ShelfApp, auth: AuthAdapter): void {\n app.get(\"/auth/login\", async (c) => {\n if (hasSso(auth) && !hasPasswordLogin(auth)) {\n return c.redirect(buildSsoUrl(c, auth));\n }\n const ssoUrl = hasSso(auth) ? buildSsoUrl(c, auth) : undefined;\n return c.html(await renderLoginPage({ ssoUrl }));\n });\n\n app.post(\"/auth/login\", async (c) => {\n if (!hasPasswordLogin(auth)) {\n throw new HTTPException(404, { message: \"Password login is not configured\" });\n }\n const form = await c.req.formData();\n const raw = form.get(\"password\");\n const password = typeof raw === \"string\" ? raw : \"\";\n try {\n const token = await auth.login(password, SHARED_USER);\n c.header(\"set-cookie\", sessionCookieHeader(token));\n return hxRedirect(c, \"/\");\n } catch {\n return c.html(await renderLoginPage({ error: \"Invalid password\" }), 401);\n }\n });\n\n app.get(\"/auth/callback\", async (c) => {\n if (!auth.handleCallback) {\n return c.notFound();\n }\n const code = c.req.query(\"code\") ?? \"\";\n const state = c.req.query(\"state\") ?? \"\";\n if (!code || !state) {\n throw new HTTPException(400, { message: \"Missing code or state\" });\n }\n const user = await auth.handleCallback({ provider: \"oidc\", code, state });\n if (!user) {\n return c.html(await renderLoginPage({ error: \"SSO sign in failed\" }), 401);\n }\n const token = await auth.createSession(user);\n c.header(\"set-cookie\", sessionCookieHeader(token));\n return c.redirect(\"/\");\n });\n\n app.post(\"/auth/logout\", async (c) => {\n await auth.destroySession(\"\");\n c.header(\"set-cookie\", clearSessionCookieHeader());\n return hxRedirect(c, \"/auth/login\");\n });\n}"],"mappings":";;;;;;;;;AASA,MAAM,qBAAqB;AAC3B,MAAM,sBAAsB;AAE5B,MAAM,cAAwB;CAAE,IAAI;CAAU,OAAO;CAAe,MAAM;CAAS,MAAM;AAAQ;AAEjG,SAAS,oBAAoB,OAAuB;CAClD,OAAO,GAAG,eAAe,GAAG,MAAM,4CAA4C;AAChF;AAEA,SAAS,2BAAmC;CAC1C,OAAO,GAAG,eAAe;AAC3B;AAKA,SAAS,iBAAiB,MAAyC;CACjE,OAAO,WAAW;AACpB;AAEA,SAAS,OAAO,MAAoC;CAClD,OAAO,cAAc;AACvB;AAEA,SAAS,YAAY,GAAY,MAAuB;CACtD,MAAM,QAAQ,YAAY,QAAQ,CAAC,CAAC;CACpC,EAAE,OAAO,cAAc,GAAG,mBAAmB,GAAG,MAAM,8CAA8C;CACpG,OAAO,KAAK,SAAS,KAAK;AAC5B;AAEA,SAAgB,aAAa,KAAe,MAAyB;CACnE,IAAI,IAAI,eAAe,OAAO,MAAM;EAClC,IAAI,OAAO,IAAI,KAAK,CAAC,iBAAiB,IAAI,GACxC,OAAO,EAAE,SAAS,YAAY,GAAG,IAAI,CAAC;EAExC,MAAM,SAAS,OAAO,IAAI,IAAI,YAAY,GAAG,IAAI,IAAI,KAAA;EACrD,OAAO,EAAE,KAAK,MAAM,gBAAgB,EAAE,OAAO,CAAC,CAAC;CACjD,CAAC;CAED,IAAI,KAAK,eAAe,OAAO,MAAM;EACnC,IAAI,CAAC,iBAAiB,IAAI,GACxB,MAAM,IAAI,cAAc,KAAK,EAAE,SAAS,mCAAmC,CAAC;EAG9E,MAAM,OAAM,MADO,EAAE,IAAI,SAAS,EAAA,CACjB,IAAI,UAAU;EAC/B,MAAM,WAAW,OAAO,QAAQ,WAAW,MAAM;EACjD,IAAI;GACF,MAAM,QAAQ,MAAM,KAAK,MAAM,UAAU,WAAW;GACpD,EAAE,OAAO,cAAc,oBAAoB,KAAK,CAAC;GACjD,OAAO,WAAW,GAAG,GAAG;EAC1B,QAAQ;GACN,OAAO,EAAE,KAAK,MAAM,gBAAgB,EAAE,OAAO,mBAAmB,CAAC,GAAG,GAAG;EACzE;CACF,CAAC;CAED,IAAI,IAAI,kBAAkB,OAAO,MAAM;EACrC,IAAI,CAAC,KAAK,gBACR,OAAO,EAAE,SAAS;EAEpB,MAAM,OAAO,EAAE,IAAI,MAAM,MAAM,KAAK;EACpC,MAAM,QAAQ,EAAE,IAAI,MAAM,OAAO,KAAK;EACtC,IAAI,CAAC,QAAQ,CAAC,OACZ,MAAM,IAAI,cAAc,KAAK,EAAE,SAAS,wBAAwB,CAAC;EAEnE,MAAM,OAAO,MAAM,KAAK,eAAe;GAAE,UAAU;GAAQ;GAAM;EAAM,CAAC;EACxE,IAAI,CAAC,MACH,OAAO,EAAE,KAAK,MAAM,gBAAgB,EAAE,OAAO,qBAAqB,CAAC,GAAG,GAAG;EAE3E,MAAM,QAAQ,MAAM,KAAK,cAAc,IAAI;EAC3C,EAAE,OAAO,cAAc,oBAAoB,KAAK,CAAC;EACjD,OAAO,EAAE,SAAS,GAAG;CACvB,CAAC;CAED,IAAI,KAAK,gBAAgB,OAAO,MAAM;EACpC,MAAM,KAAK,eAAe,EAAE;EAC5B,EAAE,OAAO,cAAc,yBAAyB,CAAC;EACjD,OAAO,WAAW,GAAG,aAAa;CACpC,CAAC;AACH"}
|