@jant/core 0.3.46 → 0.3.47
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/bin/commands/db/execute-file.js +12 -4
- package/bin/commands/db/rehearse.js +2 -2
- package/bin/commands/export.js +12 -4
- package/bin/commands/import-site.js +60 -267
- package/bin/commands/migrate.js +36 -69
- package/bin/commands/reset-password.js +10 -4
- package/bin/commands/site/export.js +59 -248
- package/bin/commands/site/snapshot/export.js +58 -45
- package/bin/commands/site/snapshot/import.js +104 -52
- package/bin/lib/node-env.js +100 -0
- package/bin/lib/runtime-target.js +64 -0
- package/bin/lib/site-snapshot.js +185 -54
- package/bin/lib/sql-export.js +19 -2
- package/dist/{app-DB-P66E5.js → app-3REcR-3U.js} +331 -189
- package/dist/app-B67XOEyo.js +6 -0
- package/dist/client/.vite/manifest.json +2 -2
- package/dist/client/_assets/{client-auth-BLCUje4M.js → client-auth-Ce5WEAVS.js} +102 -49
- package/dist/client/_assets/client-s71Js1Cu.css +2 -0
- package/dist/{github-sync-CQ1x271f.js → export-ZBlfKSKm.js} +12 -439
- package/dist/github-sync-C593r22F.js +4 -0
- package/dist/github-sync-bL1hnx3Q.js +428 -0
- package/dist/index.js +3 -2
- package/dist/node.js +5 -4
- package/package.json +3 -2
- package/src/__tests__/helpers/export-fixtures.ts +0 -1
- package/src/client/components/__tests__/jant-settings-avatar.test.ts +2 -0
- package/src/client/components/__tests__/jant-settings-general.test.ts +70 -0
- package/src/client/components/jant-settings-general.ts +164 -22
- package/src/client/components/settings-types.ts +4 -6
- package/src/client-auth.ts +1 -1
- package/src/db/__tests__/demo-canonical-snapshot.test.ts +1 -1
- package/src/db/__tests__/migration-rehearsal.test.ts +2 -5
- package/src/db/backfills/0004_register_apple_touch_media_rows.sql +65 -0
- package/src/db/migrations/0021_thankful_phalanx.sql +16 -0
- package/src/db/migrations/meta/0021_snapshot.json +2121 -0
- package/src/db/migrations/meta/_journal.json +7 -0
- package/src/db/migrations/pg/0019_gray_natasha_romanoff.sql +20 -0
- package/src/db/migrations/pg/meta/0019_snapshot.json +2718 -0
- package/src/db/migrations/pg/meta/_journal.json +7 -0
- package/src/db/pg/schema.ts +21 -26
- package/src/db/rehearsal-fixtures/demo-current.json +1 -1
- package/src/db/schema.ts +16 -20
- package/src/i18n/__tests__/middleware.test.ts +43 -1
- package/src/i18n/coverage.generated.ts +17 -0
- package/src/i18n/i18n.ts +18 -2
- package/src/i18n/index.ts +3 -0
- package/src/i18n/locales/settings/en.po +16 -11
- package/src/i18n/locales/settings/en.ts +1 -1
- package/src/i18n/locales/settings/zh-Hans.po +17 -12
- package/src/i18n/locales/settings/zh-Hans.ts +1 -1
- package/src/i18n/locales/settings/zh-Hant.po +16 -11
- package/src/i18n/locales/settings/zh-Hant.ts +1 -1
- package/src/i18n/locales.ts +84 -2
- package/src/i18n/middleware.ts +25 -16
- package/src/i18n/supported-locales.ts +153 -0
- package/src/lib/__tests__/csp-builder.test.ts +19 -2
- package/src/lib/__tests__/feed.test.ts +242 -1
- package/src/lib/__tests__/post-meta.test.ts +0 -1
- package/src/lib/__tests__/view.test.ts +0 -1
- package/src/lib/csp-builder.ts +28 -10
- package/src/lib/feed.ts +153 -3
- package/src/middleware/__tests__/secure-headers.test.ts +89 -0
- package/src/middleware/auth.ts +1 -1
- package/src/middleware/secure-headers.ts +47 -1
- package/src/node/__tests__/cli-runtime-target.test.ts +110 -2
- package/src/node/__tests__/cli-site-snapshot.test.ts +308 -13
- package/src/node/__tests__/cli-site-token-env.test.ts +2 -7
- package/src/node/__tests__/cli-snapshot-meta.test.ts +85 -0
- package/src/node/__tests__/cli-sql-export.test.ts +49 -0
- package/src/node/index.ts +1 -0
- package/src/preset.css +8 -2
- package/src/routes/api/__tests__/settings.test.ts +3 -2
- package/src/routes/api/github-sync.tsx +1 -1
- package/src/routes/api/settings.ts +4 -1
- package/src/routes/auth/signin.tsx +6 -0
- package/src/routes/pages/archive.tsx +4 -2
- package/src/services/__tests__/post.test.ts +19 -19
- package/src/services/__tests__/search.test.ts +0 -1
- package/src/services/__tests__/settings.test.ts +22 -3
- package/src/services/bootstrap.ts +7 -3
- package/src/services/collection.ts +3 -3
- package/src/services/export.ts +0 -3
- package/src/services/navigation.ts +0 -2
- package/src/services/path.ts +1 -38
- package/src/services/post.ts +32 -66
- package/src/services/search.ts +0 -6
- package/src/services/settings.ts +47 -6
- package/src/services/site-admin.ts +6 -1
- package/src/styles/ui.css +12 -23
- package/src/types/entities.ts +0 -1
- package/src/ui/color-themes.ts +1 -1
- package/src/ui/dash/settings/GeneralContent.tsx +17 -19
- package/src/ui/dash/settings/SettingsRootContent.tsx +17 -28
- package/src/ui/feed/NoteCard.tsx +1 -11
- package/src/ui/feed/__tests__/timeline-cards.test.ts +1 -1
- package/src/ui/pages/PostPage.tsx +2 -0
- package/bin/commands/collections.js +0 -268
- package/bin/commands/media.js +0 -302
- package/bin/commands/posts.js +0 -262
- package/bin/commands/search.js +0 -53
- package/bin/commands/settings.js +0 -93
- package/bin/lib/http-api.js +0 -223
- package/bin/lib/media-upload.js +0 -206
- package/dist/app-CM7sb3xO.js +0 -5
- package/dist/client/_assets/client-DDs6NzB3.css +0 -2
- package/src/__tests__/bin/content-cli.test.ts +0 -179
- package/src/__tests__/bin/media-cli.test.ts +0 -192
- /package/dist/{github-api-BkRWnqMx.js → github-api-Bh0PH3zr.js} +0 -0
- /package/dist/{github-app-WeadXMb8.js → github-app-D0GvNnqp.js} +0 -0
|
@@ -1,6 +1,4 @@
|
|
|
1
1
|
import { d as sanitizeUrl, g as toPublicPath, v as __exportAll } from "./url-umUptr5z.js";
|
|
2
|
-
import { r as getInstallationToken } from "./github-app-WeadXMb8.js";
|
|
3
|
-
import { r as parseRepoSlug, t as createGitHubClient } from "./github-api-BkRWnqMx.js";
|
|
4
2
|
import { strToU8, zipSync } from "fflate";
|
|
5
3
|
import { Extension, Node } from "@tiptap/core";
|
|
6
4
|
import { MarkdownManager } from "@tiptap/markdown";
|
|
@@ -3125,28 +3123,6 @@ function serializeMarkdownDocument(doc) {
|
|
|
3125
3123
|
return plain.slice(0, maxLength).trim() + "...";
|
|
3126
3124
|
}
|
|
3127
3125
|
//#endregion
|
|
3128
|
-
//#region src/lib/markdown-to-tiptap.ts
|
|
3129
|
-
/**
|
|
3130
|
-
* Markdown → TipTap JSON Conversion
|
|
3131
|
-
*
|
|
3132
|
-
* Converts Markdown strings to TipTap JSON documents using the official
|
|
3133
|
-
* Tiptap MarkdownManager and the same extension schema used elsewhere in Jant.
|
|
3134
|
-
*/
|
|
3135
|
-
/**
|
|
3136
|
-
* Converts a Markdown string to a TipTap JSON document string.
|
|
3137
|
-
*
|
|
3138
|
-
* @param markdown - Markdown source text
|
|
3139
|
-
* @returns Stringified TipTap JSON document
|
|
3140
|
-
*
|
|
3141
|
-
* @example
|
|
3142
|
-
* ```ts
|
|
3143
|
-
* const json = markdownToTiptapJson("Hello **world**");
|
|
3144
|
-
* // '{"type":"doc","content":[{"type":"paragraph","content":[...]}]}'
|
|
3145
|
-
* ```
|
|
3146
|
-
*/ function markdownToTiptapJson(markdown) {
|
|
3147
|
-
return JSON.stringify(parseMarkdownDocument(markdown));
|
|
3148
|
-
}
|
|
3149
|
-
//#endregion
|
|
3150
3126
|
//#region src/lib/tiptap-to-markdown.ts
|
|
3151
3127
|
/**
|
|
3152
3128
|
* Tiptap JSON → Markdown Converter
|
|
@@ -3388,7 +3364,17 @@ var feed_post_content_default = "{{- /*\n Feed entry content builder — mirror
|
|
|
3388
3364
|
*
|
|
3389
3365
|
* Real Hugo templates and CSS are scaffolded as placeholders here and
|
|
3390
3366
|
* filled in by Commit 5.
|
|
3391
|
-
*/
|
|
3367
|
+
*/ var export_exports = /* @__PURE__ */ __exportAll({
|
|
3368
|
+
buildExportedCollectionDirectoryItems: () => buildExportedCollectionDirectoryItems,
|
|
3369
|
+
buildExportedCollectionMetrics: () => buildExportedCollectionMetrics,
|
|
3370
|
+
buildSiteIconAssets: () => buildSiteIconAssets,
|
|
3371
|
+
createExportService: () => createExportService,
|
|
3372
|
+
getArchiveSummaryText: () => getArchiveSummaryText,
|
|
3373
|
+
getMediaUrl: () => getMediaUrl,
|
|
3374
|
+
getPublicUrlForProvider: () => getPublicUrlForProvider,
|
|
3375
|
+
readStorageObjectBytes: () => readStorageObjectBytes
|
|
3376
|
+
});
|
|
3377
|
+
function buildDefaultAppleTouchAsset() {
|
|
3392
3378
|
return {
|
|
3393
3379
|
appleTouchBytes: getDefaultJantAppleTouchIconBytes(),
|
|
3394
3380
|
appleTouchMode: "default"
|
|
@@ -4023,7 +4009,6 @@ function buildExportedCollectionMetrics(collections, posts, collectionsByPost) {
|
|
|
4023
4009
|
recentActivityAt: collection.updatedAt
|
|
4024
4010
|
});
|
|
4025
4011
|
for (const post of posts) {
|
|
4026
|
-
if (post.deletedAt !== null) continue;
|
|
4027
4012
|
if (post.status === "draft" || post.visibility === "private") continue;
|
|
4028
4013
|
if (post.replyToId !== null) continue;
|
|
4029
4014
|
const activityAt = post.lastActivityAt ?? post.publishedAt ?? post.updatedAt ?? post.createdAt;
|
|
@@ -4311,416 +4296,4 @@ Safe to re-run; files already on disk are reused. Anything that fails to downloa
|
|
|
4311
4296
|
`;
|
|
4312
4297
|
}
|
|
4313
4298
|
//#endregion
|
|
4314
|
-
|
|
4315
|
-
/**
|
|
4316
|
-
* GitHub Sync Service
|
|
4317
|
-
*
|
|
4318
|
-
* Handles bidirectional content synchronization between Jant and a GitHub repo.
|
|
4319
|
-
* Posts are serialized as Hugo-format Markdown bundles (reusing the export
|
|
4320
|
-
* format): each post is a branch bundle at `content/{slug}/_index.md` with
|
|
4321
|
-
* reply leaves at `content/{root-slug}/{reply-slug}/index.md`.
|
|
4322
|
-
*
|
|
4323
|
-
* Push (Jant → GitHub):
|
|
4324
|
-
* - Always full sync: regenerate Jant-managed files in a single atomic commit
|
|
4325
|
-
* - Uses base_tree so untracked files in the repo (READMEs, CI, etc.) are preserved
|
|
4326
|
-
* - Debounced: multiple rapid changes collapse into one sync
|
|
4327
|
-
*
|
|
4328
|
-
* Pull (GitHub → Jant):
|
|
4329
|
-
* - Webhook-triggered: match modified files to existing posts by slug and update
|
|
4330
|
-
* - Unknown files are skipped; new posts cannot be created from GitHub
|
|
4331
|
-
* - File deletions are intentionally ignored to avoid catastrophic data loss
|
|
4332
|
-
* (e.g. user deletes the repo → site wiped). Deletes must go through Jant's UI.
|
|
4333
|
-
*
|
|
4334
|
-
* Anti-loop: all commits from Jant include `[jant-sync]` in the message.
|
|
4335
|
-
* Incoming webhooks with this marker are skipped.
|
|
4336
|
-
*/ var github_sync_exports = /* @__PURE__ */ __exportAll({
|
|
4337
|
-
JANT_MANAGED_GLOBS: () => JANT_MANAGED_GLOBS,
|
|
4338
|
-
JANT_SYNC_MARKER_PATH: () => JANT_SYNC_MARKER_PATH,
|
|
4339
|
-
JANT_SYNC_MARKER_SCHEMA_VERSION: () => 3,
|
|
4340
|
-
SYNC_COMMIT_MARKER: () => SYNC_COMMIT_MARKER,
|
|
4341
|
-
classifyRepoForSync: () => classifyRepoForSync,
|
|
4342
|
-
computeManagedDeletions: () => computeManagedDeletions,
|
|
4343
|
-
createGitHubSyncService: () => createGitHubSyncService,
|
|
4344
|
-
isManagedPath: () => isManagedPath,
|
|
4345
|
-
pathMatchesManagedGlob: () => pathMatchesManagedGlob
|
|
4346
|
-
});
|
|
4347
|
-
/** Marker included in commit messages to prevent webhook loops. */ var SYNC_COMMIT_MARKER = "[jant-sync]";
|
|
4348
|
-
/**
|
|
4349
|
-
* Path of the ownership marker file written at the repo root.
|
|
4350
|
-
*
|
|
4351
|
-
* Presence of this file (with a matching `site_id`) identifies the repo
|
|
4352
|
-
* as actively managed by a Jant site. Used to distinguish three states
|
|
4353
|
-
* during connect: empty repo, Jant-owned repo, foreign repo with existing
|
|
4354
|
-
* content. Also serves as the initialization file for empty repos so we
|
|
4355
|
-
* don't need a separate throwaway placeholder.
|
|
4356
|
-
*/ var JANT_SYNC_MARKER_PATH = ".jant-sync";
|
|
4357
|
-
/**
|
|
4358
|
-
* Hard list of paths Jant fully owns and always overwrites on push.
|
|
4359
|
-
* Anything outside this set is user territory and preserved via base_tree.
|
|
4360
|
-
* Files inside this set that Jant no longer generates are deleted on the
|
|
4361
|
-
* next push (see `computeManagedDeletions`).
|
|
4362
|
-
*
|
|
4363
|
-
* - `content/**` — posts, collections, sections (rendered by Hugo)
|
|
4364
|
-
* - `data/jant.toml` — nav, branding, collections directory (the rest of
|
|
4365
|
-
* `data/` is user territory so Hugo's `data/menu.toml` convention, etc.,
|
|
4366
|
-
* can be used freely)
|
|
4367
|
-
* - `themes/jant/**` — the packaged Jant theme (layouts + static assets)
|
|
4368
|
-
* - `hugo.toml` — site config, including `theme = "jant"`
|
|
4369
|
-
* - `.gitignore`, `README.md` — scaffolded once, then kept in sync
|
|
4370
|
-
* - `.jant-sync` — ownership marker; written by this service, not by export
|
|
4371
|
-
*
|
|
4372
|
-
* The list is also stored in the marker itself (`managed_globs`) so future
|
|
4373
|
-
* schema bumps can diff the old and new sets to decide what needs cleanup.
|
|
4374
|
-
*/ var JANT_MANAGED_GLOBS = [
|
|
4375
|
-
"content/**",
|
|
4376
|
-
"data/jant.toml",
|
|
4377
|
-
"themes/jant/**",
|
|
4378
|
-
"hugo.toml",
|
|
4379
|
-
".gitignore",
|
|
4380
|
-
"README.md",
|
|
4381
|
-
".jant-sync"
|
|
4382
|
-
];
|
|
4383
|
-
/**
|
|
4384
|
-
* Match a repo-relative path against a single glob from
|
|
4385
|
-
* `JANT_MANAGED_GLOBS`. Only two forms are supported because those are
|
|
4386
|
-
* the only two shapes the constant uses:
|
|
4387
|
-
*
|
|
4388
|
-
* - Exact path (`"hugo.toml"`, `"data/jant.toml"`)
|
|
4389
|
-
* - Directory prefix + `/**` (`"content/**"`, `"themes/jant/**"`)
|
|
4390
|
-
*
|
|
4391
|
-
* Exported for testing.
|
|
4392
|
-
*/ function pathMatchesManagedGlob(path, glob) {
|
|
4393
|
-
if (glob.endsWith("/**")) {
|
|
4394
|
-
const prefix = glob.slice(0, -3);
|
|
4395
|
-
return path === prefix || path.startsWith(prefix + "/");
|
|
4396
|
-
}
|
|
4397
|
-
return path === glob;
|
|
4398
|
-
}
|
|
4399
|
-
/**
|
|
4400
|
-
* True when `path` falls inside one of Jant's managed globs.
|
|
4401
|
-
*
|
|
4402
|
-
* Exported for testing.
|
|
4403
|
-
*/ function isManagedPath(path) {
|
|
4404
|
-
return JANT_MANAGED_GLOBS.some((g) => pathMatchesManagedGlob(path, g));
|
|
4405
|
-
}
|
|
4406
|
-
/**
|
|
4407
|
-
* Compute the tree items that should null-out files on the remote HEAD
|
|
4408
|
-
* which Jant claims ownership of (matches `JANT_MANAGED_GLOBS`) but is
|
|
4409
|
-
* not writing in the current push (not in `writtenPaths`).
|
|
4410
|
-
*
|
|
4411
|
-
* Returns a list of `{ sha: null }` tree entries suitable for appending
|
|
4412
|
-
* to the payload of `createTree`. Exported for testing.
|
|
4413
|
-
*/ function computeManagedDeletions(headTreeItems, writtenPaths) {
|
|
4414
|
-
const items = [];
|
|
4415
|
-
for (const item of headTreeItems) {
|
|
4416
|
-
if (item.type !== "blob") continue;
|
|
4417
|
-
if (!isManagedPath(item.path)) continue;
|
|
4418
|
-
if (writtenPaths.has(item.path)) continue;
|
|
4419
|
-
items.push({
|
|
4420
|
-
path: item.path,
|
|
4421
|
-
mode: "100644",
|
|
4422
|
-
type: "blob",
|
|
4423
|
-
sha: null
|
|
4424
|
-
});
|
|
4425
|
-
}
|
|
4426
|
-
return items;
|
|
4427
|
-
}
|
|
4428
|
-
function parseMarker(text) {
|
|
4429
|
-
try {
|
|
4430
|
-
const parsed = JSON.parse(text);
|
|
4431
|
-
if (typeof parsed.site_id === "string" && typeof parsed.site_host === "string" && typeof parsed.created_at === "number" && typeof parsed.schema_version === "number") return parsed;
|
|
4432
|
-
} catch {}
|
|
4433
|
-
return null;
|
|
4434
|
-
}
|
|
4435
|
-
function decodeMarkerContent(file) {
|
|
4436
|
-
if (file.encoding === "base64") try {
|
|
4437
|
-
const cleaned = file.content.replace(/\s+/g, "");
|
|
4438
|
-
const binary = atob(cleaned);
|
|
4439
|
-
const bytes = Uint8Array.from(binary, (ch) => ch.charCodeAt(0));
|
|
4440
|
-
return new TextDecoder("utf-8").decode(bytes);
|
|
4441
|
-
} catch {
|
|
4442
|
-
return "";
|
|
4443
|
-
}
|
|
4444
|
-
return file.content;
|
|
4445
|
-
}
|
|
4446
|
-
function formatMarker(marker) {
|
|
4447
|
-
return `${JSON.stringify(marker, null, 2)}\n`;
|
|
4448
|
-
}
|
|
4449
|
-
function safeHost(siteUrl) {
|
|
4450
|
-
try {
|
|
4451
|
-
return new URL(siteUrl).host;
|
|
4452
|
-
} catch {
|
|
4453
|
-
return "";
|
|
4454
|
-
}
|
|
4455
|
-
}
|
|
4456
|
-
/**
|
|
4457
|
-
* Classify a repository to decide how to proceed with a sync connection.
|
|
4458
|
-
*
|
|
4459
|
-
* - `empty`: repo has no commits on the default branch (or repo is brand new).
|
|
4460
|
-
* - `owned`: `.jant-sync` marker present with matching `site_id`.
|
|
4461
|
-
* - `owned-by-other-site`: `.jant-sync` present but `site_id` differs —
|
|
4462
|
-
* another Jant site already backs up here, blocking the connect.
|
|
4463
|
-
* - `foreign`: non-empty repo without a marker — requires explicit
|
|
4464
|
-
* user confirmation before connect.
|
|
4465
|
-
*/ async function classifyRepoForSync(client, owner, repo, siteId) {
|
|
4466
|
-
const defaultBranch = (await client.getRepo(owner, repo)).default_branch;
|
|
4467
|
-
try {
|
|
4468
|
-
await client.getRef(owner, repo, `heads/${defaultBranch}`);
|
|
4469
|
-
} catch {
|
|
4470
|
-
return { kind: "empty" };
|
|
4471
|
-
}
|
|
4472
|
-
const markerFile = await client.getFileContent(owner, repo, JANT_SYNC_MARKER_PATH);
|
|
4473
|
-
if (!markerFile) return {
|
|
4474
|
-
kind: "foreign",
|
|
4475
|
-
defaultBranch
|
|
4476
|
-
};
|
|
4477
|
-
const marker = parseMarker(decodeMarkerContent(markerFile));
|
|
4478
|
-
if (!marker) return {
|
|
4479
|
-
kind: "foreign",
|
|
4480
|
-
defaultBranch
|
|
4481
|
-
};
|
|
4482
|
-
if (marker.site_id === siteId) return {
|
|
4483
|
-
kind: "owned",
|
|
4484
|
-
marker
|
|
4485
|
-
};
|
|
4486
|
-
return {
|
|
4487
|
-
kind: "owned-by-other-site",
|
|
4488
|
-
marker
|
|
4489
|
-
};
|
|
4490
|
-
}
|
|
4491
|
-
function createGitHubSyncService(services, siteId, siteConfig, deps = {}) {
|
|
4492
|
-
/**
|
|
4493
|
-
* Build the ownership marker for this push. Preserves `created_at`
|
|
4494
|
-
* from an existing marker (when readable) so the timestamp reflects
|
|
4495
|
-
* when this repo was first bound to this site, not the latest push.
|
|
4496
|
-
*/ function buildMarker(existingContent, now) {
|
|
4497
|
-
const existing = existingContent ? parseMarker(existingContent) : null;
|
|
4498
|
-
const preservedCreatedAt = existing && existing.site_id === siteId ? existing.created_at : now;
|
|
4499
|
-
return {
|
|
4500
|
-
schema_version: 3,
|
|
4501
|
-
site_id: siteId,
|
|
4502
|
-
site_host: safeHost(siteConfig.siteUrl),
|
|
4503
|
-
created_at: preservedCreatedAt,
|
|
4504
|
-
managed_globs: [...JANT_MANAGED_GLOBS]
|
|
4505
|
-
};
|
|
4506
|
-
}
|
|
4507
|
-
async function loadConfig() {
|
|
4508
|
-
const [repo, enabled, authModeRaw] = await Promise.all([
|
|
4509
|
-
services.settings.get("GITHUB_SYNC_REPO"),
|
|
4510
|
-
services.settings.get("GITHUB_SYNC_ENABLED"),
|
|
4511
|
-
services.settings.get("GITHUB_SYNC_AUTH_MODE")
|
|
4512
|
-
]);
|
|
4513
|
-
if (!repo || enabled !== "true") return null;
|
|
4514
|
-
const authMode = authModeRaw === "app" ? "app" : "pat";
|
|
4515
|
-
const [token, installationId, webhookId, webhookSecret, lastPushSha] = await Promise.all([
|
|
4516
|
-
services.settings.get("GITHUB_SYNC_TOKEN"),
|
|
4517
|
-
services.settings.get("GITHUB_SYNC_APP_INSTALLATION_ID"),
|
|
4518
|
-
services.settings.get("GITHUB_SYNC_WEBHOOK_ID"),
|
|
4519
|
-
services.settings.get("GITHUB_SYNC_WEBHOOK_SECRET"),
|
|
4520
|
-
services.settings.get("GITHUB_SYNC_LAST_PUSH_SHA")
|
|
4521
|
-
]);
|
|
4522
|
-
if (authMode === "pat" && !token) return null;
|
|
4523
|
-
if (authMode === "app" && !installationId) return null;
|
|
4524
|
-
return {
|
|
4525
|
-
authMode,
|
|
4526
|
-
token: token ?? void 0,
|
|
4527
|
-
installationId: installationId ?? void 0,
|
|
4528
|
-
repo,
|
|
4529
|
-
enabled: true,
|
|
4530
|
-
webhookId: webhookId ?? void 0,
|
|
4531
|
-
webhookSecret: webhookSecret ?? void 0,
|
|
4532
|
-
lastPushSha: lastPushSha ?? void 0
|
|
4533
|
-
};
|
|
4534
|
-
}
|
|
4535
|
-
function createClient(config) {
|
|
4536
|
-
const parsed = parseRepoSlug(config.repo);
|
|
4537
|
-
if (!parsed) throw new Error(`Invalid repo slug: ${config.repo}`);
|
|
4538
|
-
let client;
|
|
4539
|
-
if (config.authMode === "app") {
|
|
4540
|
-
if (!deps.githubApp) throw new Error("GitHub App is not configured on this deployment. Set GITHUB_APP_ID, GITHUB_APP_PRIVATE_KEY, and GITHUB_APP_SLUG to use App auth.");
|
|
4541
|
-
if (!config.installationId) throw new Error("GitHub App installation id is missing.");
|
|
4542
|
-
const app = deps.githubApp;
|
|
4543
|
-
const installationId = config.installationId;
|
|
4544
|
-
client = createGitHubClient(() => getInstallationToken(app, installationId));
|
|
4545
|
-
} else {
|
|
4546
|
-
if (!config.token) throw new Error("GitHub sync PAT is missing.");
|
|
4547
|
-
client = createGitHubClient(config.token);
|
|
4548
|
-
}
|
|
4549
|
-
return {
|
|
4550
|
-
client,
|
|
4551
|
-
owner: parsed.owner,
|
|
4552
|
-
repo: parsed.repo
|
|
4553
|
-
};
|
|
4554
|
-
}
|
|
4555
|
-
/**
|
|
4556
|
-
* Get the HEAD SHA, initializing an empty repo if needed.
|
|
4557
|
-
* GitHub's Git Trees API requires at least one commit to exist.
|
|
4558
|
-
*/ async function getOrInitHead(client, owner, repo, defaultBranch, seedMarker) {
|
|
4559
|
-
try {
|
|
4560
|
-
return { sha: (await client.getRef(owner, repo, `heads/${defaultBranch}`)).sha };
|
|
4561
|
-
} catch {
|
|
4562
|
-
await client.createOrUpdateFile(owner, repo, JANT_SYNC_MARKER_PATH, {
|
|
4563
|
-
content: formatMarker(seedMarker),
|
|
4564
|
-
message: `Initialize Jant sync ${SYNC_COMMIT_MARKER}`
|
|
4565
|
-
});
|
|
4566
|
-
return { sha: (await client.getRef(owner, repo, `heads/${defaultBranch}`)).sha };
|
|
4567
|
-
}
|
|
4568
|
-
}
|
|
4569
|
-
return {
|
|
4570
|
-
getConfig: loadConfig,
|
|
4571
|
-
async pushFullSync() {
|
|
4572
|
-
const config = await loadConfig();
|
|
4573
|
-
if (!config) throw new Error("GitHub Sync is not configured");
|
|
4574
|
-
const { client, owner, repo } = createClient(config);
|
|
4575
|
-
const exportFiles = await createExportService(services, siteConfig, deps).generateHugoFiles();
|
|
4576
|
-
const defaultBranch = (await client.getRepo(owner, repo)).default_branch;
|
|
4577
|
-
const now = Math.floor(Date.now() / 1e3);
|
|
4578
|
-
const existingMarkerBeforeInit = await client.getFileContent(owner, repo, JANT_SYNC_MARKER_PATH).catch(() => null);
|
|
4579
|
-
const marker = buildMarker(existingMarkerBeforeInit ? decodeMarkerContent(existingMarkerBeforeInit) : null, now);
|
|
4580
|
-
const { sha: headSha } = await getOrInitHead(client, owner, repo, defaultBranch, marker);
|
|
4581
|
-
const treeItems = [{
|
|
4582
|
-
path: JANT_SYNC_MARKER_PATH,
|
|
4583
|
-
mode: "100644",
|
|
4584
|
-
type: "blob",
|
|
4585
|
-
content: formatMarker(marker)
|
|
4586
|
-
}];
|
|
4587
|
-
for (const file of exportFiles) if (typeof file.content === "string") treeItems.push({
|
|
4588
|
-
path: file.path,
|
|
4589
|
-
mode: "100644",
|
|
4590
|
-
type: "blob",
|
|
4591
|
-
content: file.content
|
|
4592
|
-
});
|
|
4593
|
-
else {
|
|
4594
|
-
const blob = await client.createBlob(owner, repo, uint8ArrayToBase64(file.content), "base64");
|
|
4595
|
-
treeItems.push({
|
|
4596
|
-
path: file.path,
|
|
4597
|
-
mode: "100644",
|
|
4598
|
-
type: "blob",
|
|
4599
|
-
sha: blob.sha
|
|
4600
|
-
});
|
|
4601
|
-
}
|
|
4602
|
-
const headCommit = await client.getCommit(owner, repo, headSha);
|
|
4603
|
-
const headTree = await client.getTree(owner, repo, headCommit.treeSha, { recursive: true });
|
|
4604
|
-
if (headTree.truncated) throw new Error("GitHub tree exceeds API limits (>100k entries or >7MB); incremental deletion cannot run safely against this repo.");
|
|
4605
|
-
const writtenPaths = new Set(treeItems.map((item) => item.path));
|
|
4606
|
-
treeItems.push(...computeManagedDeletions(headTree.tree, writtenPaths));
|
|
4607
|
-
const tree = await client.createTree(owner, repo, treeItems, headCommit.treeSha);
|
|
4608
|
-
const commit = await client.createCommit(owner, repo, {
|
|
4609
|
-
message: `Sync site ${SYNC_COMMIT_MARKER}`,
|
|
4610
|
-
tree: tree.sha,
|
|
4611
|
-
parents: [headSha]
|
|
4612
|
-
});
|
|
4613
|
-
await client.updateRef(owner, repo, `heads/${defaultBranch}`, commit.sha);
|
|
4614
|
-
await services.settings.set("GITHUB_SYNC_LAST_PUSH_SHA", commit.sha);
|
|
4615
|
-
await services.settings.set("GITHUB_SYNC_LAST_PUSH_AT", String(Math.floor(Date.now() / 1e3)));
|
|
4616
|
-
return { commitSha: commit.sha };
|
|
4617
|
-
},
|
|
4618
|
-
async handleWebhookPush(payload) {
|
|
4619
|
-
if (payload.commits.some((c) => c.message.includes("[jant-sync]")) && payload.commits.length === 1) return;
|
|
4620
|
-
const modified = /* @__PURE__ */ new Set();
|
|
4621
|
-
for (const commit of payload.commits) {
|
|
4622
|
-
if (commit.message.includes("[jant-sync]")) continue;
|
|
4623
|
-
for (const file of [...commit.modified, ...commit.added]) if (classifyBundlePath(file)) modified.add(file);
|
|
4624
|
-
}
|
|
4625
|
-
if (modified.size === 0) return;
|
|
4626
|
-
const config = await loadConfig();
|
|
4627
|
-
if (!config) return;
|
|
4628
|
-
const { client, owner, repo } = createClient(config);
|
|
4629
|
-
for (const filePath of modified) {
|
|
4630
|
-
const classification = classifyBundlePath(filePath);
|
|
4631
|
-
if (!classification) continue;
|
|
4632
|
-
const fileContent = await client.getFileContent(owner, repo, filePath, payload.after);
|
|
4633
|
-
if (!fileContent) continue;
|
|
4634
|
-
const { frontMatter, body } = await parseFrontMatter(decodeBase64Content(fileContent.content));
|
|
4635
|
-
const slug = typeof frontMatter.slug === "string" && frontMatter.slug.trim() ? frontMatter.slug.trim() : classification.slug;
|
|
4636
|
-
if (!slug) continue;
|
|
4637
|
-
const pathRecord = await services.paths.getByPath(slug);
|
|
4638
|
-
if (!pathRecord?.postId) continue;
|
|
4639
|
-
const existingPost = await services.posts.getById(pathRecord.postId);
|
|
4640
|
-
if (!existingPost) continue;
|
|
4641
|
-
if (classification.kind === "reply") {
|
|
4642
|
-
const rootPath = await services.paths.getByPath(classification.rootSlug);
|
|
4643
|
-
if (!rootPath?.postId) continue;
|
|
4644
|
-
if (existingPost.threadId !== rootPath.postId) continue;
|
|
4645
|
-
}
|
|
4646
|
-
const trimmedBody = body.trim();
|
|
4647
|
-
const tiptapBody = trimmedBody ? markdownToTiptapJson(trimmedBody) : null;
|
|
4648
|
-
const updateData = {};
|
|
4649
|
-
if (tiptapBody !== null) updateData.body = tiptapBody;
|
|
4650
|
-
if (frontMatter.title !== void 0) updateData.title = frontMatter.title;
|
|
4651
|
-
if (frontMatter.link_url !== void 0) updateData.url = frontMatter.link_url;
|
|
4652
|
-
if (frontMatter.source_name !== void 0) updateData.sourceName = frontMatter.source_name;
|
|
4653
|
-
if (frontMatter.source_url !== void 0) updateData.sourceUrl = frontMatter.source_url;
|
|
4654
|
-
if (frontMatter.quote_text !== void 0) updateData.quoteText = frontMatter.quote_text;
|
|
4655
|
-
if (frontMatter.rating !== void 0) updateData.rating = frontMatter.rating;
|
|
4656
|
-
if (Object.keys(updateData).length > 0) await services.posts.update(existingPost.id, updateData);
|
|
4657
|
-
}
|
|
4658
|
-
},
|
|
4659
|
-
async setupWebhook(callbackUrl) {
|
|
4660
|
-
const config = await loadConfig();
|
|
4661
|
-
if (!config) throw new Error("GitHub Sync is not configured");
|
|
4662
|
-
const { client, owner, repo } = createClient(config);
|
|
4663
|
-
const secretBytes = new Uint8Array(32);
|
|
4664
|
-
crypto.getRandomValues(secretBytes);
|
|
4665
|
-
const secret = Array.from(secretBytes).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
4666
|
-
const webhook = await client.createWebhook(owner, repo, {
|
|
4667
|
-
url: callbackUrl,
|
|
4668
|
-
secret,
|
|
4669
|
-
events: ["push"]
|
|
4670
|
-
});
|
|
4671
|
-
await services.settings.set("GITHUB_SYNC_WEBHOOK_SECRET", secret);
|
|
4672
|
-
await services.settings.set("GITHUB_SYNC_WEBHOOK_ID", String(webhook.id));
|
|
4673
|
-
return { webhookId: webhook.id };
|
|
4674
|
-
},
|
|
4675
|
-
async teardownWebhook() {
|
|
4676
|
-
const config = await loadConfig();
|
|
4677
|
-
if (!config) return;
|
|
4678
|
-
if (config.webhookId) try {
|
|
4679
|
-
const { client, owner, repo } = createClient(config);
|
|
4680
|
-
await client.deleteWebhook(owner, repo, parseInt(config.webhookId));
|
|
4681
|
-
} catch {}
|
|
4682
|
-
await services.settings.set("GITHUB_SYNC_ENABLED", "false");
|
|
4683
|
-
await services.settings.set("GITHUB_SYNC_TOKEN", "");
|
|
4684
|
-
await services.settings.set("GITHUB_SYNC_REPO", "");
|
|
4685
|
-
await services.settings.set("GITHUB_SYNC_WEBHOOK_SECRET", "");
|
|
4686
|
-
await services.settings.set("GITHUB_SYNC_WEBHOOK_ID", "");
|
|
4687
|
-
await services.settings.set("GITHUB_SYNC_LAST_PUSH_SHA", "");
|
|
4688
|
-
await services.settings.set("GITHUB_SYNC_AUTH_MODE", "pat");
|
|
4689
|
-
await services.settings.set("GITHUB_SYNC_APP_INSTALLATION_ID", "");
|
|
4690
|
-
}
|
|
4691
|
-
};
|
|
4692
|
-
}
|
|
4693
|
-
function decodeBase64Content(content) {
|
|
4694
|
-
const cleaned = content.replace(/\n/g, "");
|
|
4695
|
-
return decodeURIComponent(escape(atob(cleaned)));
|
|
4696
|
-
}
|
|
4697
|
-
function classifyBundlePath(path) {
|
|
4698
|
-
if (!path.startsWith("content/")) return null;
|
|
4699
|
-
const segments = path.slice(8).split("/");
|
|
4700
|
-
if (segments.length === 2 && segments[1] === "_index.md") {
|
|
4701
|
-
const slug = segments[0];
|
|
4702
|
-
if (!slug) return null;
|
|
4703
|
-
return {
|
|
4704
|
-
kind: "root",
|
|
4705
|
-
slug
|
|
4706
|
-
};
|
|
4707
|
-
}
|
|
4708
|
-
if (segments.length === 3 && segments[2] === "index.md") {
|
|
4709
|
-
const rootSlug = segments[0];
|
|
4710
|
-
const slug = segments[1];
|
|
4711
|
-
if (!rootSlug || !slug) return null;
|
|
4712
|
-
return {
|
|
4713
|
-
kind: "reply",
|
|
4714
|
-
rootSlug,
|
|
4715
|
-
slug
|
|
4716
|
-
};
|
|
4717
|
-
}
|
|
4718
|
-
return null;
|
|
4719
|
-
}
|
|
4720
|
-
function uint8ArrayToBase64(bytes) {
|
|
4721
|
-
let binary = "";
|
|
4722
|
-
for (let i = 0; i < bytes.length; i++) binary += String.fromCharCode(bytes[i]);
|
|
4723
|
-
return btoa(binary);
|
|
4724
|
-
}
|
|
4725
|
-
//#endregion
|
|
4726
|
-
export { JANT_BRAND_PACK_FILENAME as A, getJantLogoFills as B, formatTime as C, toISOString as D, time_exports as E, getJantBrandPackHref as F, arrayBufferToBase64 as G, getJantPositiveLogoPngHref as H, getJantBundledAsset as I, base64ToUint8Array as K, getJantIconFilename as L, JANT_REPO_URL as M, getDefaultJantAppleTouchIconBytes as N, HOME_BRANDING_LINK_LABEL as O, getDefaultJantFaviconIcoBytes as P, getJantIconHref as R, formatRelativeTime as S, now as T, JANT_LOGO_PATH_DATA as U, getJantLogoHref as V, JANT_LOGO_VIEW_BOX as W, getImageUrl as _, tiptapJsonToMarkdown as a, formatDate as b, render as c, extractSummary as d, extractSummaryHtml as f, escapeHtml as g, trimTiptapBody as h, createExportService as i, JANT_POSITIVE_LOGO_PNG_FILENAME as j, HOME_BRANDING_PREFIX as k, toPlainText as l, renderTiptapJson as m, createGitHubSyncService as n, markdownToTiptapJson as o, renderTiptapDocument as p, github_sync_exports as r, markdown_exports as s, SYNC_COMMIT_MARKER as t, extractBodyText as u, getMediaUrl as v, formatYearMonth as w, formatRelativeAge as x, getPublicUrlForProvider as y, getJantLogoFilename as z };
|
|
4299
|
+
export { JANT_POSITIVE_LOGO_PNG_FILENAME as A, getJantLogoHref as B, formatYearMonth as C, HOME_BRANDING_LINK_LABEL as D, toISOString as E, getJantBundledAsset as F, base64ToUint8Array as G, JANT_LOGO_PATH_DATA as H, getJantIconFilename as I, getJantIconHref as L, getDefaultJantAppleTouchIconBytes as M, getDefaultJantFaviconIcoBytes as N, HOME_BRANDING_PREFIX as O, getJantBrandPackHref as P, getJantLogoFilename as R, formatTime as S, time_exports as T, JANT_LOGO_VIEW_BOX as U, getJantPositiveLogoPngHref as V, arrayBufferToBase64 as W, getMediaUrl as _, markdown_exports as a, formatRelativeAge as b, parseMarkdownDocument as c, extractSummaryHtml as d, renderTiptapDocument as f, getImageUrl as g, escapeHtml as h, tiptapJsonToMarkdown as i, JANT_REPO_URL as j, JANT_BRAND_PACK_FILENAME as k, extractBodyText as l, trimTiptapBody as m, export_exports as n, render as o, renderTiptapJson as p, parseFrontMatter as r, toPlainText as s, createExportService as t, extractSummary as u, getPublicUrlForProvider as v, now as w, formatRelativeTime as x, formatDate as y, getJantLogoFills as z };
|