@twango/twiki 0.0.0-stage → 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/.github/actions/product-cache/action.yml +32 -0
- package/.github/actions/product-cache/index.mjs +450 -0
- package/LICENSE +21 -0
- package/README.md +146 -2
- package/bun.lock +1701 -0
- package/cli/runtime.test.ts +420 -0
- package/cli/runtime.ts +385 -0
- package/cli/twiki.ts +305 -0
- package/package.json +138 -4
- package/site/_headers +7 -0
- package/site/assets/fonts/OFL.txt +93 -0
- package/site/assets/fonts/OverusedGrotesk-SemiBold.ttf +0 -0
- package/site/assets/social-background.svg +38 -0
- package/site/bun.lock +1701 -0
- package/site/database/index.ts +7 -0
- package/site/database/schema.ts +95 -0
- package/site/drizzle.config.ts +7 -0
- package/site/migrations/0001_auth.sql +14 -0
- package/site/migrations/0002_site_access.sql +5 -0
- package/site/migrations/0003_github_username.sql +1 -0
- package/site/migrations/meta/0002_snapshot.json +432 -0
- package/site/migrations/meta/0003_snapshot.json +439 -0
- package/site/migrations/meta/_journal.json +27 -0
- package/site/package.json +120 -0
- package/site/src/app.d.ts +14 -0
- package/site/src/app.html +11 -0
- package/site/src/hooks.server.ts +12 -0
- package/site/src/lib/analytics-config.json +1 -0
- package/site/src/lib/analytics.ts +40 -0
- package/site/src/lib/auth-config.json +3 -0
- package/site/src/lib/components/SEO.svelte +113 -0
- package/site/src/lib/components/doc/AccessLink.svelte +27 -0
- package/site/src/lib/components/doc/Backlinks.svelte +59 -0
- package/site/src/lib/components/doc/Breadcrumbs.svelte +29 -0
- package/site/src/lib/components/doc/ContentMeta.svelte +61 -0
- package/site/src/lib/components/doc/DocPager.svelte +91 -0
- package/site/src/lib/components/doc/DocShell.svelte +115 -0
- package/site/src/lib/components/doc/LicenseNotice.svelte +26 -0
- package/site/src/lib/components/doc/LockedContent.svelte +29 -0
- package/site/src/lib/components/doc/NavTree.svelte +158 -0
- package/site/src/lib/components/doc/NoteActions.svelte +139 -0
- package/site/src/lib/components/doc/PublicationBadge.svelte +49 -0
- package/site/src/lib/components/doc/ReaderModeToggle.svelte +19 -0
- package/site/src/lib/components/doc/Sidebar.svelte +351 -0
- package/site/src/lib/components/doc/Toc.svelte +339 -0
- package/site/src/lib/components/doc/enhancements.ts +146 -0
- package/site/src/lib/components/doc/heading-tracker.svelte.ts +39 -0
- package/site/src/lib/components/doc/image-preview.ts +114 -0
- package/site/src/lib/components/doc/reader-mode.svelte.ts +40 -0
- package/site/src/lib/components/embeds/mermaid.ts +353 -0
- package/site/src/lib/components/files/FileCode.svelte +81 -0
- package/site/src/lib/components/files/FileExplorer.svelte +74 -0
- package/site/src/lib/components/files/FileHistory.svelte +170 -0
- package/site/src/lib/components/files/FileIcon.svelte +28 -0
- package/site/src/lib/components/files/FileRmd.svelte +209 -0
- package/site/src/lib/components/files/FileTabs.svelte +180 -0
- package/site/src/lib/components/files/FileTree.svelte +67 -0
- package/site/src/lib/components/files/blame-gutter.ts +45 -0
- package/site/src/lib/components/files/code-view.ts +98 -0
- package/site/src/lib/components/files/file-workspace.svelte.ts +100 -0
- package/site/src/lib/components/graph/GraphPanel.svelte +275 -0
- package/site/src/lib/components/graph/GraphView.svelte +100 -0
- package/site/src/lib/components/graph/graph-data.ts +137 -0
- package/site/src/lib/components/graph/graph-model.ts +103 -0
- package/site/src/lib/components/graph/index.ts +3 -0
- package/site/src/lib/components/graph/render-graph.ts +626 -0
- package/site/src/lib/components/popover/index.ts +1 -0
- package/site/src/lib/components/popover/link-popovers.ts +233 -0
- package/site/src/lib/components/popover/popover.css +82 -0
- package/site/src/lib/components/search/SearchButton.svelte +57 -0
- package/site/src/lib/components/search/SearchPalette.svelte +189 -0
- package/site/src/lib/components/search/index.ts +3 -0
- package/site/src/lib/components/search/pagefind-client.svelte.ts +129 -0
- package/site/src/lib/components/search/pagefind-query.ts +22 -0
- package/site/src/lib/components/search/pagefind-url.ts +12 -0
- package/site/src/lib/components/search/search-state.svelte.ts +22 -0
- package/site/src/lib/components/ui/IconButton.svelte +30 -0
- package/site/src/lib/components/ui/ThemeToggle.svelte +22 -0
- package/site/src/lib/components/ui/index.ts +1 -0
- package/site/src/lib/config.ts +9 -0
- package/site/src/lib/content-license.ts +7 -0
- package/site/src/lib/file-history.ts +37 -0
- package/site/src/lib/file-icons.ts +27 -0
- package/site/src/lib/file-tabs.ts +72 -0
- package/site/src/lib/files.ts +73 -0
- package/site/src/lib/github-user.ts +70 -0
- package/site/src/lib/metadata.ts +166 -0
- package/site/src/lib/notebook-nav.ts +63 -0
- package/site/src/lib/publication.ts +1 -0
- package/site/src/lib/rmd.ts +156 -0
- package/site/src/lib/server/access-admin.ts +33 -0
- package/site/src/lib/server/content-model.ts +253 -0
- package/site/src/lib/server/content-provider.ts +266 -0
- package/site/src/lib/server/content.ts +277 -0
- package/site/src/lib/server/runtime-content.ts +34 -0
- package/site/src/lib/site-config.json +9 -0
- package/site/src/lib/social-image.ts +13 -0
- package/site/src/lib/styles/article.css +607 -0
- package/site/src/lib/styles/foundation.css +138 -0
- package/site/src/lib/styles/print.css +115 -0
- package/site/src/lib/styles/shell.css +182 -0
- package/site/src/lib/text-exports.ts +5 -0
- package/site/src/lib/types.ts +140 -0
- package/site/src/lib/utils.ts +6 -0
- package/site/src/routes/+error.svelte +16 -0
- package/site/src/routes/+layout.server.ts +10 -0
- package/site/src/routes/+layout.svelte +72 -0
- package/site/src/routes/+layout.ts +3 -0
- package/site/src/routes/404/+page.svelte +13 -0
- package/site/src/routes/[...slug]/+page.server.ts +24 -0
- package/site/src/routes/[...slug]/+page.svelte +180 -0
- package/site/src/routes/[course]/files/[...file]/+page.svelte +373 -0
- package/site/src/routes/[course]/files/[...file]/+page.ts +38 -0
- package/site/src/routes/admin/access/+page.server.ts +53 -0
- package/site/src/routes/admin/access/+page.svelte +118 -0
- package/site/src/routes/graph.json/+server.ts +7 -0
- package/site/src/routes/index.xml/+server.ts +7 -0
- package/site/src/routes/layout.css +10 -0
- package/site/src/routes/robots.txt/+server.ts +9 -0
- package/site/src/routes/sitemap.xml/+server.ts +7 -0
- package/site/src/routes/tags/+page.server.ts +5 -0
- package/site/src/routes/tags/+page.svelte +38 -0
- package/site/src/routes/tags/[...tag]/+page.server.ts +22 -0
- package/site/src/routes/tags/[...tag]/+page.svelte +52 -0
- package/site/static/favicon.png +0 -0
- package/site/static/fonts/JetBrainsMono-Italic-VF.woff2 +0 -0
- package/site/static/fonts/JetBrainsMono-VF.woff2 +0 -0
- package/site/static/fonts/OverusedGrotesk-VF.woff2 +0 -0
- package/site/static/fonts/OverusedGroteskItalic-VF.woff2 +0 -0
- package/site/static/fonts/Virgil.woff2 +0 -0
- package/site/svelte.config.js +71 -0
- package/site/tooling/LICENSE-quartz +25 -0
- package/site/tooling/adapter.js +55 -0
- package/site/tooling/assets.ts +323 -0
- package/site/tooling/benchmark.ts +191 -0
- package/site/tooling/build.ts +407 -0
- package/site/tooling/cache.ts +133 -0
- package/site/tooling/check-diagrams.ts +164 -0
- package/site/tooling/check-links.ts +86 -0
- package/site/tooling/check-targets.ts +139 -0
- package/site/tooling/compiler.ts +1386 -0
- package/site/tooling/deploy.ts +112 -0
- package/site/tooling/fingerprint.ts +81 -0
- package/site/tooling/lib/app-version.js +92 -0
- package/site/tooling/lib/application-asset-urls.ts +27 -0
- package/site/tooling/lib/application-cache.ts +269 -0
- package/site/tooling/lib/asset-hash-cache.ts +116 -0
- package/site/tooling/lib/autotag.ts +99 -0
- package/site/tooling/lib/build-assembly.ts +148 -0
- package/site/tooling/lib/build-workspace.ts +386 -0
- package/site/tooling/lib/cache-selection.ts +70 -0
- package/site/tooling/lib/cloudflare-prewarm.js +40 -0
- package/site/tooling/lib/comments.ts +32 -0
- package/site/tooling/lib/content-cache.ts +244 -0
- package/site/tooling/lib/content-urls.ts +50 -0
- package/site/tooling/lib/course-cache.ts +298 -0
- package/site/tooling/lib/course-files.ts +281 -0
- package/site/tooling/lib/currency.ts +33 -0
- package/site/tooling/lib/deployment-config.ts +222 -0
- package/site/tooling/lib/deployment-manifest.ts +274 -0
- package/site/tooling/lib/deployment-prepare.ts +325 -0
- package/site/tooling/lib/deployment-upload.ts +293 -0
- package/site/tooling/lib/dev-git-state.ts +130 -0
- package/site/tooling/lib/dev-history.ts +86 -0
- package/site/tooling/lib/dev-social-images.ts +38 -0
- package/site/tooling/lib/dev-state.ts +107 -0
- package/site/tooling/lib/edition-builds.ts +184 -0
- package/site/tooling/lib/edition-paths.js +29 -0
- package/site/tooling/lib/empty-tag-route.js +23 -0
- package/site/tooling/lib/file-history.ts +201 -0
- package/site/tooling/lib/file-icons.ts +93 -0
- package/site/tooling/lib/file-policy.ts +38 -0
- package/site/tooling/lib/file-search.js +13 -0
- package/site/tooling/lib/git-revisions.ts +41 -0
- package/site/tooling/lib/history-pool.ts +74 -0
- package/site/tooling/lib/history-worker.ts +22 -0
- package/site/tooling/lib/knit-rmd.R +38 -0
- package/site/tooling/lib/lastmod.ts +201 -0
- package/site/tooling/lib/markdown-files.ts +51 -0
- package/site/tooling/lib/ofm.ts +634 -0
- package/site/tooling/lib/output.ts +60 -0
- package/site/tooling/lib/product-cache.ts +1006 -0
- package/site/tooling/lib/project-config.ts +288 -0
- package/site/tooling/lib/public-assets.ts +38 -0
- package/site/tooling/lib/public-links.ts +56 -0
- package/site/tooling/lib/publishing.ts +156 -0
- package/site/tooling/lib/r-runtime.ts +257 -0
- package/site/tooling/lib/rebuild-queue.ts +25 -0
- package/site/tooling/lib/rmd-preflight.ts +166 -0
- package/site/tooling/lib/rmd-preview-cache.ts +93 -0
- package/site/tooling/lib/rmd-previews.ts +129 -0
- package/site/tooling/lib/runtime-data-modules.ts +30 -0
- package/site/tooling/lib/runtime-packaging.ts +321 -0
- package/site/tooling/lib/search-cache.js +165 -0
- package/site/tooling/lib/search-inputs.js +129 -0
- package/site/tooling/lib/slug.ts +377 -0
- package/site/tooling/lib/social-images.ts +210 -0
- package/site/tooling/lib/text-exports.ts +51 -0
- package/site/tooling/lib/visit-elements.ts +17 -0
- package/site/tooling/live-test.ts +138 -0
- package/site/tooling/pipeline.ts +103 -0
- package/site/tooling/prepare.ts +3 -0
- package/site/tooling/publish.ts +28 -0
- package/site/tooling/r-cache.ts +45 -0
- package/site/tooling/r-render-profile.json +7 -0
- package/site/tooling/search.js +94 -0
- package/site/tooling/verify-deployment.ts +109 -0
- package/site/tooling/vite-content.ts +240 -0
- package/site/tooling/wrangler-paths.js +25 -0
- package/site/tooling/wrangler.ts +19 -0
- package/site/tsconfig.json +16 -0
- package/site/vite.config.ts +40 -0
- package/site/worker/access.ts +57 -0
- package/site/worker/analytics.ts +100 -0
- package/site/worker/auth-routes.ts +78 -0
- package/site/worker/auth.ts +78 -0
- package/site/worker/content-runtime.ts +226 -0
- package/site/worker/framing.ts +4 -0
- package/site/worker/gate.ts +147 -0
- package/site/worker/index.ts +9 -0
- package/site/worker/login.ts +62 -0
- package/site/worker/publication.ts +44 -0
- package/site/worker/runtime.ts +104 -0
- package/site/worker/tsconfig.json +13 -0
- package/site/wrangler.jsonc +37 -0
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
import { existsSync, type FSWatcher } from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import type { Plugin } from 'vite';
|
|
4
|
+
import { prepareContent } from './pipeline';
|
|
5
|
+
import { contentRepositories, watchContentGitDirectory } from './lib/dev-git-state';
|
|
6
|
+
import { rebuildQueue } from './lib/rebuild-queue';
|
|
7
|
+
import { resolveRuntimeDataImport } from './lib/runtime-data-modules';
|
|
8
|
+
import { resolveProject, projectSourceVisible } from './lib/project-config';
|
|
9
|
+
|
|
10
|
+
export function content(): Plugin {
|
|
11
|
+
return {
|
|
12
|
+
name: 'wisconsin-content',
|
|
13
|
+
enforce: 'pre',
|
|
14
|
+
resolveId(source) {
|
|
15
|
+
return resolveRuntimeDataImport(source, this.environment.config);
|
|
16
|
+
},
|
|
17
|
+
config: {
|
|
18
|
+
order: 'pre',
|
|
19
|
+
async handler(_config, environment) {
|
|
20
|
+
if (
|
|
21
|
+
environment.command === 'build' &&
|
|
22
|
+
process.env.WISCONSIN_SKIP_CONTENT !== '1' &&
|
|
23
|
+
process.env.npm_lifecycle_event !== 'prepare'
|
|
24
|
+
)
|
|
25
|
+
await prepareContent(true);
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
async configureServer(server) {
|
|
29
|
+
await prepareContent(true, undefined, true);
|
|
30
|
+
const project = resolveProject(server.config.root);
|
|
31
|
+
const repo = project.root;
|
|
32
|
+
const directory = project.contentDir;
|
|
33
|
+
let closed = false;
|
|
34
|
+
let failure: Error | undefined;
|
|
35
|
+
let changedInputs = new Set<string>();
|
|
36
|
+
let forceRebuild = false;
|
|
37
|
+
let gitTopologyChanged = true;
|
|
38
|
+
const gitWatchers = new Map<
|
|
39
|
+
string,
|
|
40
|
+
{ watcher: FSWatcher; recursive: boolean; files: Set<string>; repositories: Set<string> }
|
|
41
|
+
>();
|
|
42
|
+
const queue = rebuildQueue(async () => {
|
|
43
|
+
if (closed) return;
|
|
44
|
+
try {
|
|
45
|
+
const inputs = changedInputs;
|
|
46
|
+
changedInputs = new Set();
|
|
47
|
+
const reuse = !forceRebuild && !failure;
|
|
48
|
+
forceRebuild = false;
|
|
49
|
+
if (gitTopologyChanged) {
|
|
50
|
+
refreshGitWatchers();
|
|
51
|
+
gitTopologyChanged = false;
|
|
52
|
+
}
|
|
53
|
+
// Git can refresh an index or another worktree's ref without changing
|
|
54
|
+
// our inputs. Validate its snapshot; content events and failure
|
|
55
|
+
// recovery still force reconciliation.
|
|
56
|
+
const changed = await prepareContent(reuse, inputs, true);
|
|
57
|
+
const recovered = Boolean(failure);
|
|
58
|
+
failure = undefined;
|
|
59
|
+
if (!changed?.size && !recovered) return;
|
|
60
|
+
const reader = path.resolve(server.config.root, 'src/lib/server/content.ts');
|
|
61
|
+
const generated = path.resolve(server.config.root, 'src/lib/generated') + path.sep;
|
|
62
|
+
for (const environment of Object.values(server.environments)) {
|
|
63
|
+
for (const module of environment.moduleGraph.getModulesByFile(reader) ?? [])
|
|
64
|
+
environment.moduleGraph.invalidateModule(module);
|
|
65
|
+
for (const file of changed ?? []) {
|
|
66
|
+
if (!file.startsWith(generated)) continue;
|
|
67
|
+
for (const module of environment.moduleGraph.getModulesByFile(file) ?? [])
|
|
68
|
+
await environment.reloadModule(module);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
const assetRoot = path.resolve(server.config.root, 'build/generated/assets') + path.sep;
|
|
72
|
+
const changedAsset = [...(changed ?? [])].some(
|
|
73
|
+
(file) =>
|
|
74
|
+
file.startsWith(assetRoot) && !file.startsWith(assetRoot + '_files' + path.sep)
|
|
75
|
+
);
|
|
76
|
+
// Browsers cache image/PDF URLs independently of SvelteKit's page data.
|
|
77
|
+
if (changedAsset) server.ws.send({ type: 'full-reload' });
|
|
78
|
+
else server.ws.send({ type: 'custom', event: 'wisconsin:content', data: { recovered } });
|
|
79
|
+
} catch (error) {
|
|
80
|
+
failure = error instanceof Error ? error : new Error(String(error));
|
|
81
|
+
changedInputs.add(repo); // Recovery must reconcile every course after a partial failure.
|
|
82
|
+
forceRebuild = true;
|
|
83
|
+
gitTopologyChanged = true;
|
|
84
|
+
server.config.logger.error(failure.message);
|
|
85
|
+
server.ws.send({
|
|
86
|
+
type: 'error',
|
|
87
|
+
err: { message: failure.message, stack: failure.stack ?? '' }
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
const rebuild = (file: string) => {
|
|
92
|
+
forceRebuild = true;
|
|
93
|
+
changedInputs.add(file);
|
|
94
|
+
queue.schedule();
|
|
95
|
+
};
|
|
96
|
+
const rebuildGit = (repository: string) => {
|
|
97
|
+
changedInputs.add(repository);
|
|
98
|
+
queue.schedule();
|
|
99
|
+
};
|
|
100
|
+
const changed = (event: string, file: string) => {
|
|
101
|
+
if (!['add', 'change', 'unlink'].includes(event)) return;
|
|
102
|
+
if (file === project.configFile) {
|
|
103
|
+
void server.restart().catch((error) => server.config.logger.error(String(error)));
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
if (
|
|
107
|
+
file === path.join(repo, '.gitmodules') ||
|
|
108
|
+
(file.startsWith(directory + path.sep) &&
|
|
109
|
+
['.gitmodules', '.git'].includes(path.basename(file)))
|
|
110
|
+
) {
|
|
111
|
+
gitTopologyChanged = true;
|
|
112
|
+
rebuild(file);
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
if (
|
|
116
|
+
!file.startsWith(directory + path.sep) ||
|
|
117
|
+
!projectSourceVisible(project, path.relative(directory, file).split(path.sep).join('/'))
|
|
118
|
+
)
|
|
119
|
+
return;
|
|
120
|
+
// Hidden paths never enter the content catalog; Git metadata is watched separately.
|
|
121
|
+
if (
|
|
122
|
+
path
|
|
123
|
+
.relative(directory, file)
|
|
124
|
+
.split(path.sep)
|
|
125
|
+
.some((part) => part.startsWith('.'))
|
|
126
|
+
)
|
|
127
|
+
return;
|
|
128
|
+
rebuild(file);
|
|
129
|
+
};
|
|
130
|
+
server.watcher.add([
|
|
131
|
+
directory,
|
|
132
|
+
path.join(repo, '.gitmodules'),
|
|
133
|
+
...(project.configFile ? [project.configFile] : [])
|
|
134
|
+
]);
|
|
135
|
+
server.watcher.on('all', changed);
|
|
136
|
+
function refreshGitWatchers() {
|
|
137
|
+
const wanted = new Map<
|
|
138
|
+
string,
|
|
139
|
+
{ recursive: boolean; files: Set<string>; repositories: Set<string> }
|
|
140
|
+
>();
|
|
141
|
+
const add = (directory: string, file: string, repository: string, recursive = false) => {
|
|
142
|
+
const entry = wanted.get(directory) ?? {
|
|
143
|
+
recursive,
|
|
144
|
+
files: new Set<string>(),
|
|
145
|
+
repositories: new Set<string>()
|
|
146
|
+
};
|
|
147
|
+
entry.recursive ||= recursive;
|
|
148
|
+
entry.files.add(file);
|
|
149
|
+
entry.repositories.add(repository);
|
|
150
|
+
wanted.set(directory, entry);
|
|
151
|
+
};
|
|
152
|
+
for (const repository of contentRepositories(repo)) {
|
|
153
|
+
for (const file of [
|
|
154
|
+
repository.index,
|
|
155
|
+
repository.config,
|
|
156
|
+
repository.worktreeConfig,
|
|
157
|
+
path.join(repository.gitDirectory, 'HEAD'),
|
|
158
|
+
path.join(repository.commonDirectory, 'packed-refs'),
|
|
159
|
+
path.join(repository.commonDirectory, 'refs')
|
|
160
|
+
])
|
|
161
|
+
add(path.dirname(file), path.basename(file), repository.directory);
|
|
162
|
+
const refs = path.join(repository.commonDirectory, 'refs');
|
|
163
|
+
if (existsSync(refs)) add(refs, '', repository.directory, true);
|
|
164
|
+
}
|
|
165
|
+
for (const [directory, entry] of gitWatchers) {
|
|
166
|
+
if (!wanted.has(directory) || wanted.get(directory)!.recursive !== entry.recursive) {
|
|
167
|
+
entry.watcher.close();
|
|
168
|
+
gitWatchers.delete(directory);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
for (const [directory, entry] of wanted) {
|
|
172
|
+
const previous = gitWatchers.get(directory);
|
|
173
|
+
if (previous) {
|
|
174
|
+
previous.files = entry.files;
|
|
175
|
+
previous.repositories = entry.repositories;
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
178
|
+
// Watch metadata directories shallowly; recurse only through refs,
|
|
179
|
+
// never through the shared object store.
|
|
180
|
+
const watcher = watchContentGitDirectory(
|
|
181
|
+
directory,
|
|
182
|
+
entry.recursive,
|
|
183
|
+
() => gitWatchers.get(directory)?.files ?? new Set(),
|
|
184
|
+
() => {
|
|
185
|
+
const current = gitWatchers.get(directory);
|
|
186
|
+
if (closed || !current) return;
|
|
187
|
+
gitTopologyChanged = true;
|
|
188
|
+
for (const repository of current.repositories) rebuildGit(repository);
|
|
189
|
+
}
|
|
190
|
+
);
|
|
191
|
+
if (!watcher) continue;
|
|
192
|
+
watcher.on('error', () => {
|
|
193
|
+
const current = gitWatchers.get(directory);
|
|
194
|
+
watcher.close();
|
|
195
|
+
gitWatchers.delete(directory);
|
|
196
|
+
if (closed) return;
|
|
197
|
+
gitTopologyChanged = true;
|
|
198
|
+
for (const repository of current?.repositories ?? []) rebuildGit(repository);
|
|
199
|
+
});
|
|
200
|
+
gitWatchers.set(directory, { ...entry, watcher });
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
refreshGitWatchers();
|
|
204
|
+
gitTopologyChanged = false;
|
|
205
|
+
server.middlewares.use((request, response, next) => {
|
|
206
|
+
void queue
|
|
207
|
+
.wait()
|
|
208
|
+
.then(async () => {
|
|
209
|
+
if (failure) return next(failure);
|
|
210
|
+
// Keep optional renderers and history builders off the startup path.
|
|
211
|
+
const pathname = new URL(request.url ?? '/', 'http://localhost').pathname;
|
|
212
|
+
if (pathname.startsWith('/__content/history')) {
|
|
213
|
+
const { serveDevHistory } = await import('./lib/dev-history');
|
|
214
|
+
if (serveDevHistory(request, response, server.config.root, repo)) return;
|
|
215
|
+
}
|
|
216
|
+
if (pathname.startsWith('/_og/')) {
|
|
217
|
+
const { serveDevSocialImage } = await import('./lib/dev-social-images');
|
|
218
|
+
if (await serveDevSocialImage(request, response, server.config.root)) return;
|
|
219
|
+
}
|
|
220
|
+
next();
|
|
221
|
+
})
|
|
222
|
+
.catch(next);
|
|
223
|
+
});
|
|
224
|
+
server.httpServer?.once('close', () => {
|
|
225
|
+
closed = true;
|
|
226
|
+
|
|
227
|
+
server.watcher.off('all', changed);
|
|
228
|
+
for (const { watcher } of gitWatchers.values()) watcher.close();
|
|
229
|
+
});
|
|
230
|
+
},
|
|
231
|
+
hotUpdate({ file }) {
|
|
232
|
+
if (
|
|
233
|
+
file.includes('/src/lib/generated/') ||
|
|
234
|
+
file.includes('/build/generated/') ||
|
|
235
|
+
file.startsWith(path.resolve('static') + path.sep)
|
|
236
|
+
)
|
|
237
|
+
return [];
|
|
238
|
+
}
|
|
239
|
+
};
|
|
240
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { existsSync, lstatSync, mkdirSync, readlinkSync, renameSync, symlinkSync } from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
/** Keep Wrangler's hardcoded temporary paths under the IDE-excluded build directory.
|
|
5
|
+
* @param {string} site
|
|
6
|
+
*/
|
|
7
|
+
export function prepareWranglerPaths(site) {
|
|
8
|
+
const legacy = path.join(site, '.wrangler');
|
|
9
|
+
const target = path.join(site, 'build/.wrangler');
|
|
10
|
+
mkdirSync(path.dirname(target), { recursive: true });
|
|
11
|
+
const stat = lstatSync(legacy, { throwIfNoEntry: false });
|
|
12
|
+
if (stat?.isSymbolicLink()) {
|
|
13
|
+
if (path.resolve(site, readlinkSync(legacy)) !== target)
|
|
14
|
+
throw new Error('Unexpected .wrangler symlink; refusing to replace local state');
|
|
15
|
+
} else if (stat) {
|
|
16
|
+
if (existsSync(target))
|
|
17
|
+
throw new Error(
|
|
18
|
+
'Both .wrangler and build/.wrangler exist; refusing to overwrite local state'
|
|
19
|
+
);
|
|
20
|
+
renameSync(legacy, target);
|
|
21
|
+
}
|
|
22
|
+
mkdirSync(target, { recursive: true });
|
|
23
|
+
if (!stat?.isSymbolicLink()) symlinkSync('build/.wrangler', legacy, 'dir');
|
|
24
|
+
return target;
|
|
25
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { prepareWranglerPaths } from './wrangler-paths.js';
|
|
4
|
+
|
|
5
|
+
const site = path.resolve(import.meta.dirname, '..');
|
|
6
|
+
const directory = prepareWranglerPaths(site);
|
|
7
|
+
const child = spawn('bunx', ['wrangler', ...process.argv.slice(2)], {
|
|
8
|
+
cwd: site,
|
|
9
|
+
stdio: 'inherit',
|
|
10
|
+
env: { ...process.env, WRANGLER_CACHE_DIR: path.join(directory, 'cache') }
|
|
11
|
+
});
|
|
12
|
+
for (const signal of ['SIGINT', 'SIGTERM'] as const) process.on(signal, () => child.kill(signal));
|
|
13
|
+
child.on('error', (error) => {
|
|
14
|
+
console.error(error);
|
|
15
|
+
process.exitCode = 1;
|
|
16
|
+
});
|
|
17
|
+
child.on('exit', (code) => {
|
|
18
|
+
process.exitCode = code ?? 1;
|
|
19
|
+
});
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"extends": "./build/.svelte-kit/tsconfig.json",
|
|
3
|
+
"compilerOptions": {
|
|
4
|
+
"types": ["node", "bun"],
|
|
5
|
+
"rewriteRelativeImportExtensions": true,
|
|
6
|
+
"allowJs": true,
|
|
7
|
+
"checkJs": true,
|
|
8
|
+
"esModuleInterop": true,
|
|
9
|
+
"forceConsistentCasingInFileNames": true,
|
|
10
|
+
"resolveJsonModule": true,
|
|
11
|
+
"skipLibCheck": true,
|
|
12
|
+
"sourceMap": true,
|
|
13
|
+
"strict": true,
|
|
14
|
+
"moduleResolution": "bundler"
|
|
15
|
+
}
|
|
16
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { statSync } from 'node:fs';
|
|
2
|
+
import { isAbsolute } from 'node:path';
|
|
3
|
+
import tailwindcss from '@tailwindcss/vite';
|
|
4
|
+
import { sveltekit } from '@sveltejs/kit/vite';
|
|
5
|
+
import { defineConfig, searchForWorkspaceRoot } from 'vite';
|
|
6
|
+
import { content } from './tooling/vite-content';
|
|
7
|
+
import { applicationAssetUrls } from './tooling/lib/application-asset-urls';
|
|
8
|
+
|
|
9
|
+
const dependencyPaths: unknown = JSON.parse(process.env.TWIKI_DEPENDENCY_PATHS ?? '[]');
|
|
10
|
+
if (
|
|
11
|
+
!Array.isArray(dependencyPaths) ||
|
|
12
|
+
!dependencyPaths.every(
|
|
13
|
+
(value): value is string =>
|
|
14
|
+
typeof value === 'string' && isAbsolute(value) && statSync(value).isDirectory()
|
|
15
|
+
)
|
|
16
|
+
)
|
|
17
|
+
throw new Error('TWIKI_DEPENDENCY_PATHS must contain absolute dependency directories');
|
|
18
|
+
|
|
19
|
+
export default defineConfig({
|
|
20
|
+
plugins: [content(), tailwindcss(), sveltekit(), applicationAssetUrls()],
|
|
21
|
+
// The file viewer imports WebR lazily. Discover it at startup so opening
|
|
22
|
+
// a file does not trigger dependency optimization and reset the page.
|
|
23
|
+
optimizeDeps: { include: ['webr'] },
|
|
24
|
+
// Gzip size reporting recompresses bundles solely for the build log.
|
|
25
|
+
build: { reportCompressedSize: false },
|
|
26
|
+
server: {
|
|
27
|
+
// CLI staging keeps dependencies in the installed package, outside the consumer runtime.
|
|
28
|
+
// Preserve Vite's workspace boundary and allow only that resolved dependency tree.
|
|
29
|
+
fs: {
|
|
30
|
+
allow: [
|
|
31
|
+
searchForWorkspaceRoot(process.cwd()),
|
|
32
|
+
...dependencyPaths,
|
|
33
|
+
...(process.env.TWIKI_DEPENDENCY_DIR ? [process.env.TWIKI_DEPENDENCY_DIR] : [])
|
|
34
|
+
]
|
|
35
|
+
},
|
|
36
|
+
watch: {
|
|
37
|
+
ignored: ['**/site/build/**']
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
});
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { and, desc, eq, isNotNull, or } from 'drizzle-orm';
|
|
2
|
+
import { database } from '../database';
|
|
3
|
+
import { account, siteAccess } from '../database/schema';
|
|
4
|
+
import type { AuthEnv } from './auth';
|
|
5
|
+
import type { D1Database } from '@cloudflare/workers-types';
|
|
6
|
+
|
|
7
|
+
export async function isOwner(binding: D1Database, userId: string, ownerId: string) {
|
|
8
|
+
return Boolean(
|
|
9
|
+
await database(binding)
|
|
10
|
+
.select({ id: account.id })
|
|
11
|
+
.from(account)
|
|
12
|
+
.where(
|
|
13
|
+
and(
|
|
14
|
+
eq(account.userId, userId),
|
|
15
|
+
eq(account.providerId, 'github'),
|
|
16
|
+
eq(account.accountId, ownerId)
|
|
17
|
+
)
|
|
18
|
+
)
|
|
19
|
+
.get()
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export async function isGithubAllowed(env: AuthEnv, githubId: string) {
|
|
24
|
+
return (
|
|
25
|
+
githubId === env.OWNER_GITHUB_ID ||
|
|
26
|
+
Boolean(
|
|
27
|
+
await database(env.DB)
|
|
28
|
+
.select({ id: siteAccess.githubId })
|
|
29
|
+
.from(siteAccess)
|
|
30
|
+
.where(eq(siteAccess.githubId, githubId))
|
|
31
|
+
.get()
|
|
32
|
+
)
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export async function accessForUser(
|
|
37
|
+
env: AuthEnv,
|
|
38
|
+
userId: string
|
|
39
|
+
): Promise<'owner' | 'member' | null> {
|
|
40
|
+
const owner = eq(account.accountId, env.OWNER_GITHUB_ID);
|
|
41
|
+
const match = await database(env.DB)
|
|
42
|
+
.select({ accountId: account.accountId })
|
|
43
|
+
.from(account)
|
|
44
|
+
.leftJoin(siteAccess, eq(siteAccess.githubId, account.accountId))
|
|
45
|
+
.where(
|
|
46
|
+
and(
|
|
47
|
+
eq(account.userId, userId),
|
|
48
|
+
eq(account.providerId, 'github'),
|
|
49
|
+
or(owner, isNotNull(siteAccess.githubId))
|
|
50
|
+
)
|
|
51
|
+
)
|
|
52
|
+
.orderBy(desc(owner))
|
|
53
|
+
.limit(1)
|
|
54
|
+
.get();
|
|
55
|
+
if (!match) return null;
|
|
56
|
+
return match.accountId === env.OWNER_GITHUB_ID ? 'owner' : 'member';
|
|
57
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
export interface AnalyticsOptions {
|
|
2
|
+
siteId: string;
|
|
3
|
+
endpoint: string;
|
|
4
|
+
}
|
|
5
|
+
/** Opt-in analytics relay; never forwards vault authentication upstream. */
|
|
6
|
+
export async function proxyAnalytics(
|
|
7
|
+
request: Request,
|
|
8
|
+
fetcher: typeof fetch = fetch,
|
|
9
|
+
options?: AnalyticsOptions
|
|
10
|
+
) {
|
|
11
|
+
if (!options) return new Response('Not found', { status: 404 });
|
|
12
|
+
if (!/^[0-9]+$/.test(options.siteId))
|
|
13
|
+
return new Response('Invalid analytics configuration', { status: 503 });
|
|
14
|
+
let upstream: URL;
|
|
15
|
+
try {
|
|
16
|
+
upstream = new URL(options.endpoint);
|
|
17
|
+
if (
|
|
18
|
+
upstream.protocol !== 'https:' ||
|
|
19
|
+
upstream.username ||
|
|
20
|
+
upstream.password ||
|
|
21
|
+
upstream.search ||
|
|
22
|
+
upstream.hash
|
|
23
|
+
)
|
|
24
|
+
throw Error();
|
|
25
|
+
} catch {
|
|
26
|
+
return new Response('Invalid analytics configuration', { status: 503 });
|
|
27
|
+
}
|
|
28
|
+
const endpoints = new Map([
|
|
29
|
+
[
|
|
30
|
+
'/api/analytics/site/tracking-config/' + options.siteId,
|
|
31
|
+
{ method: 'GET', upstream: '/site/tracking-config/' + options.siteId }
|
|
32
|
+
],
|
|
33
|
+
['/api/analytics/track', { method: 'POST', upstream: '/track' }],
|
|
34
|
+
['/api/analytics/identify', { method: 'POST', upstream: '/identify' }]
|
|
35
|
+
]);
|
|
36
|
+
const url = new URL(request.url);
|
|
37
|
+
const endpoint = endpoints.get(url.pathname);
|
|
38
|
+
if (!endpoint) return new Response('Not found', { status: 404 });
|
|
39
|
+
if (request.method !== endpoint.method)
|
|
40
|
+
return new Response('Method not allowed', { status: 405, headers: { Allow: endpoint.method } });
|
|
41
|
+
const origin = request.headers.get('origin');
|
|
42
|
+
if ((origin && origin !== url.origin) || request.headers.get('sec-fetch-site') === 'cross-site')
|
|
43
|
+
return new Response('Forbidden', { status: 403 });
|
|
44
|
+
|
|
45
|
+
let body: string | undefined;
|
|
46
|
+
if (request.method === 'POST') {
|
|
47
|
+
try {
|
|
48
|
+
body = await request.text();
|
|
49
|
+
const payload = JSON.parse(body);
|
|
50
|
+
if (String(payload?.site_id) !== options.siteId)
|
|
51
|
+
return new Response('Invalid site', { status: 400 });
|
|
52
|
+
} catch {
|
|
53
|
+
return new Response('Invalid JSON', { status: 400 });
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Preserve browser metadata for bot detection, without forwarding site credentials.
|
|
58
|
+
const headers = new Headers();
|
|
59
|
+
for (const [name, value] of request.headers) {
|
|
60
|
+
if (
|
|
61
|
+
['accept', 'accept-encoding', 'accept-language', 'user-agent', 'referer', 'origin'].includes(
|
|
62
|
+
name
|
|
63
|
+
) ||
|
|
64
|
+
name.startsWith('sec-fetch-') ||
|
|
65
|
+
name.startsWith('sec-ch-')
|
|
66
|
+
)
|
|
67
|
+
headers.set(name, value);
|
|
68
|
+
}
|
|
69
|
+
if (body !== undefined) headers.set('Content-Type', 'application/json');
|
|
70
|
+
const ip = request.headers.get('cf-connecting-ip');
|
|
71
|
+
if (ip) headers.set('X-Forwarded-For', ip);
|
|
72
|
+
|
|
73
|
+
try {
|
|
74
|
+
const response = await fetcher(upstream.href.replace(/\/$/, '') + endpoint.upstream, {
|
|
75
|
+
method: endpoint.method,
|
|
76
|
+
headers,
|
|
77
|
+
body,
|
|
78
|
+
redirect: 'manual',
|
|
79
|
+
signal: AbortSignal.timeout(5000)
|
|
80
|
+
});
|
|
81
|
+
// workerd supports manual redirects, but not fetch's redirect: 'error' mode.
|
|
82
|
+
if (response.status >= 300 && response.status < 400)
|
|
83
|
+
return new Response('Analytics upstream redirected', {
|
|
84
|
+
status: 502,
|
|
85
|
+
headers: { 'Cache-Control': 'no-store' }
|
|
86
|
+
});
|
|
87
|
+
return new Response(response.body, {
|
|
88
|
+
status: response.status,
|
|
89
|
+
headers: {
|
|
90
|
+
'Content-Type': response.headers.get('Content-Type') ?? 'application/json',
|
|
91
|
+
'Cache-Control': 'no-store'
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
} catch {
|
|
95
|
+
return new Response('Analytics unavailable', {
|
|
96
|
+
status: 502,
|
|
97
|
+
headers: { 'Cache-Control': 'no-store' }
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import type { createAuth } from './auth';
|
|
2
|
+
import { loginPage, returnPath } from './login';
|
|
3
|
+
|
|
4
|
+
type Auth = ReturnType<typeof createAuth>;
|
|
5
|
+
|
|
6
|
+
const endpoints = new Set([
|
|
7
|
+
'/api/auth/sign-in/social',
|
|
8
|
+
'/api/auth/callback/github',
|
|
9
|
+
'/api/auth/get-session',
|
|
10
|
+
'/api/auth/sign-out',
|
|
11
|
+
'/api/auth/error'
|
|
12
|
+
]);
|
|
13
|
+
|
|
14
|
+
export function copyCookies(source: Headers, target: Headers) {
|
|
15
|
+
for (const cookie of source.getSetCookie()) target.append('Set-Cookie', cookie);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function redirect(location: string, cookies?: Headers) {
|
|
19
|
+
const headers = new Headers({ Location: location });
|
|
20
|
+
if (cookies) copyCookies(cookies, headers);
|
|
21
|
+
return new Response(null, { status: 303, headers });
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function handleAuthEndpoint(request: Request, auth: Auth) {
|
|
25
|
+
const { pathname } = new URL(request.url);
|
|
26
|
+
if (!endpoints.has(pathname)) return new Response('Not found', { status: 404 });
|
|
27
|
+
if (pathname === '/api/auth/error') return redirect('/login?error=1');
|
|
28
|
+
return auth.handler(request);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function sameOrigin(request: Request) {
|
|
32
|
+
return (
|
|
33
|
+
request.headers.get('origin') === new URL(request.url).origin &&
|
|
34
|
+
request.headers.get('sec-fetch-site') !== 'cross-site'
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function postAuth(request: Request, auth: Auth, path: string, body: object) {
|
|
39
|
+
const headers = new Headers(request.headers);
|
|
40
|
+
headers.set('Content-Type', 'application/json');
|
|
41
|
+
headers.delete('content-length');
|
|
42
|
+
return auth.handler(
|
|
43
|
+
new Request(new URL(path, request.url), {
|
|
44
|
+
method: 'POST',
|
|
45
|
+
headers,
|
|
46
|
+
body: JSON.stringify(body)
|
|
47
|
+
})
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function formError(response: Response, next: string) {
|
|
52
|
+
const page = loginPage(next, true);
|
|
53
|
+
const result = new Response(page.body, { status: response.status, headers: page.headers });
|
|
54
|
+
const retryAfter = response.headers.get('x-retry-after');
|
|
55
|
+
if (retryAfter) result.headers.set('Retry-After', retryAfter);
|
|
56
|
+
copyCookies(response.headers, result.headers);
|
|
57
|
+
return result;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export async function signIn(request: Request, auth: Auth) {
|
|
61
|
+
if (!sameOrigin(request)) return new Response('Forbidden', { status: 403 });
|
|
62
|
+
const next = returnPath((await request.formData()).get('next'));
|
|
63
|
+
const response = await postAuth(request, auth, '/api/auth/sign-in/social', {
|
|
64
|
+
provider: 'github',
|
|
65
|
+
callbackURL: next,
|
|
66
|
+
errorCallbackURL: '/login?error=1'
|
|
67
|
+
});
|
|
68
|
+
if (!response.ok) return formError(response, next);
|
|
69
|
+
const { url } = (await response.json()) as { url: string };
|
|
70
|
+
return redirect(url, response.headers);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export async function signOut(request: Request, auth: Auth) {
|
|
74
|
+
if (!sameOrigin(request)) return new Response('Forbidden', { status: 403 });
|
|
75
|
+
const response = await postAuth(request, auth, '/api/auth/sign-out', {});
|
|
76
|
+
if (!response.ok) return formError(response, '/login');
|
|
77
|
+
return redirect('/login', response.headers);
|
|
78
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { betterAuth } from 'better-auth/minimal';
|
|
2
|
+
import { drizzleAdapter } from '@better-auth/drizzle-adapter';
|
|
3
|
+
import { APIError } from 'better-auth/api';
|
|
4
|
+
import type { D1Database } from '@cloudflare/workers-types';
|
|
5
|
+
import { accessForUser, isGithubAllowed } from './access';
|
|
6
|
+
import { database } from '../database';
|
|
7
|
+
export { isOwner } from './access';
|
|
8
|
+
|
|
9
|
+
export interface AuthEnv {
|
|
10
|
+
DB: D1Database;
|
|
11
|
+
ORIGIN: string;
|
|
12
|
+
DISABLE_ANALYTICS?: string;
|
|
13
|
+
TWIKI_AUTH_MODE?: 'none' | 'github';
|
|
14
|
+
TWIKI_SITE_NAME?: string;
|
|
15
|
+
TWIKI_SITE_DESCRIPTION?: string;
|
|
16
|
+
TWIKI_ANALYTICS_SITE_ID?: string;
|
|
17
|
+
TWIKI_ANALYTICS_ENDPOINT?: string;
|
|
18
|
+
GITHUB_CLIENT_ID: string;
|
|
19
|
+
GITHUB_CLIENT_SECRET: string;
|
|
20
|
+
BETTER_AUTH_SECRET: string;
|
|
21
|
+
OWNER_GITHUB_ID: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function createAuth(env: AuthEnv) {
|
|
25
|
+
if (
|
|
26
|
+
!env.DB ||
|
|
27
|
+
!env.ORIGIN ||
|
|
28
|
+
!env.GITHUB_CLIENT_ID ||
|
|
29
|
+
!env.GITHUB_CLIENT_SECRET ||
|
|
30
|
+
!env.BETTER_AUTH_SECRET ||
|
|
31
|
+
env.BETTER_AUTH_SECRET.length < 32 ||
|
|
32
|
+
!/^\d+$/.test(env.OWNER_GITHUB_ID ?? '')
|
|
33
|
+
) {
|
|
34
|
+
throw new Error('Authentication configuration is incomplete');
|
|
35
|
+
}
|
|
36
|
+
return betterAuth({
|
|
37
|
+
appName: env.TWIKI_SITE_NAME ?? 'twiki',
|
|
38
|
+
baseURL: env.ORIGIN,
|
|
39
|
+
secret: env.BETTER_AUTH_SECRET,
|
|
40
|
+
database: drizzleAdapter(database(env.DB), { provider: 'sqlite' }),
|
|
41
|
+
trustedOrigins: [env.ORIGIN],
|
|
42
|
+
emailAndPassword: { enabled: false },
|
|
43
|
+
// OAuth mapping requires an input field; auth-routes.ts blocks client profile updates.
|
|
44
|
+
user: { additionalFields: { githubUsername: { type: 'string', required: false } } },
|
|
45
|
+
account: { accountLinking: { enabled: false }, encryptOAuthTokens: true },
|
|
46
|
+
session: { cookieCache: { enabled: false }, expiresIn: 60 * 60 * 24 * 7 },
|
|
47
|
+
rateLimit: { enabled: true, storage: 'database' },
|
|
48
|
+
advanced: {
|
|
49
|
+
useSecureCookies: new URL(env.ORIGIN).protocol === 'https:',
|
|
50
|
+
ipAddress: { ipAddressHeaders: ['cf-connecting-ip'] }
|
|
51
|
+
},
|
|
52
|
+
socialProviders: {
|
|
53
|
+
github: {
|
|
54
|
+
clientId: env.GITHUB_CLIENT_ID,
|
|
55
|
+
clientSecret: env.GITHUB_CLIENT_SECRET,
|
|
56
|
+
// Refresh renamed GitHub accounts and populate usernames for existing users.
|
|
57
|
+
overrideUserInfoOnSignIn: true,
|
|
58
|
+
async mapProfileToUser(profile) {
|
|
59
|
+
if (!(await isGithubAllowed(env, String(profile.id)))) {
|
|
60
|
+
throw new APIError('FORBIDDEN', { message: 'This account does not have access.' });
|
|
61
|
+
}
|
|
62
|
+
return { githubUsername: profile.login };
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
},
|
|
66
|
+
databaseHooks: {
|
|
67
|
+
account: {
|
|
68
|
+
create: {
|
|
69
|
+
before: async (account) =>
|
|
70
|
+
account.providerId === 'github' && (await isGithubAllowed(env, account.accountId))
|
|
71
|
+
}
|
|
72
|
+
},
|
|
73
|
+
session: {
|
|
74
|
+
create: { before: async (session) => Boolean(await accessForUser(env, session.userId)) }
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
}
|