@nexpress/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/LICENSE +21 -0
- package/README.md +69 -0
- package/dist/audit-54XLVCWD.js +14 -0
- package/dist/audit-54XLVCWD.js.map +1 -0
- package/dist/auth.d.ts +640 -0
- package/dist/auth.js +94 -0
- package/dist/auth.js.map +1 -0
- package/dist/can-YLUHRJAB.js +19 -0
- package/dist/can-YLUHRJAB.js.map +1 -0
- package/dist/chunk-2G264RCD.js +68 -0
- package/dist/chunk-2G264RCD.js.map +1 -0
- package/dist/chunk-2YDGE7YX.js +92 -0
- package/dist/chunk-2YDGE7YX.js.map +1 -0
- package/dist/chunk-473S4TER.js +538 -0
- package/dist/chunk-473S4TER.js.map +1 -0
- package/dist/chunk-4ZLMEKFX.js +18 -0
- package/dist/chunk-4ZLMEKFX.js.map +1 -0
- package/dist/chunk-55FU6WED.js +179 -0
- package/dist/chunk-55FU6WED.js.map +1 -0
- package/dist/chunk-6YI5K2TI.js +1959 -0
- package/dist/chunk-6YI5K2TI.js.map +1 -0
- package/dist/chunk-BHK3AD3Q.js +41 -0
- package/dist/chunk-BHK3AD3Q.js.map +1 -0
- package/dist/chunk-CRUQBZUF.js +39 -0
- package/dist/chunk-CRUQBZUF.js.map +1 -0
- package/dist/chunk-CTSQ7BRI.js +175 -0
- package/dist/chunk-CTSQ7BRI.js.map +1 -0
- package/dist/chunk-DK2JBJH7.js +81 -0
- package/dist/chunk-DK2JBJH7.js.map +1 -0
- package/dist/chunk-DP2PREDU.js +597 -0
- package/dist/chunk-DP2PREDU.js.map +1 -0
- package/dist/chunk-EQ2Z3KMD.js +24 -0
- package/dist/chunk-EQ2Z3KMD.js.map +1 -0
- package/dist/chunk-FZ7O6DWI.js +305 -0
- package/dist/chunk-FZ7O6DWI.js.map +1 -0
- package/dist/chunk-ISLYFQWL.js +1270 -0
- package/dist/chunk-ISLYFQWL.js.map +1 -0
- package/dist/chunk-JJL74ZPK.js +68 -0
- package/dist/chunk-JJL74ZPK.js.map +1 -0
- package/dist/chunk-JKXAPSU4.js +24 -0
- package/dist/chunk-JKXAPSU4.js.map +1 -0
- package/dist/chunk-KU5M27ZC.js +24 -0
- package/dist/chunk-KU5M27ZC.js.map +1 -0
- package/dist/chunk-LSHHRDVR.js +34 -0
- package/dist/chunk-LSHHRDVR.js.map +1 -0
- package/dist/chunk-M43PGOQY.js +715 -0
- package/dist/chunk-M43PGOQY.js.map +1 -0
- package/dist/chunk-MEJAHXIO.js +150 -0
- package/dist/chunk-MEJAHXIO.js.map +1 -0
- package/dist/chunk-NUCGHWCF.js +101 -0
- package/dist/chunk-NUCGHWCF.js.map +1 -0
- package/dist/chunk-OK5HOCQI.js +845 -0
- package/dist/chunk-OK5HOCQI.js.map +1 -0
- package/dist/chunk-OROPGO65.js +13 -0
- package/dist/chunk-OROPGO65.js.map +1 -0
- package/dist/chunk-PPAS4SZR.js +176 -0
- package/dist/chunk-PPAS4SZR.js.map +1 -0
- package/dist/chunk-PPBWRKO2.js +171 -0
- package/dist/chunk-PPBWRKO2.js.map +1 -0
- package/dist/chunk-PZ5AY32C.js +10 -0
- package/dist/chunk-PZ5AY32C.js.map +1 -0
- package/dist/chunk-QO7LAQZH.js +321 -0
- package/dist/chunk-QO7LAQZH.js.map +1 -0
- package/dist/chunk-QVJ2HCAX.js +225 -0
- package/dist/chunk-QVJ2HCAX.js.map +1 -0
- package/dist/chunk-RIPHIRPP.js +68 -0
- package/dist/chunk-RIPHIRPP.js.map +1 -0
- package/dist/chunk-S27S42QY.js +134 -0
- package/dist/chunk-S27S42QY.js.map +1 -0
- package/dist/chunk-SBCVAC2Z.js +40 -0
- package/dist/chunk-SBCVAC2Z.js.map +1 -0
- package/dist/chunk-TFJ4MKPH.js +694 -0
- package/dist/chunk-TFJ4MKPH.js.map +1 -0
- package/dist/chunk-THX3SHYA.js +75 -0
- package/dist/chunk-THX3SHYA.js.map +1 -0
- package/dist/chunk-UGQSQO5B.js +222 -0
- package/dist/chunk-UGQSQO5B.js.map +1 -0
- package/dist/chunk-V2UNHGAP.js +26 -0
- package/dist/chunk-V2UNHGAP.js.map +1 -0
- package/dist/chunk-VGTPQXNQ.js +2790 -0
- package/dist/chunk-VGTPQXNQ.js.map +1 -0
- package/dist/chunk-VNIHXQ7W.js +194 -0
- package/dist/chunk-VNIHXQ7W.js.map +1 -0
- package/dist/chunk-WV272MPW.js +31 -0
- package/dist/chunk-WV272MPW.js.map +1 -0
- package/dist/chunk-X5KKBOUS.js +26 -0
- package/dist/chunk-X5KKBOUS.js.map +1 -0
- package/dist/chunk-XANPEOJC.js +17 -0
- package/dist/chunk-XANPEOJC.js.map +1 -0
- package/dist/chunk-XPVQIHAQ.js +83 -0
- package/dist/chunk-XPVQIHAQ.js.map +1 -0
- package/dist/chunk-ZCINJSS4.js +75 -0
- package/dist/chunk-ZCINJSS4.js.map +1 -0
- package/dist/community.d.ts +1425 -0
- package/dist/community.js +206 -0
- package/dist/community.js.map +1 -0
- package/dist/config-2GDU7PCK.js +32 -0
- package/dist/config-2GDU7PCK.js.map +1 -0
- package/dist/context-MNZ4QXPC.js +16 -0
- package/dist/context-MNZ4QXPC.js.map +1 -0
- package/dist/db-schema.d.ts +4 -0
- package/dist/db-schema.js +102 -0
- package/dist/db-schema.js.map +1 -0
- package/dist/db.d.ts +7 -0
- package/dist/db.js +117 -0
- package/dist/db.js.map +1 -0
- package/dist/digest-SY42GQSU.js +17 -0
- package/dist/digest-SY42GQSU.js.map +1 -0
- package/dist/errors-5OS3S2J3.js +22 -0
- package/dist/errors-5OS3S2J3.js.map +1 -0
- package/dist/host-OBOI4MJK.js +51 -0
- package/dist/host-OBOI4MJK.js.map +1 -0
- package/dist/i18n.d.ts +301 -0
- package/dist/i18n.js +68 -0
- package/dist/i18n.js.map +1 -0
- package/dist/index-B6-_vr_m.d.ts +590 -0
- package/dist/index-CY55LC0u.d.ts +4722 -0
- package/dist/index-CeiTvwbp.d.ts +168 -0
- package/dist/index-XwP1ET8b.d.ts +61 -0
- package/dist/index.d.ts +2037 -0
- package/dist/index.js +2205 -0
- package/dist/index.js.map +1 -0
- package/dist/job-log-VZXWQUDK.js +24 -0
- package/dist/job-log-VZXWQUDK.js.map +1 -0
- package/dist/jobs.d.ts +4 -0
- package/dist/jobs.js +76 -0
- package/dist/jobs.js.map +1 -0
- package/dist/logger-DqGaOU_j.d.ts +29 -0
- package/dist/logger-S7REWDNE.js +16 -0
- package/dist/logger-S7REWDNE.js.map +1 -0
- package/dist/media.d.ts +5 -0
- package/dist/media.js +41 -0
- package/dist/media.js.map +1 -0
- package/dist/mentions-2IHFVSHW.js +23 -0
- package/dist/mentions-2IHFVSHW.js.map +1 -0
- package/dist/mutes-EWAE5FZR.js +21 -0
- package/dist/mutes-EWAE5FZR.js.map +1 -0
- package/dist/notification-prefs-VPJDU7I6.js +21 -0
- package/dist/notification-prefs-VPJDU7I6.js.map +1 -0
- package/dist/observability.d.ts +156 -0
- package/dist/observability.js +32 -0
- package/dist/observability.js.map +1 -0
- package/dist/profanity-adapter-NU2JQSLX.js +12 -0
- package/dist/profanity-adapter-NU2JQSLX.js.map +1 -0
- package/dist/queue-XE5BC75T.js +14 -0
- package/dist/queue-XE5BC75T.js.map +1 -0
- package/dist/rate-limit.d.ts +99 -0
- package/dist/rate-limit.js +14 -0
- package/dist/rate-limit.js.map +1 -0
- package/dist/registry-XIXDEPVI.js +31 -0
- package/dist/registry-XIXDEPVI.js.map +1 -0
- package/dist/reputation-JRL2YQHM.js +11 -0
- package/dist/reputation-JRL2YQHM.js.map +1 -0
- package/dist/routes.d.ts +43 -0
- package/dist/routes.js +12 -0
- package/dist/routes.js.map +1 -0
- package/dist/scheduled-CIQM57HT.js +20 -0
- package/dist/scheduled-CIQM57HT.js.map +1 -0
- package/dist/seo.d.ts +410 -0
- package/dist/seo.js +44 -0
- package/dist/seo.js.map +1 -0
- package/dist/settings-FOBIESPB.js +17 -0
- package/dist/settings-FOBIESPB.js.map +1 -0
- package/dist/spam-adapter-XX3G737Z.js +12 -0
- package/dist/spam-adapter-XX3G737Z.js.map +1 -0
- package/dist/strings-VAE47B2C.js +29 -0
- package/dist/strings-VAE47B2C.js.map +1 -0
- package/dist/templates-IFVJMCJ6.js +12 -0
- package/dist/templates-IFVJMCJ6.js.map +1 -0
- package/dist/types-TlsbXS0T.d.ts +871 -0
- package/package.json +129 -0
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,2037 @@
|
|
|
1
|
+
import { c as NpConfig, d as NpCollectionConfig, e as NpAuthUser, f as NpSaveOptions, g as NpSaveResult, h as NpFindOptions, i as NpFindResult, j as NpFieldConfig, a as NpNavItem, k as NpRegisteredTheme, l as NpThemeFieldRequirement, m as NpThemeManifest, n as NpUserRole, o as NpPluginConfig } from './types-TlsbXS0T.js';
|
|
2
|
+
export { p as NpAccessFunction, q as NpArrayField, r as NpBlockConfig, b as NpBlockInstance, s as NpBlocksField, t as NpCheckboxField, u as NpCollapsibleField, v as NpCollectionHook, w as NpDateField, x as NpDocumentStatus, y as NpEditorConfig, z as NpEmailField, A as NpFieldCondition, B as NpFieldValidator, C as NpFindWhere, D as NpFindWhereSystemTokens, E as NpGroupField, F as NpHookPrincipal, G as NpI18nConfig, H as NpImageSize, I as NpJobType, J as NpJsonField, K as NpNumberField, L as NpPluginContext, F as NpPrincipal, M as NpRadioField, O as NpRelationshipField, P as NpResolvedPluginLike, N as NpRichTextContent, Q as NpRichTextField, R as NpRowField, S as NpSelectField, T as NpTextField, U as NpTextareaField, V as NpThemeCollectionRequirement, W as NpUploadConfig, X as NpUploadField, Y as ROLE_HIERARCHY } from './types-TlsbXS0T.js';
|
|
3
|
+
export { ARGON2_OPTIONS, ArcticLikeProvider, ArcticLikeTokens, FromArcticOptions, IssuedOAuthState, NpCapability, NpConsumeMemberEmailVerifyResult, NpConsumeMemberResetResult, NpConsumeResetTokenOptions, NpConsumeResetTokenResult, NpCreateResetTokenOptions, NpIssuedMemberToken, NpIssuedResetToken, NpMemberAuthRow, NpMemberIdentityRow, NpMemberResetRequestResult, NpMemberTokenPayload, NpPasswordResetPurpose, NpResetRequestResult, NpTokenPayload, NpUserIdentityRow, OAuthAuthorizeParams, OAuthExchangeParams, OAuthProfile, OAuthProvider, OAuthStatePayload, ResolveMemberOAuthLoginInput, ResolveMemberOAuthLoginResult, ResolveOAuthLoginInput, ResolveOAuthLoginResult, ResolvedOAuthMember, ResolvedOAuthUser, VerifyOAuthStateResult, authenticated, can, consumeMemberEmailVerifyToken, consumeMemberPasswordReset, consumePasswordResetToken, createMemberEmailVerifyToken, createPasswordResetToken, fromArctic, getMemberFromTokenPayload, getOAuthProvider, hashPassword, invalidateAllMemberSessions, invalidateAllSessions, isAdmin, isEditorOrAbove, isOwnerOrAdmin, isTokenVerificationError, issueOAuthState, listMemberIdentities, listOAuthProviders, listUserIdentities, registerOAuthProvider, requestMemberPasswordReset, requestPasswordReset, resetOAuthProviders, resolveMemberOAuthLogin, resolveOAuthLogin, revokeMemberIdentity, revokeUserIdentity, sha256, signMemberToken, signToken, verifyCsrf, verifyMemberToken, verifyOAuthState, verifyPassword, verifyToken, verifyTokenFull } from './auth.js';
|
|
4
|
+
import { SQL } from 'drizzle-orm';
|
|
5
|
+
import { NpSitemapEntry, NpFeedEntry } from './seo.js';
|
|
6
|
+
export { ArticleJsonLd, ArticleJsonLdInput, BuildAtomFeedOptions, BuildJsonLdContext, BuildSitemapOptions, DEFAULT_SITE_SEO_SETTINGS, DiscussionForumPostingJsonLd, NpPageMetadata, NpPageMetadataInput, NpSeoSettingsPatch, NpSiteSeoSettings, NpSitemapIndexEntry, PersonJsonLd, PersonJsonLdInput, WebSiteJsonLd, buildArticleJsonLd, buildAtomFeed, buildDiscussionForumPostingJsonLd, buildPageMetadata, buildPersonJsonLd, buildSitemap, buildWebSiteJsonLd, getSiteSeoSettings, renderAtomFeed, renderSitemapIndexXml, renderSitemapXml, validateSeoSettingsPatch } from './seo.js';
|
|
7
|
+
export { c as createDbConnection, g as generateDocumentsModule, a as generateDrizzleSchema, b as generateTypeScript, d as getDb, s as setDb } from './index-XwP1ET8b.js';
|
|
8
|
+
import { z, ZodTypeAny } from 'zod';
|
|
9
|
+
export { N as NP_GLOBAL_PLUGIN_SITE_ID, n as npAuditEvents, a as npBanKindEnum, b as npBanScopeEnum, c as npBans, d as npCommentStatusEnum, e as npComments, f as npFollows, g as npJobLogs, h as npMedia, i as npMediaFolders, j as npMediaFoldersRelations, k as npMediaRefs, l as npMediaRefsRelations, m as npMediaRelations, o as npMediaStatusEnum, p as npMemberIdentities, q as npMemberMutes, r as npMemberRoleScopeEnum, s as npMemberRoles, t as npMemberSessions, u as npMemberStatusEnum, v as npMembers, w as npNavigation, x as npNavigationRelations, y as npNotifications, z as npPasswordResetPurposeEnum, A as npPluginStorage, B as npPlugins, C as npReactions, D as npReports, E as npRevisionStatusEnum, F as npRevisions, G as npRevisionsRelations, H as npSessions, I as npSessionsRelations, J as npSettings, K as npSettingsRelations, L as npSiteMemberships, M as npSites, O as npSlugHistory, P as npStringOverrides, Q as npUserOAuthIdentities, R as npUserRoleEnum, S as npUsers, T as npUsersRelations, U as npWorkerHeartbeats } from './index-CY55LC0u.js';
|
|
10
|
+
export { D as DEFAULT_JOB_LOG_RETENTION_MS, L as ListJobLogsOptions, N as NpJobCountOptions, a as NpJobHandler, b as NpJobListOptions, c as NpJobListResult, d as NpJobLogEntry, e as NpJobQueue, f as NpJobState, g as NpJobStateCounts, h as NpJobSummary, i as NpJobsPauseState, j as NpPluginScheduleStats, k as NpReconcileSchedulesResult, l as NpScheduleSummary, m as NpWorkerHealthSummary, n as NpWorkerHeartbeat, P as PAUSE_SYNC_INTERVAL_MS, o as PgBossAdapter, S as SetJobsPauseStateInput, W as WORKER_HEARTBEAT_INTERVAL_MS, p as WORKER_STALE_THRESHOLD_MS, q as configureBuiltinJobContext, r as countAliveWorkers, s as countJobLogs, t as enqueueJob, u as getAllJobHandlers, v as getCurrentJobId, w as getJobHandler, x as getJobQueue, y as getJobsPauseState, z as getOptionalJobQueue, A as listJobLogs, B as listWorkerHealth, C as markWorkerStopped, E as pruneJobLogsOlderThan, F as purgeStaleWorkers, G as recordHeartbeat, H as recordJobLog, I as registerBuiltinHandlers, J as registerJobHandler, K as runInJobContext, M as setJobQueue, O as setJobsPauseState, Q as startProducer, R as startWorker, T as stopProducer, U as stopWorker } from './index-B6-_vr_m.js';
|
|
11
|
+
import { N as NpStorageAdapter, a as NpFileMetadata } from './index-CeiTvwbp.js';
|
|
12
|
+
export { D as DEFAULT_IMAGE_SIZES, b as NpGetMediaUrlOptions, c as NpMediaUploader, d as NpMediaUploaderKindFilter, e as NpMediaVariantName, f as NpProcessedImageResult, g as NpProcessedImageVariant, h as cleanupDeletedMedia, i as deleteMedia, j as extractMediaIds, k as getMediaById, l as getMediaUrl, m as getStorageAdapter, n as listMedia, p as processImage, o as processMediaImage, s as setStorageAdapter, u as uploadMedia } from './index-CeiTvwbp.js';
|
|
13
|
+
import { ReadableStream } from 'node:stream/web';
|
|
14
|
+
export { NpLocaleDirection, NpResolveLocaleInput, NpResolveLocaleResult, NpStringOverrideRow, NpTranslationBundle, NpTranslationParams, addStrings, clearStringOverrideCacheForSite, deleteStringOverride, formatDate, formatNumber, formatRelativeTime, getAllStrings, getCurrentLocale, getI18nConfig, getLocaleDirection, getStringOverride, getStringOverridesForSite, getStrings, listStringOverridesForSite, loadStringOverridesForSite, resetI18nConfig, resetIntlFormatterCache, resetStringOverrideCache, resetStrings, resetTranslationCache, resolveLocale, setI18nConfig, setStringOverride, setStrings, t, tSync } from './i18n.js';
|
|
15
|
+
import { NodePgDatabase } from 'drizzle-orm/node-postgres';
|
|
16
|
+
export { N as NpLogLevel, a as NpLogger, c as consoleLogger, g as getLogger, b as getScopedLogger, r as resetLogger, s as setLogger } from './logger-DqGaOU_j.js';
|
|
17
|
+
export { NpErrorReportContext, NpErrorReporter, NpStartupSafetyInput, getErrorReporter, noopErrorReporter, reportError, resetErrorReporter, setErrorReporter, verifyStartupSafety } from './observability.js';
|
|
18
|
+
export { InMemoryRateLimiter, NpRateLimitDecision, NpRateLimiterAdapter, getOptionalRateLimiter, getRateLimiter, setRateLimiter } from './rate-limit.js';
|
|
19
|
+
export { AuditActor, AuditActorKind, AuditEventRow, BanKind, BanScope, BuildDigestEmailInput, CommentStatus, CommunityCapability, CommunityRoleDefinition, CommunityScope, CreateNotificationInput, DEFAULT_COMMUNITY_SETTINGS, DEFAULT_REACTION_KINDS, FanOutMentionsInput, FileReportInput, GrantMemberRoleInput, IssueBanInput, ListAuditOptions, ListMutesOptions, ListNotificationsOptions, ListReportsOptions, ListReportsResult, MENTION_HANDLE_RE, MarkReadInput, MemberAction, MemberCanTarget, MuteMemberInput, NpBanRow, NpCommentCreateInput, NpCommentDeleteInput, NpCommentHideInput, NpCommentListOptions, NpCommentListResult, NpCommentRestoreInput, NpCommentRow, NpCommentSort, NpCommentUpdateInput, NpCommunitySettings, NpDigestCadence, NpDigestEmailContent, NpDigestNotificationSummary, NpFollowInput, NpFollowRow, NpMemberMuteRow, NpMemberMuteSummary, NpMemberProfile, NpMemberPurgeResult, NpMemberRoleGrantRow, NpMemberUploadQuota, NpMentionTarget, NpNotificationKindMeta, NpNotificationListResult, NpNotificationPrefs, NpNotificationRow, NpProfanityAdapter, NpProfanityCheckContext, NpProfanityVerdict, NpProfanityVerdictKind, NpReactToInput, NpReactionRow, NpReportRow, NpReputationAdapter, NpReputationEvent, NpSpamAdapter, NpSpamCheckContext, NpSpamVerdict, NpSpamVerdictKind, Principal, RecordAuditEventInput, ResolveReportInput, RevokeBanInput, RevokeMemberRoleInput, RunDigestSweepInput, RunDigestSweepResult, SetMemberNotificationPrefsInput, addReaction, applyReputation, assertNotBanned, assertOwnsNotification, assertReactableExists, buildDigestEmail, countReactions, createComment, createNotification, deleteComment, extractMentionHandles, extractMentionHandlesFromDocData, extractMentionHandlesFromRichText, fanOutMentionNotifications, fileReport, follow, getCommunityRole, getCommunitySettings, getMemberNotificationPrefs, getMemberProfile, getMemberProfiles, getMutedTargetIds, getProfanityAdapter, getReputationAdapter, getSpamAdapter, grantMemberRole, hideComment, isFollowing, isMuted, isNotificationKindEnabled, issueBan, listAuditEvents, listBansForMember, listComments, listCommunityRoles, listFollowing, listMemberReactions, listMemberRoleGrants, listMutes, listNotificationKinds, listNotifications, listReports, markAllNotificationsRead, markNotificationsRead, memberCan, muteMember, principalCan, purgeMemberContent, recordAuditEvent, recordDigestSent, registerCommunityRole, registerNotificationKind, removeReaction, renderCommentMarkdown, resetCommunityRoles, resetProfanityAdapter, resetReputationAdapter, resetSpamAdapter, resolveMentionedMembers, resolveReport, restoreComment, revokeBan, revokeMemberRole, runDigestSweep, setMemberNotificationPrefs, setProfanityAdapter, setReputationAdapter, setSpamAdapter, staffDeleteComment, staffHideComment, staffRestoreComment, unfollow, unmuteMember, unreadNotificationCount, unresolvedReportCount, updateComment, updateCommunitySettings, validateCommunitySettingsPatch, withMemberWrite } from './community.js';
|
|
20
|
+
export { NpCustomRoute, clearCustomRoutes, getCustomRoutes, registerCustomRoute } from './routes.js';
|
|
21
|
+
import '@node-rs/argon2';
|
|
22
|
+
import 'pg';
|
|
23
|
+
import 'drizzle-orm/pg-core';
|
|
24
|
+
import 'pg-boss';
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Validates the project's NpConfig against the declarative schema and returns
|
|
28
|
+
* it unchanged on success. Catches common mistakes (bad collection slug,
|
|
29
|
+
* missing auth.secret, malformed storage adapter, etc.) at module-eval time
|
|
30
|
+
* with a clear message instead of a cryptic runtime failure once the app
|
|
31
|
+
* tries to boot.
|
|
32
|
+
*
|
|
33
|
+
* The most common boot trip-up by far is "auth.secret" / "site.url" /
|
|
34
|
+
* "db.connectionString" missing on a fresh install. We translate Zod's raw
|
|
35
|
+
* `String must contain at least 1 character` style messages into actionable
|
|
36
|
+
* "set NP_SECRET in .env, or run `pnpm run setup`" hints so the new operator
|
|
37
|
+
* isn't googling Zod path strings.
|
|
38
|
+
*
|
|
39
|
+
* Unknown plugin entries are accepted here — the plugin loader does the
|
|
40
|
+
* deeper validation of manifests against @nexpress/plugin-sdk.
|
|
41
|
+
*/
|
|
42
|
+
declare function defineConfig(config: NpConfig): NpConfig;
|
|
43
|
+
|
|
44
|
+
declare function defineCollection(config: NpCollectionConfig): NpCollectionConfig;
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Public error-code contract (#290).
|
|
48
|
+
*
|
|
49
|
+
* `NpError.code` is the machine-readable string the API surface
|
|
50
|
+
* sends to clients (`response.error.code`). The moment a client
|
|
51
|
+
* branches on `error.code === "VALIDATION_ERROR"` it becomes
|
|
52
|
+
* part of the public contract — adding a typo'd code or
|
|
53
|
+
* renaming an existing one breaks every integration.
|
|
54
|
+
*
|
|
55
|
+
* `NpErrorCode` is the union of every code the framework
|
|
56
|
+
* currently emits. Adding a new code requires extending this
|
|
57
|
+
* union deliberately — no more "casual" string adoption that
|
|
58
|
+
* accumulates over a year of PRs.
|
|
59
|
+
*
|
|
60
|
+
* The `NpError` constructor still accepts plain `string` so
|
|
61
|
+
* out-of-tree plugins that throw their own codes keep working;
|
|
62
|
+
* internal code paths use the union to get IntelliSense and
|
|
63
|
+
* catch typos at compile time. The `(string & {})` trick on
|
|
64
|
+
* the public type keeps editor completion narrow without
|
|
65
|
+
* locking the runtime contract.
|
|
66
|
+
*
|
|
67
|
+
* **Stability**: codes follow semver. Renames or removals are
|
|
68
|
+
* major-bump only. New codes may land in minor versions. See
|
|
69
|
+
* `docs/api-error-codes.md` for the catalogue an operator /
|
|
70
|
+
* client team should rely on.
|
|
71
|
+
*/
|
|
72
|
+
type NpErrorCode = "FORBIDDEN" | "NOT_FOUND" | "VALIDATION_ERROR" | "UNAUTHORIZED" | "CONFLICT" | "RATE_LIMITED" | "TOO_MANY_REQUESTS" | "INVALID_URL" | "EMAIL_ADAPTER_MISSING_DEPENDENCY" | "EMAIL_DELIVERY_FAILED" | "SITE_CONTEXT_MISSING" | "INTERNAL_ERROR";
|
|
73
|
+
/**
|
|
74
|
+
* The constructor signature accepts the union *or* an arbitrary
|
|
75
|
+
* string. Inside the codebase, passing a literal that isn't in
|
|
76
|
+
* the union triggers a TypeScript error in strict editors
|
|
77
|
+
* (autocompletion narrows to `NpErrorCode`). External plugins
|
|
78
|
+
* authoring their own codes keep working — they just won't get
|
|
79
|
+
* the autocomplete win.
|
|
80
|
+
*/
|
|
81
|
+
type NpErrorCodeInput = NpErrorCode | (string & Record<never, never>);
|
|
82
|
+
declare class NpError extends Error {
|
|
83
|
+
readonly code: NpErrorCodeInput;
|
|
84
|
+
readonly statusCode: number;
|
|
85
|
+
constructor(message: string, code: NpErrorCodeInput, statusCode?: number);
|
|
86
|
+
}
|
|
87
|
+
declare class NpForbiddenError extends NpError {
|
|
88
|
+
constructor(collection: string, operation: string);
|
|
89
|
+
}
|
|
90
|
+
declare class NpNotFoundError extends NpError {
|
|
91
|
+
constructor(collection: string, id: string);
|
|
92
|
+
}
|
|
93
|
+
declare class NpValidationError extends NpError {
|
|
94
|
+
readonly errors: Array<{
|
|
95
|
+
field: string;
|
|
96
|
+
message: string;
|
|
97
|
+
}>;
|
|
98
|
+
constructor(message: string, errors: Array<{
|
|
99
|
+
field: string;
|
|
100
|
+
message: string;
|
|
101
|
+
}>);
|
|
102
|
+
}
|
|
103
|
+
declare class NpAuthError extends NpError {
|
|
104
|
+
constructor(message?: string);
|
|
105
|
+
}
|
|
106
|
+
declare class NpConflictError extends NpError {
|
|
107
|
+
constructor(message: string);
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Per-actor rate limit / quota exceeded. Distinct from
|
|
111
|
+
* `NpValidationError` because the request shape was valid — the
|
|
112
|
+
* server is rejecting it on policy grounds. The 429 status lets
|
|
113
|
+
* client UIs recognize the case and surface a "you've hit your
|
|
114
|
+
* daily limit" message rather than a generic validation error.
|
|
115
|
+
*/
|
|
116
|
+
declare class NpRateLimitError extends NpError {
|
|
117
|
+
constructor(message: string);
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* No site context resolved when one was required (#272). Thrown
|
|
121
|
+
* by `requireSiteId()` on write paths. 500 because this is a
|
|
122
|
+
* server-side wiring bug — the user request was well-formed,
|
|
123
|
+
* but the framework couldn't tell which tenant to write to.
|
|
124
|
+
* Promoted from a plain `Error` to a real `NpError` subclass so
|
|
125
|
+
* the API layer surfaces it as a uniform error envelope and
|
|
126
|
+
* clients can branch on the stable `SITE_CONTEXT_MISSING` code.
|
|
127
|
+
*/
|
|
128
|
+
declare class NpSiteContextMissingError extends NpError {
|
|
129
|
+
constructor(message: string);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Plain-text concatenation of every searchable field. Used by
|
|
134
|
+
* the moderation pipeline (spam/profanity adapters need the
|
|
135
|
+
* full text, weights don't matter there).
|
|
136
|
+
*/
|
|
137
|
+
declare function buildSearchVector(config: NpCollectionConfig, data: Record<string, unknown>): string;
|
|
138
|
+
/**
|
|
139
|
+
* Phase 10.7 — split searchable fields into Postgres tsvector
|
|
140
|
+
* weight buckets so title-like matches outrank body matches at
|
|
141
|
+
* query time.
|
|
142
|
+
*
|
|
143
|
+
* Convention (no per-field opt-in to keep collections
|
|
144
|
+
* declarations terse):
|
|
145
|
+
* - field.name === "title" or "name" → weight A
|
|
146
|
+
* - other text / textarea / email → weight B
|
|
147
|
+
* - richText → weight C
|
|
148
|
+
*
|
|
149
|
+
* Sites that want different weights can name their primary
|
|
150
|
+
* field "title" and the framework picks the right bucket
|
|
151
|
+
* automatically. (A future revision can add `field.search.weight`
|
|
152
|
+
* for explicit control.)
|
|
153
|
+
*
|
|
154
|
+
* Postgres ts_rank() applies the default weight scale
|
|
155
|
+
* { D: 0.1, C: 0.2, B: 0.4, A: 1.0 }, so an A-weighted match
|
|
156
|
+
* scores ~10× a D-weighted one — meaningful boost for titles.
|
|
157
|
+
*/
|
|
158
|
+
interface NpSearchVectorParts {
|
|
159
|
+
/** Title-like fields. Highest rank weight. */
|
|
160
|
+
a: string;
|
|
161
|
+
/** Body fields (text/textarea/email). */
|
|
162
|
+
b: string;
|
|
163
|
+
/** Rich-text body. */
|
|
164
|
+
c: string;
|
|
165
|
+
/** Reserved for future categorization (tags, slugs, etc.). */
|
|
166
|
+
d: string;
|
|
167
|
+
}
|
|
168
|
+
declare function buildSearchVectorParts(config: NpCollectionConfig, data: Record<string, unknown>): NpSearchVectorParts;
|
|
169
|
+
/**
|
|
170
|
+
* Build the weighted-tsvector SQL fragment that the pipeline
|
|
171
|
+
* binds to `searchVector` on insert/update. Each non-empty
|
|
172
|
+
* bucket becomes `setweight(to_tsvector('english', $bucket), '<W>')`;
|
|
173
|
+
* the buckets are concatenated with `||`. Empty buckets are
|
|
174
|
+
* skipped so the resulting expression is always non-trivial.
|
|
175
|
+
*
|
|
176
|
+
* If every bucket is empty (collection has no text fields, or
|
|
177
|
+
* every text field is null on this row), returns an empty
|
|
178
|
+
* tsvector cast — Postgres accepts this as a valid empty
|
|
179
|
+
* vector value.
|
|
180
|
+
*/
|
|
181
|
+
declare function buildWeightedSearchVectorSql(config: NpCollectionConfig, data: Record<string, unknown>): SQL;
|
|
182
|
+
|
|
183
|
+
interface CollectionRegistration {
|
|
184
|
+
config: NpCollectionConfig;
|
|
185
|
+
table: unknown;
|
|
186
|
+
childTables?: Record<string, unknown>;
|
|
187
|
+
joinTables?: Record<string, unknown>;
|
|
188
|
+
}
|
|
189
|
+
declare function registerCollection(slug: string, table: unknown, config: NpCollectionConfig, opts?: {
|
|
190
|
+
childTables?: Record<string, unknown>;
|
|
191
|
+
joinTables?: Record<string, unknown>;
|
|
192
|
+
}): void;
|
|
193
|
+
declare function getCollectionConfig(slug: string): NpCollectionConfig;
|
|
194
|
+
declare function getCollectionTable(slug: string): unknown;
|
|
195
|
+
declare function getCollectionRegistration(slug: string): CollectionRegistration;
|
|
196
|
+
declare function getAllCollectionSlugs(): string[];
|
|
197
|
+
|
|
198
|
+
declare function saveDocument(collection: string, docId: string | null, data: Record<string, unknown>, user: NpAuthUser, options?: NpSaveOptions): Promise<NpSaveResult>;
|
|
199
|
+
/**
|
|
200
|
+
* Member-side document create. Only valid when
|
|
201
|
+
* `config.community?.memberWrite?.create === true`. Assumes the API
|
|
202
|
+
* layer has already authenticated the member (and that the cookie's
|
|
203
|
+
* status was checked) — this function adds:
|
|
204
|
+
* - the per-collection opt-in gate, and
|
|
205
|
+
* - `assertNotBanned(memberId)` (site-wide; per-collection bans
|
|
206
|
+
* resolve to the same site scope)
|
|
207
|
+
*
|
|
208
|
+
* Fires the `document.created` reputation event after a successful
|
|
209
|
+
* write so adapters can credit the author the same way they credit
|
|
210
|
+
* comments / reactions. Member-side update / delete live in
|
|
211
|
+
* `updateMemberDocument` / `deleteMemberDocument` below.
|
|
212
|
+
*/
|
|
213
|
+
/**
|
|
214
|
+
* Member-side document update. Only valid when
|
|
215
|
+
* `config.community?.memberWrite?.update === true` AND the existing
|
|
216
|
+
* row's `member_author_id` matches the caller. Members can NOT
|
|
217
|
+
* change `_status` via update — `options.status` is stripped here
|
|
218
|
+
* so a forged body field can't bypass the moderation pipeline. The
|
|
219
|
+
* author column itself is also locked — see saveDocumentImpl.
|
|
220
|
+
*/
|
|
221
|
+
declare function updateMemberDocument(collection: string, docId: string, data: Record<string, unknown>, memberId: string, options?: NpSaveOptions): Promise<NpSaveResult>;
|
|
222
|
+
declare function createMemberDocument(collection: string, data: Record<string, unknown>, memberId: string, options?: NpSaveOptions): Promise<NpSaveResult>;
|
|
223
|
+
/**
|
|
224
|
+
* Persist an in-flight editor snapshot as a revision **without** touching
|
|
225
|
+
* the main document row. Designed for client-side autosave loops: the
|
|
226
|
+
* editor sends every few seconds while the user types, and a crash mid-
|
|
227
|
+
* edit can be recovered by restoring the latest autosave revision.
|
|
228
|
+
*
|
|
229
|
+
* - Requires `versions.drafts` to be enabled on the collection.
|
|
230
|
+
* - Optionally gated by `versions.drafts.autosave === true` (when
|
|
231
|
+
* `versions` is the object form). Throws `NpValidationError` otherwise
|
|
232
|
+
* so the API can return a tidy 4xx instead of silently writing.
|
|
233
|
+
* - Skips the full zod validation that `saveDocument` runs — autosave
|
|
234
|
+
* payloads may be temporarily incomplete (the user is still typing).
|
|
235
|
+
* - Skips hooks, jobs, and revalidation: nothing is "saved" yet.
|
|
236
|
+
* - Deduplicates against the most recent autosave: if the snapshot is
|
|
237
|
+
* byte-identical to the previous autosave row, returns the existing
|
|
238
|
+
* summary instead of writing a new one. Avoids unbounded autosave
|
|
239
|
+
* rows during long idle edit sessions where react-hook-form fires
|
|
240
|
+
* spurious "change" events.
|
|
241
|
+
*/
|
|
242
|
+
declare function autosaveRevision(collection: string, documentId: string, data: Record<string, unknown>, user: NpAuthUser): Promise<{
|
|
243
|
+
id: string;
|
|
244
|
+
version: number;
|
|
245
|
+
status: "autosave";
|
|
246
|
+
createdAt: Date;
|
|
247
|
+
reused: boolean;
|
|
248
|
+
}>;
|
|
249
|
+
declare function deleteDocument(collection: string, docId: string, user: NpAuthUser): Promise<void>;
|
|
250
|
+
/**
|
|
251
|
+
* Member-side delete. Owner-only — the existing row's
|
|
252
|
+
* `member_author_id` must match the caller. Fires
|
|
253
|
+
* `document.deleted` reputation event so adapters can debit the
|
|
254
|
+
* author the same way `comment.deleted` debits commenters.
|
|
255
|
+
*
|
|
256
|
+
* The reputation event is gated on the row's status at delete
|
|
257
|
+
* time: only `published` docs ever earned a `document.created`
|
|
258
|
+
* credit (the create path withholds it for pending rows; promote
|
|
259
|
+
* later backfills the credit). Issuing a `document.deleted`
|
|
260
|
+
* debit for a row that was never credited would drive the
|
|
261
|
+
* member negative for deleting their own not-yet-visible
|
|
262
|
+
* content (#126). The audit row is unconditional — the operator
|
|
263
|
+
* still wants to see "member deleted X".
|
|
264
|
+
*/
|
|
265
|
+
declare function deleteMemberDocument(collection: string, docId: string, memberId: string): Promise<void>;
|
|
266
|
+
/**
|
|
267
|
+
* Staff promotion of a member-authored `pending` row to `published`
|
|
268
|
+
* (Phase 9.7d). Closes the loop on the 9.7c moderation gate:
|
|
269
|
+
* - the row's status flips to `published` (visible on the public
|
|
270
|
+
* site immediately)
|
|
271
|
+
* - the deferred `document.created` reputation event fires now,
|
|
272
|
+
* crediting the author for content that was held in review
|
|
273
|
+
* (mirrors how a comment promoted from `pending` would, in a
|
|
274
|
+
* hypothetical comment-promote API — not implemented yet)
|
|
275
|
+
* - audit log records `document.promote` with the staff actor
|
|
276
|
+
* and the original member author in the payload
|
|
277
|
+
*
|
|
278
|
+
* Guards:
|
|
279
|
+
* - 404 if the row doesn't exist
|
|
280
|
+
* - 400 (validation) if the row isn't currently `pending`
|
|
281
|
+
* - 400 (validation) if the row isn't member-authored
|
|
282
|
+
* (`member_author_id` is null) — staff drafts use the standard
|
|
283
|
+
* edit path
|
|
284
|
+
*
|
|
285
|
+
* Idempotence: a second promote on an already-`published` row 400s
|
|
286
|
+
* rather than silently no-op'ing — the audit trail and reputation
|
|
287
|
+
* backfill must run exactly once per row.
|
|
288
|
+
*/
|
|
289
|
+
declare function promoteMemberDocument(collection: string, docId: string, staffUserId: string): Promise<NpSaveResult>;
|
|
290
|
+
declare function findDocuments<T extends object = Record<string, unknown>>(collection: string, options: NpFindOptions<NoInfer<T>>, user?: NpAuthUser): Promise<NpFindResult<T>>;
|
|
291
|
+
declare function getDocumentById<T extends object = Record<string, unknown>>(collection: string, id: string, user?: NpAuthUser): Promise<T | null>;
|
|
292
|
+
|
|
293
|
+
type NpRevisionStatus = "draft" | "published" | "autosave";
|
|
294
|
+
interface NpRevisionSummary {
|
|
295
|
+
id: string;
|
|
296
|
+
collection: string;
|
|
297
|
+
documentId: string;
|
|
298
|
+
version: number;
|
|
299
|
+
status: NpRevisionStatus;
|
|
300
|
+
changedFields: string[];
|
|
301
|
+
authorId: string | null;
|
|
302
|
+
createdAt: Date;
|
|
303
|
+
}
|
|
304
|
+
interface NpRevision extends NpRevisionSummary {
|
|
305
|
+
snapshot: Record<string, unknown>;
|
|
306
|
+
}
|
|
307
|
+
interface NpRevisionListOptions {
|
|
308
|
+
limit?: number;
|
|
309
|
+
offset?: number;
|
|
310
|
+
}
|
|
311
|
+
interface NpRevisionListResult {
|
|
312
|
+
revisions: NpRevisionSummary[];
|
|
313
|
+
total: number;
|
|
314
|
+
}
|
|
315
|
+
declare function listRevisions(collection: string, documentId: string, options?: NpRevisionListOptions, user?: NpAuthUser | null): Promise<NpRevisionListResult>;
|
|
316
|
+
declare function getRevision(collection: string, documentId: string, revisionId: string, user?: NpAuthUser | null): Promise<NpRevision>;
|
|
317
|
+
declare function restoreRevision(collection: string, documentId: string, revisionId: string, user: NpAuthUser): Promise<NpSaveResult>;
|
|
318
|
+
|
|
319
|
+
interface PublishScheduledResult {
|
|
320
|
+
published: number;
|
|
321
|
+
byCollection: Record<string, string[]>;
|
|
322
|
+
}
|
|
323
|
+
/**
|
|
324
|
+
* Scans every registered collection that has a `publishedAt` date field,
|
|
325
|
+
* flips rows with `status="scheduled"` whose `publishedAt <= now` to
|
|
326
|
+
* `status="published"`, and fires `content:afterPublish` for each.
|
|
327
|
+
*
|
|
328
|
+
* Safe to call repeatedly (idempotent once a doc is published) and cheap —
|
|
329
|
+
* each UPDATE runs against an indexed status column and no-ops when empty.
|
|
330
|
+
*/
|
|
331
|
+
declare function publishScheduledDocuments(atTime?: Date): Promise<PublishScheduledResult>;
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* Cross-collection pending queue (Phase 9.7e). Lists every
|
|
335
|
+
* member-authored row that landed `status = "pending"` — the ones
|
|
336
|
+
* waiting on a staff promote (9.7d) or staff delete. Only collections
|
|
337
|
+
* that opt into `community.memberWrite.create` are scanned; the rest
|
|
338
|
+
* either have no member-author column at all or aren't part of the
|
|
339
|
+
* member-write surface.
|
|
340
|
+
*
|
|
341
|
+
* Phase 12.11 — replaced the v1 fan-out (one round trip per
|
|
342
|
+
* collection, no SQL `LIMIT`, JS-side merge + page) with a single
|
|
343
|
+
* `UNION ALL` query whose outer `LIMIT/OFFSET` runs at the database.
|
|
344
|
+
* The per-collection `(status, member_author_id)` is still
|
|
345
|
+
* status-indexed (`np_c_<slug>_status_idx`), so each branch of the
|
|
346
|
+
* union narrows fast; the database does the cross-collection
|
|
347
|
+
* `ORDER BY created_at DESC` + paging. A site with dozens of
|
|
348
|
+
* member-writable surfaces no longer fans out N+2 round trips, and
|
|
349
|
+
* an unattended collection accumulating thousands of pendings can't
|
|
350
|
+
* blow heap by being read fully into memory.
|
|
351
|
+
*/
|
|
352
|
+
interface NpPendingDocSummary {
|
|
353
|
+
id: string;
|
|
354
|
+
collectionSlug: string;
|
|
355
|
+
/** From the doc's `title` field, when it has one. Never empty. */
|
|
356
|
+
title: string;
|
|
357
|
+
slug: string | null;
|
|
358
|
+
status: "pending";
|
|
359
|
+
createdAt: Date;
|
|
360
|
+
/**
|
|
361
|
+
* Resolved from `np_members` via `member_author_id`. Null when the
|
|
362
|
+
* member was deleted after authoring (FK is `ON DELETE SET NULL`)
|
|
363
|
+
* — mods see the audit trail for the original actor in that case.
|
|
364
|
+
*/
|
|
365
|
+
memberAuthor: {
|
|
366
|
+
id: string;
|
|
367
|
+
handle: string;
|
|
368
|
+
displayName: string;
|
|
369
|
+
} | null;
|
|
370
|
+
}
|
|
371
|
+
interface NpListPendingDocsOptions {
|
|
372
|
+
/** Restrict to one collection. Useful for per-collection queue
|
|
373
|
+
* pages; omit for the global queue. */
|
|
374
|
+
collectionSlug?: string;
|
|
375
|
+
limit?: number;
|
|
376
|
+
offset?: number;
|
|
377
|
+
}
|
|
378
|
+
interface NpListPendingDocsResult {
|
|
379
|
+
docs: NpPendingDocSummary[];
|
|
380
|
+
totalDocs: number;
|
|
381
|
+
}
|
|
382
|
+
declare function listPendingMemberDocs(options?: NpListPendingDocsOptions): Promise<NpListPendingDocsResult>;
|
|
383
|
+
|
|
384
|
+
interface SearchCollectionsOptions {
|
|
385
|
+
q: string;
|
|
386
|
+
collections?: string[];
|
|
387
|
+
limit?: number;
|
|
388
|
+
offset?: number;
|
|
389
|
+
/**
|
|
390
|
+
* Extra where-filter applied on top of the default `{ status: "published" }`
|
|
391
|
+
* for each collection. Pass `{}` to disable the status filter (caller should
|
|
392
|
+
* only do this for authenticated admin contexts).
|
|
393
|
+
*/
|
|
394
|
+
where?: Record<string, unknown>;
|
|
395
|
+
/**
|
|
396
|
+
* Phase 12.4 — restrict i18n collections to one locale. Non-
|
|
397
|
+
* i18n collections ignore this (no `locale` column to match).
|
|
398
|
+
* Public site search reads this from the URL's locale prefix
|
|
399
|
+
* so visitors browsing in `/ko/` only see Korean hits.
|
|
400
|
+
*/
|
|
401
|
+
locale?: string;
|
|
402
|
+
}
|
|
403
|
+
interface SearchResultItem {
|
|
404
|
+
collection: string;
|
|
405
|
+
doc: Record<string, unknown>;
|
|
406
|
+
}
|
|
407
|
+
interface SearchResult {
|
|
408
|
+
results: SearchResultItem[];
|
|
409
|
+
total: number;
|
|
410
|
+
perCollection: Record<string, number>;
|
|
411
|
+
}
|
|
412
|
+
/**
|
|
413
|
+
* Cross-collection full-text search using the existing `search_vector` column
|
|
414
|
+
* on each collection table. Built on top of `findDocuments` so it inherits
|
|
415
|
+
* the ts_rank ordering, access-control read checks, and pagination.
|
|
416
|
+
*
|
|
417
|
+
* Results are merged in per-collection slug order; for an MVP the within-
|
|
418
|
+
* collection ranking is authoritative. A future version can do a UNION across
|
|
419
|
+
* tables if global ranking becomes a priority.
|
|
420
|
+
*/
|
|
421
|
+
declare function searchCollections(opts: SearchCollectionsOptions): Promise<SearchResult>;
|
|
422
|
+
interface ReindexResult {
|
|
423
|
+
collection: string;
|
|
424
|
+
processed: number;
|
|
425
|
+
}
|
|
426
|
+
/**
|
|
427
|
+
* Rebuilds the `search_vector` column for every row in a collection. Useful
|
|
428
|
+
* after bulk imports or for recovering from corrupted vectors. Idempotent —
|
|
429
|
+
* safe to run against a live collection while writes continue.
|
|
430
|
+
*/
|
|
431
|
+
declare function reindexCollection(slug: string): Promise<ReindexResult>;
|
|
432
|
+
|
|
433
|
+
/**
|
|
434
|
+
* Phase 10.6 — pluggable search adapter. Same shape as the
|
|
435
|
+
* spam / profanity / reputation adapters from Phase 9.6+:
|
|
436
|
+
* a single global slot, opt-in via `setSearchAdapter`, default
|
|
437
|
+
* is "no adapter" which keeps the existing pg `tsvector` path
|
|
438
|
+
* authoritative.
|
|
439
|
+
*
|
|
440
|
+
* Plugins implement this interface to delegate search to an
|
|
441
|
+
* external engine — Algolia, Meilisearch, OpenSearch, etc.
|
|
442
|
+
* The plugin is responsible for KEEPING the engine's index
|
|
443
|
+
* fresh; that's done by subscribing to the `content:afterCreate` /
|
|
444
|
+
* `:afterUpdate` / `:afterDelete` hooks the framework already
|
|
445
|
+
* fires (9.7o made those Principal-aware) — no new plumbing
|
|
446
|
+
* in the pipeline. The adapter ONLY handles the read path.
|
|
447
|
+
*
|
|
448
|
+
* Returning `null` / `undefined` from `search()` means "I don't
|
|
449
|
+
* know how to handle this query; fall through to the pg
|
|
450
|
+
* default." Useful when an adapter only indexes certain
|
|
451
|
+
* collections, or wants to defer to pg under specific
|
|
452
|
+
* conditions (e.g. very short queries).
|
|
453
|
+
*
|
|
454
|
+
* Errors thrown by the adapter are fail-open: the framework
|
|
455
|
+
* logs a warning and falls back to pg. Sites that want
|
|
456
|
+
* fail-closed wrap their adapter in try/catch and return
|
|
457
|
+
* `null` on error.
|
|
458
|
+
*/
|
|
459
|
+
interface NpSearchAdapterContext {
|
|
460
|
+
/** Trimmed query string (already non-empty by the time this runs). */
|
|
461
|
+
q: string;
|
|
462
|
+
/** Subset of collection slugs the caller asked to search. */
|
|
463
|
+
collections?: string[];
|
|
464
|
+
/** Page size cap, already normalized. */
|
|
465
|
+
limit: number;
|
|
466
|
+
/** Skip count, already normalized. */
|
|
467
|
+
offset: number;
|
|
468
|
+
/**
|
|
469
|
+
* Phase 12.4 — locale to scope results to. When set, the
|
|
470
|
+
* framework expects only docs in this locale (for i18n
|
|
471
|
+
* collections) plus all docs from non-i18n collections. The
|
|
472
|
+
* default pg path applies a `locale = $1` filter on i18n
|
|
473
|
+
* collections; external adapters typically rebuild the index
|
|
474
|
+
* with one document per (sourceId, locale) and filter on the
|
|
475
|
+
* locale field. Adapters that don't support per-locale
|
|
476
|
+
* filtering can return `null` to fall through to pg.
|
|
477
|
+
*/
|
|
478
|
+
locale?: string;
|
|
479
|
+
}
|
|
480
|
+
interface NpSearchAdapter {
|
|
481
|
+
/**
|
|
482
|
+
* Implementation hook. Return a `SearchResult` to override the
|
|
483
|
+
* default pg tsvector path, or `null` / `undefined` to fall
|
|
484
|
+
* through. Throws are fail-open (logged + treated as null).
|
|
485
|
+
*/
|
|
486
|
+
search(ctx: NpSearchAdapterContext): Promise<SearchResult | null | undefined> | SearchResult | null | undefined;
|
|
487
|
+
}
|
|
488
|
+
/**
|
|
489
|
+
* Replace the global search adapter. Call once at app boot,
|
|
490
|
+
* typically from a plugin's `setup()`. The framework holds at
|
|
491
|
+
* most one adapter; sites that want to layer multiple engines
|
|
492
|
+
* (e.g. blog → Algolia, products → Meilisearch) compose them
|
|
493
|
+
* inside a single adapter and dispatch on `ctx.collections`.
|
|
494
|
+
*/
|
|
495
|
+
declare function setSearchAdapter(adapter: NpSearchAdapter): void;
|
|
496
|
+
declare function getSearchAdapter(): NpSearchAdapter | null;
|
|
497
|
+
/** Reset to no adapter. Tests use this between cases. */
|
|
498
|
+
declare function resetSearchAdapter(): void;
|
|
499
|
+
|
|
500
|
+
declare function buildZodSchema(fields: NpFieldConfig[]): z.ZodObject<Record<string, z.ZodTypeAny>>;
|
|
501
|
+
declare function getCollectionZodSchema(config: NpCollectionConfig): z.ZodSchema;
|
|
502
|
+
|
|
503
|
+
interface TranslationRow {
|
|
504
|
+
id: string;
|
|
505
|
+
locale: string;
|
|
506
|
+
slug: string;
|
|
507
|
+
status: string;
|
|
508
|
+
title?: unknown;
|
|
509
|
+
updatedAt?: Date | string | null;
|
|
510
|
+
translationGroupId: string;
|
|
511
|
+
}
|
|
512
|
+
/**
|
|
513
|
+
* Phase 12.3 — list every locale variant linked to the given
|
|
514
|
+
* document. The `translationGroupId` keys the sibling lookup;
|
|
515
|
+
* the originating row is included so callers can render a
|
|
516
|
+
* "current row + others" tab strip without a separate query.
|
|
517
|
+
*
|
|
518
|
+
* Returns rows in the locale order from `getI18nConfig()` so
|
|
519
|
+
* the UI's tab order matches the configured locale list rather
|
|
520
|
+
* than insertion order.
|
|
521
|
+
*/
|
|
522
|
+
declare function findTranslations(collection: string, docId: string): Promise<TranslationRow[]>;
|
|
523
|
+
/**
|
|
524
|
+
* Phase 12.3 — copy a doc into a new locale. The source row's
|
|
525
|
+
* data is reused minus `id` / `slug` / `locale` / status; the
|
|
526
|
+
* new row gets the source's `translationGroupId` so the two
|
|
527
|
+
* link as siblings. The pipeline's locale validation runs as
|
|
528
|
+
* usual (rejects unknown locales).
|
|
529
|
+
*
|
|
530
|
+
* The copy lands as a draft (`status: "draft"`) — translators
|
|
531
|
+
* almost always want to review before publishing. Promote
|
|
532
|
+
* normally via the existing publish flow once the translation
|
|
533
|
+
* is ready.
|
|
534
|
+
*
|
|
535
|
+
* Slug is intentionally NOT copied: `applySlugField` will
|
|
536
|
+
* derive a fresh one from the title (or whatever the configured
|
|
537
|
+
* `useField` is) so the (locale, slug) unique index doesn't
|
|
538
|
+
* collide if the source already used the slug in that locale.
|
|
539
|
+
* Callers can override post-create via the regular update path.
|
|
540
|
+
*/
|
|
541
|
+
declare function createTranslation(collection: string, sourceDocId: string, targetLocale: string, user: NpAuthUser): Promise<{
|
|
542
|
+
id: string;
|
|
543
|
+
}>;
|
|
544
|
+
/**
|
|
545
|
+
* Phase 12.6 — translation completeness snapshot for the
|
|
546
|
+
* admin Locales tab.
|
|
547
|
+
*
|
|
548
|
+
* Walks every i18n-enabled collection and counts:
|
|
549
|
+
* - `totalGroups` — distinct `translation_group_id` values
|
|
550
|
+
* (one per "logical document"; if the source has 5 base
|
|
551
|
+
* pages, that's 5 groups regardless of locale spread)
|
|
552
|
+
* - `counts[locale]` — actual rows per locale
|
|
553
|
+
* - `missing[locale]` — `totalGroups - counts[locale]`,
|
|
554
|
+
* i.e. how many groups still need this locale
|
|
555
|
+
*
|
|
556
|
+
* Returns `null` when i18n isn't configured. Non-i18n
|
|
557
|
+
* collections are silently skipped — they don't have the
|
|
558
|
+
* `translation_group_id` column and the dashboard isn't
|
|
559
|
+
* meaningful for them.
|
|
560
|
+
*
|
|
561
|
+
* One SQL round-trip per i18n collection (two GROUP BYs in a
|
|
562
|
+
* single query). For 1–2 i18n collections this is well under
|
|
563
|
+
* the cost of the existing dashboard widgets.
|
|
564
|
+
*/
|
|
565
|
+
interface NpTranslationProgressLocaleStats {
|
|
566
|
+
count: number;
|
|
567
|
+
missing: number;
|
|
568
|
+
}
|
|
569
|
+
interface NpCollectionTranslationProgress {
|
|
570
|
+
collection: string;
|
|
571
|
+
totalGroups: number;
|
|
572
|
+
perLocale: Record<string, NpTranslationProgressLocaleStats>;
|
|
573
|
+
}
|
|
574
|
+
interface NpTranslationProgress {
|
|
575
|
+
defaultLocale: string;
|
|
576
|
+
locales: string[];
|
|
577
|
+
collections: NpCollectionTranslationProgress[];
|
|
578
|
+
}
|
|
579
|
+
declare function getTranslationProgress(): Promise<NpTranslationProgress | null>;
|
|
580
|
+
|
|
581
|
+
interface NpThemeColors {
|
|
582
|
+
primary: string;
|
|
583
|
+
primaryForeground: string;
|
|
584
|
+
background: string;
|
|
585
|
+
foreground: string;
|
|
586
|
+
muted: string;
|
|
587
|
+
mutedForeground: string;
|
|
588
|
+
border: string;
|
|
589
|
+
card: string;
|
|
590
|
+
cardForeground: string;
|
|
591
|
+
accent: string;
|
|
592
|
+
accentForeground: string;
|
|
593
|
+
destructive: string;
|
|
594
|
+
destructiveForeground: string;
|
|
595
|
+
}
|
|
596
|
+
interface NpThemeTypography {
|
|
597
|
+
fontHeading: string;
|
|
598
|
+
fontBody: string;
|
|
599
|
+
fontMono: string;
|
|
600
|
+
fontSizeBase: string;
|
|
601
|
+
lineHeight: string;
|
|
602
|
+
fontSizeSm: string;
|
|
603
|
+
fontSizeLg: string;
|
|
604
|
+
fontSizeXl: string;
|
|
605
|
+
fontSize2xl: string;
|
|
606
|
+
fontSize3xl: string;
|
|
607
|
+
fontSize4xl: string;
|
|
608
|
+
}
|
|
609
|
+
interface NpThemeShape {
|
|
610
|
+
radiusSm: string;
|
|
611
|
+
radiusMd: string;
|
|
612
|
+
radiusLg: string;
|
|
613
|
+
radiusFull: string;
|
|
614
|
+
shadowSm: string;
|
|
615
|
+
shadowMd: string;
|
|
616
|
+
shadowLg: string;
|
|
617
|
+
}
|
|
618
|
+
interface NpThemeTokens {
|
|
619
|
+
colors: NpThemeColors;
|
|
620
|
+
typography: NpThemeTypography;
|
|
621
|
+
shape: NpThemeShape;
|
|
622
|
+
}
|
|
623
|
+
/**
|
|
624
|
+
* Author-facing partial token shape. Themes that override only a
|
|
625
|
+
* few colors / fonts / radii ship one of these via `defineTheme`'s
|
|
626
|
+
* `impl.tokens`. Each sub-tree is `Partial<...>` so a theme that
|
|
627
|
+
* sets only `colors.primary` doesn't have to copy the rest of
|
|
628
|
+
* `colors` from `DEFAULT_THEME`. The runtime merger
|
|
629
|
+
* (`getTheme()` in `content/helpers.ts`) layers an overlay onto
|
|
630
|
+
* `DEFAULT_THEME` field-by-field.
|
|
631
|
+
*/
|
|
632
|
+
interface NpThemeTokensOverlay {
|
|
633
|
+
colors?: Partial<NpThemeColors>;
|
|
634
|
+
typography?: Partial<NpThemeTypography>;
|
|
635
|
+
shape?: Partial<NpThemeShape>;
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
/**
|
|
639
|
+
* Resolves the effective theme tokens for the current site by
|
|
640
|
+
* layering three sources, last-writer-wins:
|
|
641
|
+
*
|
|
642
|
+
* 1. `DEFAULT_THEME` — framework baseline (always full).
|
|
643
|
+
* 2. The active theme's `impl.tokens` — author-shipped defaults
|
|
644
|
+
* that distinguish a theme from the framework baseline (e.g.
|
|
645
|
+
* magazine's warm cream palette, portfolio's dark surface).
|
|
646
|
+
* 3. The DB row in `np_settings.theme` — admin overrides via the
|
|
647
|
+
* theme settings tab.
|
|
648
|
+
*
|
|
649
|
+
* Each layer is `NpThemeTokensOverlay` (sub-tree-Partial) — themes
|
|
650
|
+
* only declare the keys they care about, admins only save deltas
|
|
651
|
+
* they edit. The merge ensures every emitted token has a value.
|
|
652
|
+
*
|
|
653
|
+
* This is the canonical token resolver — `apps/web`'s preview
|
|
654
|
+
* route calls it directly so the page-builder iframe matches what
|
|
655
|
+
* the public render produces for the same active theme.
|
|
656
|
+
*/
|
|
657
|
+
declare function getTheme(): Promise<NpThemeTokens>;
|
|
658
|
+
declare function getNavigation(location?: string): Promise<NpNavItem[]>;
|
|
659
|
+
declare function getPageBySlug(slug: string, options?: {
|
|
660
|
+
draft?: boolean;
|
|
661
|
+
locale?: string;
|
|
662
|
+
}): Promise<Record<string, unknown> | null>;
|
|
663
|
+
declare function getPostBySlug(slug: string, options?: {
|
|
664
|
+
draft?: boolean;
|
|
665
|
+
}): Promise<Record<string, unknown> | null>;
|
|
666
|
+
declare function findPosts(options: NpFindOptions, user?: NpAuthUser): Promise<NpFindResult>;
|
|
667
|
+
declare function getAllPageSlugs(): Promise<string[]>;
|
|
668
|
+
declare function findSlugRedirect(collection: string, oldSlug: string): Promise<string | null>;
|
|
669
|
+
declare function getSetting<T = unknown>(key: string): Promise<T | null>;
|
|
670
|
+
|
|
671
|
+
declare const npConfigSchema: z.ZodObject<{
|
|
672
|
+
site: z.ZodObject<{
|
|
673
|
+
name: z.ZodString;
|
|
674
|
+
url: z.ZodString;
|
|
675
|
+
}, z.core.$strip>;
|
|
676
|
+
db: z.ZodObject<{
|
|
677
|
+
connectionString: z.ZodString;
|
|
678
|
+
pool: z.ZodOptional<z.ZodObject<{
|
|
679
|
+
max: z.ZodOptional<z.ZodNumber>;
|
|
680
|
+
}, z.core.$strip>>;
|
|
681
|
+
}, z.core.$strip>;
|
|
682
|
+
storage: z.ZodOptional<z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
683
|
+
adapter: z.ZodLiteral<"local">;
|
|
684
|
+
local: z.ZodObject<{
|
|
685
|
+
directory: z.ZodString;
|
|
686
|
+
baseUrl: z.ZodString;
|
|
687
|
+
}, z.core.$strip>;
|
|
688
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
689
|
+
adapter: z.ZodLiteral<"s3">;
|
|
690
|
+
s3: z.ZodObject<{
|
|
691
|
+
bucket: z.ZodString;
|
|
692
|
+
region: z.ZodString;
|
|
693
|
+
endpoint: z.ZodOptional<z.ZodString>;
|
|
694
|
+
}, z.core.$strip>;
|
|
695
|
+
}, z.core.$strip>], "adapter">>;
|
|
696
|
+
collections: z.ZodArray<z.ZodLazy<z.ZodType<unknown, unknown, z.core.$ZodTypeInternals<unknown, unknown>>>>;
|
|
697
|
+
blocks: z.ZodOptional<z.ZodArray<z.ZodUnknown>>;
|
|
698
|
+
editor: z.ZodOptional<z.ZodObject<{
|
|
699
|
+
features: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
700
|
+
}, z.core.$strip>>;
|
|
701
|
+
images: z.ZodOptional<z.ZodObject<{
|
|
702
|
+
sizes: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
703
|
+
name: z.ZodString;
|
|
704
|
+
width: z.ZodNumber;
|
|
705
|
+
height: z.ZodOptional<z.ZodNumber>;
|
|
706
|
+
crop: z.ZodOptional<z.ZodEnum<{
|
|
707
|
+
center: "center";
|
|
708
|
+
top: "top";
|
|
709
|
+
bottom: "bottom";
|
|
710
|
+
left: "left";
|
|
711
|
+
right: "right";
|
|
712
|
+
}>>;
|
|
713
|
+
}, z.core.$strip>>>;
|
|
714
|
+
format: z.ZodOptional<z.ZodEnum<{
|
|
715
|
+
webp: "webp";
|
|
716
|
+
avif: "avif";
|
|
717
|
+
jpeg: "jpeg";
|
|
718
|
+
png: "png";
|
|
719
|
+
}>>;
|
|
720
|
+
quality: z.ZodOptional<z.ZodNumber>;
|
|
721
|
+
}, z.core.$strip>>;
|
|
722
|
+
auth: z.ZodOptional<z.ZodObject<{
|
|
723
|
+
secret: z.ZodString;
|
|
724
|
+
tokenExpiration: z.ZodOptional<z.ZodNumber>;
|
|
725
|
+
refreshTokenExpiration: z.ZodOptional<z.ZodNumber>;
|
|
726
|
+
maxLoginAttempts: z.ZodOptional<z.ZodNumber>;
|
|
727
|
+
lockoutDuration: z.ZodOptional<z.ZodNumber>;
|
|
728
|
+
}, z.core.$strip>>;
|
|
729
|
+
plugins: z.ZodOptional<z.ZodArray<z.ZodUnknown>>;
|
|
730
|
+
typescript: z.ZodOptional<z.ZodUnknown>;
|
|
731
|
+
themes: z.ZodOptional<z.ZodArray<z.ZodUnknown>>;
|
|
732
|
+
i18n: z.ZodOptional<z.ZodObject<{
|
|
733
|
+
locales: z.ZodArray<z.ZodString>;
|
|
734
|
+
defaultLocale: z.ZodString;
|
|
735
|
+
}, z.core.$strip>>;
|
|
736
|
+
}, z.core.$strip>;
|
|
737
|
+
declare const collectionConfigSchema: z.ZodObject<{
|
|
738
|
+
slug: z.ZodString;
|
|
739
|
+
labels: z.ZodObject<{
|
|
740
|
+
singular: z.ZodString;
|
|
741
|
+
plural: z.ZodString;
|
|
742
|
+
}, z.core.$strip>;
|
|
743
|
+
slugField: z.ZodOptional<z.ZodUnion<readonly [z.ZodBoolean, z.ZodObject<{
|
|
744
|
+
useField: z.ZodOptional<z.ZodString>;
|
|
745
|
+
unique: z.ZodOptional<z.ZodBoolean>;
|
|
746
|
+
}, z.core.$strip>]>>;
|
|
747
|
+
i18n: z.ZodOptional<z.ZodBoolean>;
|
|
748
|
+
fields: z.ZodArray<z.ZodType<unknown, unknown, z.core.$ZodTypeInternals<unknown, unknown>>>;
|
|
749
|
+
access: z.ZodOptional<z.ZodObject<{
|
|
750
|
+
create: z.ZodOptional<z.ZodCustom<(...args: unknown[]) => unknown, (...args: unknown[]) => unknown>>;
|
|
751
|
+
read: z.ZodOptional<z.ZodCustom<(...args: unknown[]) => unknown, (...args: unknown[]) => unknown>>;
|
|
752
|
+
update: z.ZodOptional<z.ZodCustom<(...args: unknown[]) => unknown, (...args: unknown[]) => unknown>>;
|
|
753
|
+
delete: z.ZodOptional<z.ZodCustom<(...args: unknown[]) => unknown, (...args: unknown[]) => unknown>>;
|
|
754
|
+
}, z.core.$strip>>;
|
|
755
|
+
hooks: z.ZodOptional<z.ZodObject<{
|
|
756
|
+
beforeCreate: z.ZodOptional<z.ZodArray<z.ZodCustom<(...args: unknown[]) => unknown, (...args: unknown[]) => unknown>>>;
|
|
757
|
+
afterCreate: z.ZodOptional<z.ZodArray<z.ZodCustom<(...args: unknown[]) => unknown, (...args: unknown[]) => unknown>>>;
|
|
758
|
+
beforeUpdate: z.ZodOptional<z.ZodArray<z.ZodCustom<(...args: unknown[]) => unknown, (...args: unknown[]) => unknown>>>;
|
|
759
|
+
afterUpdate: z.ZodOptional<z.ZodArray<z.ZodCustom<(...args: unknown[]) => unknown, (...args: unknown[]) => unknown>>>;
|
|
760
|
+
beforeDelete: z.ZodOptional<z.ZodArray<z.ZodCustom<(...args: unknown[]) => unknown, (...args: unknown[]) => unknown>>>;
|
|
761
|
+
afterDelete: z.ZodOptional<z.ZodArray<z.ZodCustom<(...args: unknown[]) => unknown, (...args: unknown[]) => unknown>>>;
|
|
762
|
+
beforeRead: z.ZodOptional<z.ZodArray<z.ZodCustom<(...args: unknown[]) => unknown, (...args: unknown[]) => unknown>>>;
|
|
763
|
+
afterRead: z.ZodOptional<z.ZodArray<z.ZodCustom<(...args: unknown[]) => unknown, (...args: unknown[]) => unknown>>>;
|
|
764
|
+
}, z.core.$strip>>;
|
|
765
|
+
versions: z.ZodOptional<z.ZodObject<{
|
|
766
|
+
drafts: z.ZodOptional<z.ZodUnion<readonly [z.ZodBoolean, z.ZodObject<{
|
|
767
|
+
autosave: z.ZodOptional<z.ZodBoolean>;
|
|
768
|
+
autosaveInterval: z.ZodOptional<z.ZodNumber>;
|
|
769
|
+
}, z.core.$strip>]>>;
|
|
770
|
+
max: z.ZodOptional<z.ZodNumber>;
|
|
771
|
+
}, z.core.$strip>>;
|
|
772
|
+
community: z.ZodOptional<z.ZodObject<{
|
|
773
|
+
comments: z.ZodOptional<z.ZodBoolean>;
|
|
774
|
+
memberWrite: z.ZodOptional<z.ZodObject<{
|
|
775
|
+
create: z.ZodOptional<z.ZodBoolean>;
|
|
776
|
+
update: z.ZodOptional<z.ZodBoolean>;
|
|
777
|
+
delete: z.ZodOptional<z.ZodBoolean>;
|
|
778
|
+
defaultStatus: z.ZodOptional<z.ZodEnum<{
|
|
779
|
+
published: "published";
|
|
780
|
+
pending: "pending";
|
|
781
|
+
}>>;
|
|
782
|
+
}, z.core.$strip>>;
|
|
783
|
+
}, z.core.$strip>>;
|
|
784
|
+
timestamps: z.ZodOptional<z.ZodBoolean>;
|
|
785
|
+
admin: z.ZodOptional<z.ZodObject<{
|
|
786
|
+
listColumns: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
787
|
+
defaultSort: z.ZodOptional<z.ZodString>;
|
|
788
|
+
group: z.ZodOptional<z.ZodString>;
|
|
789
|
+
hidden: z.ZodOptional<z.ZodBoolean>;
|
|
790
|
+
description: z.ZodOptional<z.ZodString>;
|
|
791
|
+
components: z.ZodOptional<z.ZodObject<{
|
|
792
|
+
listView: z.ZodOptional<z.ZodString>;
|
|
793
|
+
editView: z.ZodOptional<z.ZodString>;
|
|
794
|
+
createView: z.ZodOptional<z.ZodString>;
|
|
795
|
+
}, z.core.$strip>>;
|
|
796
|
+
}, z.core.$strip>>;
|
|
797
|
+
upload: z.ZodOptional<z.ZodObject<{
|
|
798
|
+
maxFileSize: z.ZodOptional<z.ZodNumber>;
|
|
799
|
+
allowedMimeTypes: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
800
|
+
imageSizes: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
801
|
+
name: z.ZodString;
|
|
802
|
+
width: z.ZodNumber;
|
|
803
|
+
height: z.ZodOptional<z.ZodNumber>;
|
|
804
|
+
crop: z.ZodOptional<z.ZodEnum<{
|
|
805
|
+
center: "center";
|
|
806
|
+
top: "top";
|
|
807
|
+
bottom: "bottom";
|
|
808
|
+
left: "left";
|
|
809
|
+
right: "right";
|
|
810
|
+
}>>;
|
|
811
|
+
}, z.core.$strip>>>;
|
|
812
|
+
}, z.core.$strip>>;
|
|
813
|
+
}, z.core.$strip>;
|
|
814
|
+
|
|
815
|
+
interface LocalStorageAdapterConfig {
|
|
816
|
+
directory: string;
|
|
817
|
+
baseUrl: string;
|
|
818
|
+
}
|
|
819
|
+
declare class LocalStorageAdapter implements NpStorageAdapter {
|
|
820
|
+
private readonly config;
|
|
821
|
+
constructor(config: LocalStorageAdapterConfig);
|
|
822
|
+
upload(key: string, data: Buffer | ReadableStream, _: NpFileMetadata): Promise<void>;
|
|
823
|
+
getStream(key: string): Promise<ReadableStream>;
|
|
824
|
+
getUrl(key: string): Promise<string>;
|
|
825
|
+
delete(key: string): Promise<void>;
|
|
826
|
+
exists(key: string): Promise<boolean>;
|
|
827
|
+
private resolvePath;
|
|
828
|
+
private normalizeBaseUrl;
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
interface S3StorageAdapterConfig {
|
|
832
|
+
bucket: string;
|
|
833
|
+
region: string;
|
|
834
|
+
endpoint?: string;
|
|
835
|
+
credentials?: {
|
|
836
|
+
accessKeyId: string;
|
|
837
|
+
secretAccessKey: string;
|
|
838
|
+
};
|
|
839
|
+
}
|
|
840
|
+
declare class S3StorageAdapter implements NpStorageAdapter {
|
|
841
|
+
private readonly config;
|
|
842
|
+
private clientPromise;
|
|
843
|
+
constructor(config: S3StorageAdapterConfig);
|
|
844
|
+
upload(key: string, data: Buffer | ReadableStream, metadata: NpFileMetadata): Promise<void>;
|
|
845
|
+
getStream(key: string): Promise<ReadableStream>;
|
|
846
|
+
getUrl(key: string): Promise<string>;
|
|
847
|
+
delete(key: string): Promise<void>;
|
|
848
|
+
exists(key: string): Promise<boolean>;
|
|
849
|
+
private getClient;
|
|
850
|
+
private createClient;
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
declare function createStorageAdapter(config: NpConfig["storage"]): NpStorageAdapter;
|
|
854
|
+
|
|
855
|
+
interface NpEmailMessage {
|
|
856
|
+
/** RFC-822 recipient address. Single recipient per message. */
|
|
857
|
+
to: string;
|
|
858
|
+
/** Plain-text subject line. */
|
|
859
|
+
subject: string;
|
|
860
|
+
/** Plain-text body. Always provided as a fallback. */
|
|
861
|
+
text: string;
|
|
862
|
+
/** Optional HTML body. Adapter may choose to omit if absent. */
|
|
863
|
+
html?: string;
|
|
864
|
+
/** Override the adapter's default From header when set. */
|
|
865
|
+
from?: string;
|
|
866
|
+
}
|
|
867
|
+
/**
|
|
868
|
+
* Transactional mailer contract. One method: deliver a single message.
|
|
869
|
+
* Adapters throw on failure so the pg-boss worker can retry per its
|
|
870
|
+
* configured policy. Success is a void resolve.
|
|
871
|
+
*/
|
|
872
|
+
interface NpEmailAdapter {
|
|
873
|
+
readonly kind: string;
|
|
874
|
+
send(message: NpEmailMessage): Promise<void>;
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
/**
|
|
878
|
+
* Default adapter used when no mailer is wired. Logs the message shape so a
|
|
879
|
+
* developer running the app locally can see that delivery was requested and
|
|
880
|
+
* follow the reset link from the logs without setting up SMTP.
|
|
881
|
+
*
|
|
882
|
+
* Swap via `setEmailAdapter(new SmtpEmailAdapter(...))` or a custom
|
|
883
|
+
* implementation in production.
|
|
884
|
+
*/
|
|
885
|
+
declare class NoopEmailAdapter implements NpEmailAdapter {
|
|
886
|
+
readonly kind = "noop";
|
|
887
|
+
send(message: NpEmailMessage): Promise<void>;
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
interface SmtpEmailAdapterOptions {
|
|
891
|
+
host: string;
|
|
892
|
+
port: number;
|
|
893
|
+
user?: string;
|
|
894
|
+
pass?: string;
|
|
895
|
+
/** Default `From` header when a message doesn't override. */
|
|
896
|
+
from: string;
|
|
897
|
+
/**
|
|
898
|
+
* Use implicit TLS (port 465) when `true`. When `false`, STARTTLS is
|
|
899
|
+
* negotiated (port 587 / 25). Defaults to `port === 465`.
|
|
900
|
+
*/
|
|
901
|
+
secure?: boolean;
|
|
902
|
+
}
|
|
903
|
+
/**
|
|
904
|
+
* Nodemailer-backed SMTP adapter. Works with any SMTP-speaking provider
|
|
905
|
+
* (Resend, SES, Mailgun, Postmark, Gmail, Zoho, custom relays).
|
|
906
|
+
*
|
|
907
|
+
* `nodemailer` is loaded dynamically on first send so that apps that don't
|
|
908
|
+
* use the SMTP adapter (noop or custom adapter) never pay its import cost.
|
|
909
|
+
*/
|
|
910
|
+
declare class SmtpEmailAdapter implements NpEmailAdapter {
|
|
911
|
+
readonly kind = "smtp";
|
|
912
|
+
private readonly options;
|
|
913
|
+
private transporter;
|
|
914
|
+
constructor(options: SmtpEmailAdapterOptions);
|
|
915
|
+
private ensureTransporter;
|
|
916
|
+
send(message: NpEmailMessage): Promise<void>;
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
declare function setEmailAdapter(next: NpEmailAdapter): void;
|
|
920
|
+
declare function getEmailAdapter(): NpEmailAdapter;
|
|
921
|
+
/** Reset to the built-in stub. Primarily used by tests. */
|
|
922
|
+
declare function resetEmailAdapter(): void;
|
|
923
|
+
|
|
924
|
+
interface NpPasswordResetTemplateData {
|
|
925
|
+
siteName: string;
|
|
926
|
+
name: string;
|
|
927
|
+
resetUrl: string;
|
|
928
|
+
/** When the link expires (ISO string), for display only. */
|
|
929
|
+
expiresAt?: string;
|
|
930
|
+
}
|
|
931
|
+
interface NpEmailTemplate {
|
|
932
|
+
subject: string;
|
|
933
|
+
text: string;
|
|
934
|
+
html: string;
|
|
935
|
+
}
|
|
936
|
+
declare function buildInviteEmail(data: NpPasswordResetTemplateData): NpEmailTemplate;
|
|
937
|
+
declare function buildResetEmail(data: NpPasswordResetTemplateData): NpEmailTemplate;
|
|
938
|
+
|
|
939
|
+
declare const DEFAULT_THEME: NpThemeTokens;
|
|
940
|
+
|
|
941
|
+
declare function sanitizeTokenValue(value: string): string;
|
|
942
|
+
|
|
943
|
+
/**
|
|
944
|
+
* Idempotent — call once at boot from the framework's
|
|
945
|
+
* bootstrap, again from a hot-reload, etc. Themes are matched
|
|
946
|
+
* by `manifest.id`; later registrations replace earlier ones.
|
|
947
|
+
*/
|
|
948
|
+
declare function registerThemes(themes: NpRegisteredTheme[]): void;
|
|
949
|
+
declare function getRegisteredThemes(): NpRegisteredTheme[];
|
|
950
|
+
declare function getThemeById(id: string): NpRegisteredTheme | undefined;
|
|
951
|
+
/** Tests use this between cases; production callers should never need it. */
|
|
952
|
+
declare function resetThemes(): void;
|
|
953
|
+
/**
|
|
954
|
+
* Reads the persisted active-theme id from `np_settings` for
|
|
955
|
+
* the current site. Returns `null` when no row exists —
|
|
956
|
+
* caller's job to decide the fallback (typically the first
|
|
957
|
+
* registered theme).
|
|
958
|
+
*
|
|
959
|
+
* Phase 15.4 — scoped by current site. Single-tenant
|
|
960
|
+
* deployments leave every row at `site_id = 'default'`, so
|
|
961
|
+
* the lookup behaves identically to the pre-15.4 global
|
|
962
|
+
* version.
|
|
963
|
+
*/
|
|
964
|
+
declare function getActiveThemeId(): Promise<string | null>;
|
|
965
|
+
/**
|
|
966
|
+
* Resolves the active theme to render. Looks up the persisted id
|
|
967
|
+
* in the registry; falls back to the first registered theme when
|
|
968
|
+
* the id is unset, missing, or points at a theme that's no
|
|
969
|
+
* longer in the registry (e.g. it was removed from
|
|
970
|
+
* `nexpress.config.ts` between deploys). Returns `null` only
|
|
971
|
+
* when the registry is completely empty.
|
|
972
|
+
*/
|
|
973
|
+
declare function getActiveTheme(): Promise<NpRegisteredTheme | null>;
|
|
974
|
+
/**
|
|
975
|
+
* Persist the active theme. Validates the id is registered so
|
|
976
|
+
* an admin can't pick a string that doesn't resolve to anything
|
|
977
|
+
* (which would silently fall back to the default and confuse
|
|
978
|
+
* the operator).
|
|
979
|
+
*/
|
|
980
|
+
declare function setActiveThemeId(id: string, updatedBy?: string | null): Promise<void>;
|
|
981
|
+
/**
|
|
982
|
+
* Phase 11.3 — surface the active theme's templates for a
|
|
983
|
+
* given collection so admin pickers and the catch-all renderer
|
|
984
|
+
* can introspect what's available without reaching into the
|
|
985
|
+
* opaque `impl` themselves. The result is sanitized for serial-
|
|
986
|
+
* ization (no React component refs leak through this path) so
|
|
987
|
+
* the same shape is safe to send over the API to the admin UI.
|
|
988
|
+
*/
|
|
989
|
+
interface NpThemeTemplateSummary {
|
|
990
|
+
id: string;
|
|
991
|
+
label: string;
|
|
992
|
+
description?: string;
|
|
993
|
+
}
|
|
994
|
+
declare function getThemeTemplateSummaries(collectionSlug: string): Promise<NpThemeTemplateSummary[]>;
|
|
995
|
+
/**
|
|
996
|
+
* Phase 14.5 — resolve a template's render component for the
|
|
997
|
+
* given collection + template id. Looks up theme first
|
|
998
|
+
* (theme always wins), falls back to plugin-contributed
|
|
999
|
+
* templates. Returns the opaque value (the catch-all casts to
|
|
1000
|
+
* `{ component }` at the render boundary).
|
|
1001
|
+
*/
|
|
1002
|
+
declare function resolveTemplateComponent(collectionSlug: string, templateId: string): Promise<unknown>;
|
|
1003
|
+
|
|
1004
|
+
/**
|
|
1005
|
+
* Phase F.1 — theme requirement check.
|
|
1006
|
+
*
|
|
1007
|
+
* Compares a theme's `manifest.requires` against the site's
|
|
1008
|
+
* registered collections and reports mismatches. The result
|
|
1009
|
+
* drives admin warnings on theme activation; F.8 will reuse
|
|
1010
|
+
* the same shape to drive the CLI patcher.
|
|
1011
|
+
*
|
|
1012
|
+
* "Hard" requirements (default) are normal mismatches the
|
|
1013
|
+
* operator should resolve. "Soft" requirements (`hard: false`)
|
|
1014
|
+
* are recorded separately so admin can show them at lower
|
|
1015
|
+
* severity — the theme renders without them but with degraded
|
|
1016
|
+
* behavior.
|
|
1017
|
+
*/
|
|
1018
|
+
interface NpThemeRequirementMissingField {
|
|
1019
|
+
collection: string;
|
|
1020
|
+
field: string;
|
|
1021
|
+
expected: NpThemeFieldRequirement;
|
|
1022
|
+
hard: boolean;
|
|
1023
|
+
}
|
|
1024
|
+
interface NpThemeRequirementTypeConflict {
|
|
1025
|
+
collection: string;
|
|
1026
|
+
field: string;
|
|
1027
|
+
expected: NpThemeFieldRequirement["type"];
|
|
1028
|
+
actual: string;
|
|
1029
|
+
hard: boolean;
|
|
1030
|
+
}
|
|
1031
|
+
interface NpThemeRequirementRelationConflict {
|
|
1032
|
+
collection: string;
|
|
1033
|
+
field: string;
|
|
1034
|
+
expected: NonNullable<NpThemeFieldRequirement["relationTo"]>;
|
|
1035
|
+
actual: string | string[];
|
|
1036
|
+
hard: boolean;
|
|
1037
|
+
}
|
|
1038
|
+
interface NpThemeRequirementResult {
|
|
1039
|
+
themeId: string;
|
|
1040
|
+
hasMismatches: boolean;
|
|
1041
|
+
/** Has at least one HARD mismatch — operator should resolve before activation. */
|
|
1042
|
+
hasHardMismatches: boolean;
|
|
1043
|
+
missingCollections: Array<{
|
|
1044
|
+
collection: string;
|
|
1045
|
+
createIfAbsent: boolean;
|
|
1046
|
+
}>;
|
|
1047
|
+
missingFields: NpThemeRequirementMissingField[];
|
|
1048
|
+
typeConflicts: NpThemeRequirementTypeConflict[];
|
|
1049
|
+
relationConflicts: NpThemeRequirementRelationConflict[];
|
|
1050
|
+
}
|
|
1051
|
+
declare function checkThemeRequirements(manifest: NpThemeManifest, collections: NpCollectionConfig[]): NpThemeRequirementResult;
|
|
1052
|
+
|
|
1053
|
+
/**
|
|
1054
|
+
* Read the persisted settings row for a theme and parse it via
|
|
1055
|
+
* the theme's schema. Returns the parsed value when valid;
|
|
1056
|
+
* falls back to schema defaults on parse failure (with the
|
|
1057
|
+
* failure recorded for the admin to surface, see
|
|
1058
|
+
* `getThemeSettingsWithStatus`).
|
|
1059
|
+
*
|
|
1060
|
+
* `themeId` defaults to the active theme. Pass an explicit id
|
|
1061
|
+
* to read another installed theme's settings (used by the
|
|
1062
|
+
* admin settings page).
|
|
1063
|
+
*
|
|
1064
|
+
* Return type is `unknown` because core can't type-narrow to
|
|
1065
|
+
* a specific theme's `z.infer<typeof schema>` — the schema
|
|
1066
|
+
* lives in the theme package, not in core. Theme components
|
|
1067
|
+
* should cast at the call site, ideally against an exported
|
|
1068
|
+
* type alias from the theme package itself:
|
|
1069
|
+
*
|
|
1070
|
+
* // packages/themes/magazine/src/index.ts
|
|
1071
|
+
* export const settingsSchema = z.object({ ... });
|
|
1072
|
+
* export type MagazineSettings = z.infer<typeof settingsSchema>;
|
|
1073
|
+
*
|
|
1074
|
+
* // a theme component
|
|
1075
|
+
* const settings = (await getThemeSettings()) as MagazineSettings;
|
|
1076
|
+
*/
|
|
1077
|
+
declare function getThemeSettings(themeId?: string): Promise<unknown>;
|
|
1078
|
+
interface NpThemeSettingsResult {
|
|
1079
|
+
themeId: string | null;
|
|
1080
|
+
/** Parsed settings or schema defaults (never null when a theme
|
|
1081
|
+
* has a schema; empty object when the theme has no schema). */
|
|
1082
|
+
value: unknown;
|
|
1083
|
+
/** True when there's a stored row, regardless of whether it
|
|
1084
|
+
* passed validation. */
|
|
1085
|
+
hasPersisted: boolean;
|
|
1086
|
+
/** Set when the persisted value failed `schema.parse()`. The
|
|
1087
|
+
* admin surface uses this to show a "values reset to
|
|
1088
|
+
* defaults" banner. */
|
|
1089
|
+
parseError?: string;
|
|
1090
|
+
}
|
|
1091
|
+
declare function getThemeSettingsWithStatus(themeId?: string): Promise<NpThemeSettingsResult>;
|
|
1092
|
+
/**
|
|
1093
|
+
* Validate and persist a theme's settings. Throws
|
|
1094
|
+
* `NpValidationError` when `value` doesn't pass the schema —
|
|
1095
|
+
* the admin form must surface field-level errors before
|
|
1096
|
+
* calling this.
|
|
1097
|
+
*
|
|
1098
|
+
* **Cache invalidation is the caller's responsibility.** This
|
|
1099
|
+
* function writes to `np_settings` only; it doesn't import
|
|
1100
|
+
* `next/cache`. The admin API route (`PUT
|
|
1101
|
+
* /api/admin/themes/[id]/settings`) busts `nx:theme:<siteId>`
|
|
1102
|
+
* (and `nx:sitemap:*` / `nx:feed:*` when `impl.seo` is
|
|
1103
|
+
* declared) after a successful write. Other callers — jobs,
|
|
1104
|
+
* scripts, server actions — must do the same to avoid stale
|
|
1105
|
+
* cached reads.
|
|
1106
|
+
*/
|
|
1107
|
+
declare function setThemeSettings(themeId: string, value: unknown, updatedBy?: string | null): Promise<unknown>;
|
|
1108
|
+
/**
|
|
1109
|
+
* Whether the active theme contributes SEO hooks. The settings
|
|
1110
|
+
* save path consults this to decide whether to additionally
|
|
1111
|
+
* invalidate `nx:sitemap:*` / `nx:feed:*` tags alongside the
|
|
1112
|
+
* always-busted `nx:theme:<siteId>`. Implemented here (in core)
|
|
1113
|
+
* so the API layer in `apps/web` doesn't need to duck-type the
|
|
1114
|
+
* theme `impl`.
|
|
1115
|
+
*/
|
|
1116
|
+
declare function activeThemeContributesSeo(): Promise<boolean>;
|
|
1117
|
+
|
|
1118
|
+
/**
|
|
1119
|
+
* Phase F.6 — extract the active theme's declared nav
|
|
1120
|
+
* locations as plain JSON metadata, narrowed structurally
|
|
1121
|
+
* because core treats `theme.impl` as opaque (`unknown`).
|
|
1122
|
+
*
|
|
1123
|
+
* Used by:
|
|
1124
|
+
* - The admin nav editor's location-dropdown endpoint
|
|
1125
|
+
* (`/api/navigation/locations`) so the dropdown surfaces
|
|
1126
|
+
* theme-named slots with friendly labels.
|
|
1127
|
+
* - Future cookbook recipes that surface "this theme
|
|
1128
|
+
* expects you to fill in these menus" guidance.
|
|
1129
|
+
*
|
|
1130
|
+
* Returns `[]` when no theme is active or the active theme
|
|
1131
|
+
* doesn't declare any locations.
|
|
1132
|
+
*/
|
|
1133
|
+
interface NpThemeNavLocationDescriptor {
|
|
1134
|
+
/** Location key, e.g. `"primary"`. Used as the `location`
|
|
1135
|
+
* argument to `getNavigation(...)` and the database row's
|
|
1136
|
+
* `(siteId, location)` lookup. */
|
|
1137
|
+
key: string;
|
|
1138
|
+
label: string;
|
|
1139
|
+
description?: string;
|
|
1140
|
+
maxItems?: number;
|
|
1141
|
+
}
|
|
1142
|
+
declare function getActiveThemeNavLocations(): Promise<NpThemeNavLocationDescriptor[]>;
|
|
1143
|
+
|
|
1144
|
+
/**
|
|
1145
|
+
* Phase F.7 — pure structural narrowers for theme `notFound`,
|
|
1146
|
+
* `error`, and `seo` contributions. Core treats `theme.impl`
|
|
1147
|
+
* as opaque (`unknown`); these helpers do the duck-typing in
|
|
1148
|
+
* one place so the consuming routes stay readable.
|
|
1149
|
+
*
|
|
1150
|
+
* `getActiveThemeNotFoundComponent` / `…ErrorComponent` return
|
|
1151
|
+
* the React component refs unchanged — typing as `unknown`
|
|
1152
|
+
* here, the consumers (in `apps/web`) cast to ComponentType
|
|
1153
|
+
* at the JSX site. Core can't import `react` directly without
|
|
1154
|
+
* dragging the peer into a server-only package, so the cast
|
|
1155
|
+
* is delegated.
|
|
1156
|
+
*/
|
|
1157
|
+
interface NpThemeSeoHooksExtracted {
|
|
1158
|
+
sitemapEntries?: () => Promise<NpSitemapEntry[]> | NpSitemapEntry[];
|
|
1159
|
+
feedEntries?: () => Promise<NpFeedEntry[]> | NpFeedEntry[];
|
|
1160
|
+
robotsTxt?: () => string | Promise<string>;
|
|
1161
|
+
}
|
|
1162
|
+
declare function extractNotFoundComponent(impl: unknown): unknown;
|
|
1163
|
+
declare function extractErrorComponent(impl: unknown): unknown;
|
|
1164
|
+
/**
|
|
1165
|
+
* Phase M.3 — member-tree 404 component, with fallback to the
|
|
1166
|
+
* top-level `impl.notFound`. Returns `null` when neither the
|
|
1167
|
+
* member-specific nor the top-level component is declared (the
|
|
1168
|
+
* caller renders the framework default). Same opacity contract
|
|
1169
|
+
* as `extractNotFoundComponent` — typed as `unknown` here, the
|
|
1170
|
+
* consumer in `apps/web/src/app/(member)/not-found.tsx` casts
|
|
1171
|
+
* to `ComponentType` at the JSX site.
|
|
1172
|
+
*/
|
|
1173
|
+
declare function extractMembersNotFoundComponent(impl: unknown): unknown;
|
|
1174
|
+
declare function extractSeoHooks(impl: unknown): NpThemeSeoHooksExtracted;
|
|
1175
|
+
/**
|
|
1176
|
+
* Async sugar over the active theme. Each helper returns a
|
|
1177
|
+
* fresh resolution per call; multi-site safety comes from
|
|
1178
|
+
* `getActiveTheme()` reading per-request site context.
|
|
1179
|
+
*/
|
|
1180
|
+
declare function getActiveThemeNotFound(): Promise<unknown>;
|
|
1181
|
+
declare function getActiveThemeError(): Promise<unknown>;
|
|
1182
|
+
/**
|
|
1183
|
+
* Phase M.3 — async sugar for the member-tree 404. Returns the
|
|
1184
|
+
* theme's `impl.members.notFound` when declared, falling back
|
|
1185
|
+
* to its `impl.notFound` (top-level), then `null`.
|
|
1186
|
+
*/
|
|
1187
|
+
declare function getActiveThemeMembersNotFound(): Promise<unknown>;
|
|
1188
|
+
declare function getActiveThemeSeoHooks(): Promise<NpThemeSeoHooksExtracted>;
|
|
1189
|
+
|
|
1190
|
+
/**
|
|
1191
|
+
* Phase F.3 — server-side introspection of a theme's
|
|
1192
|
+
* `settingsSchema` Zod tree into a JSON metadata shape the
|
|
1193
|
+
* admin form generator consumes.
|
|
1194
|
+
*
|
|
1195
|
+
* The schema lives in the theme package (server bundle); we
|
|
1196
|
+
* don't ship the schema itself to the browser. Instead, this
|
|
1197
|
+
* function walks the tree once on the server, emits the
|
|
1198
|
+
* metadata as plain JSON, and the admin renders form fields
|
|
1199
|
+
* from the metadata. The browser doesn't need zod at runtime.
|
|
1200
|
+
*
|
|
1201
|
+
* Coverage in v0.2: text, url, color (regex heuristic), number,
|
|
1202
|
+
* boolean, enum, array(object), object. Anything else
|
|
1203
|
+
* introspects as `{ type: "unsupported" }` so the form generator
|
|
1204
|
+
* can render a JSON textarea fallback (operator can still edit;
|
|
1205
|
+
* a follow-up phase widens coverage).
|
|
1206
|
+
*/
|
|
1207
|
+
type NpThemeSettingsField = NpThemeSettingsTextField | NpThemeSettingsTextareaField | NpThemeSettingsPasswordField | NpThemeSettingsUrlField | NpThemeSettingsColorField | NpThemeSettingsNumberField | NpThemeSettingsBooleanField | NpThemeSettingsEnumField | NpThemeSettingsArrayField | NpThemeSettingsStringArrayField | NpThemeSettingsObjectField | NpThemeSettingsUnsupportedField;
|
|
1208
|
+
interface NpThemeSettingsFieldBase {
|
|
1209
|
+
/** Field path key ("hero", "social.0.url", etc. — the
|
|
1210
|
+
* introspector returns flat keys per node; nested objects
|
|
1211
|
+
* carry their own children). */
|
|
1212
|
+
name: string;
|
|
1213
|
+
label?: string;
|
|
1214
|
+
description?: string;
|
|
1215
|
+
required: boolean;
|
|
1216
|
+
default?: unknown;
|
|
1217
|
+
}
|
|
1218
|
+
interface NpThemeSettingsTextField extends NpThemeSettingsFieldBase {
|
|
1219
|
+
type: "text";
|
|
1220
|
+
}
|
|
1221
|
+
interface NpThemeSettingsTextareaField extends NpThemeSettingsFieldBase {
|
|
1222
|
+
type: "textarea";
|
|
1223
|
+
/** Optional row count hint for the rendered `<textarea>`.
|
|
1224
|
+
* Theme authors set this via `.meta({ widget: "textarea",
|
|
1225
|
+
* rows: 6 })`. Defaults to 4 when unset. */
|
|
1226
|
+
rows?: number;
|
|
1227
|
+
}
|
|
1228
|
+
interface NpThemeSettingsPasswordField extends NpThemeSettingsFieldBase {
|
|
1229
|
+
type: "password";
|
|
1230
|
+
}
|
|
1231
|
+
interface NpThemeSettingsUrlField extends NpThemeSettingsFieldBase {
|
|
1232
|
+
type: "url";
|
|
1233
|
+
}
|
|
1234
|
+
interface NpThemeSettingsColorField extends NpThemeSettingsFieldBase {
|
|
1235
|
+
type: "color";
|
|
1236
|
+
}
|
|
1237
|
+
interface NpThemeSettingsNumberField extends NpThemeSettingsFieldBase {
|
|
1238
|
+
type: "number";
|
|
1239
|
+
int?: boolean;
|
|
1240
|
+
min?: number;
|
|
1241
|
+
max?: number;
|
|
1242
|
+
}
|
|
1243
|
+
interface NpThemeSettingsBooleanField extends NpThemeSettingsFieldBase {
|
|
1244
|
+
type: "boolean";
|
|
1245
|
+
}
|
|
1246
|
+
interface NpThemeSettingsEnumField extends NpThemeSettingsFieldBase {
|
|
1247
|
+
type: "enum";
|
|
1248
|
+
options: string[];
|
|
1249
|
+
}
|
|
1250
|
+
interface NpThemeSettingsArrayField extends NpThemeSettingsFieldBase {
|
|
1251
|
+
type: "array";
|
|
1252
|
+
/** v0.2 supports `z.array(z.object(...))`. The element
|
|
1253
|
+
* schema introspects as the array's child fields. */
|
|
1254
|
+
element: NpThemeSettingsField[];
|
|
1255
|
+
}
|
|
1256
|
+
/** Phase G follow-up — `z.array(z.string())`. Renders as a
|
|
1257
|
+
* one-item-per-line input. Surfaced for OAuth scopes and
|
|
1258
|
+
* similar string-list configs that don't fit the object-array
|
|
1259
|
+
* shape; previously fell through to the JSON-textarea
|
|
1260
|
+
* `unsupported` fallback. */
|
|
1261
|
+
interface NpThemeSettingsStringArrayField extends NpThemeSettingsFieldBase {
|
|
1262
|
+
type: "string-array";
|
|
1263
|
+
}
|
|
1264
|
+
interface NpThemeSettingsObjectField extends NpThemeSettingsFieldBase {
|
|
1265
|
+
type: "object";
|
|
1266
|
+
fields: NpThemeSettingsField[];
|
|
1267
|
+
}
|
|
1268
|
+
interface NpThemeSettingsUnsupportedField extends NpThemeSettingsFieldBase {
|
|
1269
|
+
type: "unsupported";
|
|
1270
|
+
/** Best-effort label for what was at this position so
|
|
1271
|
+
* operators can recognize their schema in the JSON fallback. */
|
|
1272
|
+
zodTypeName: string;
|
|
1273
|
+
}
|
|
1274
|
+
/**
|
|
1275
|
+
* Walk a theme's `settingsSchema` (top-level z.object) and emit
|
|
1276
|
+
* the form metadata. Returns an empty array when the schema
|
|
1277
|
+
* isn't a top-level object — themes are expected to ship
|
|
1278
|
+
* `settingsSchema: z.object({...})` (validated implicitly: a
|
|
1279
|
+
* non-object top schema yields an empty form, signalling
|
|
1280
|
+
* "nothing to configure").
|
|
1281
|
+
*/
|
|
1282
|
+
declare function introspectThemeSettingsSchema(schema: ZodTypeAny | undefined): NpThemeSettingsField[];
|
|
1283
|
+
|
|
1284
|
+
/**
|
|
1285
|
+
* Phase 14.5 — plugin-contributed page templates. Plugins
|
|
1286
|
+
* register templates the same way themes do (`{ label,
|
|
1287
|
+
* description?, component }` keyed by collection then by
|
|
1288
|
+
* template id) but the registry here is global, separate
|
|
1289
|
+
* from any one theme. The theme registry's
|
|
1290
|
+
* `getThemeTemplateSummaries` merges both sets so admin
|
|
1291
|
+
* pickers and the catch-all see a unified list; theme
|
|
1292
|
+
* registrations win on id collisions because the active
|
|
1293
|
+
* theme is the design authority for the site.
|
|
1294
|
+
*
|
|
1295
|
+
* The framework keeps `impl: unknown` on the public
|
|
1296
|
+
* registered-theme shape (no React peer dep in core), so
|
|
1297
|
+
* plugin template values stay typed as `unknown` here too.
|
|
1298
|
+
* The caller casts to the typed `NpThemeTemplate` shape
|
|
1299
|
+
* from `@nexpress/theme` at the render boundary.
|
|
1300
|
+
*/
|
|
1301
|
+
/**
|
|
1302
|
+
* Merge a single plugin's `templates` declaration into the
|
|
1303
|
+
* registry. Idempotent: re-registering the same plugin id
|
|
1304
|
+
* overwrites its previous entries.
|
|
1305
|
+
*/
|
|
1306
|
+
declare function registerPluginTemplates(pluginId: string, templates: Record<string, Record<string, unknown>>): void;
|
|
1307
|
+
/** Tests use this between cases; production callers shouldn't need it. */
|
|
1308
|
+
declare function resetPluginTemplates(): void;
|
|
1309
|
+
/**
|
|
1310
|
+
* Returns every plugin-registered template for a collection,
|
|
1311
|
+
* keyed by template id. The returned values are opaque
|
|
1312
|
+
* (`unknown`); consumers cast to the appropriate shape at the
|
|
1313
|
+
* call site (theme registry casts to summary metadata; the
|
|
1314
|
+
* catch-all casts to `{ component }`).
|
|
1315
|
+
*/
|
|
1316
|
+
declare function getPluginTemplatesForCollection(collectionSlug: string): Map<string, unknown>;
|
|
1317
|
+
|
|
1318
|
+
/**
|
|
1319
|
+
* Phase 15.1 — multi-site registry. The framework treats
|
|
1320
|
+
* sites as long-lived rows in `np_sites`; the bootstrap calls
|
|
1321
|
+
* `ensureDefaultSite()` at boot to guarantee at least one row
|
|
1322
|
+
* exists so single-tenant installs (the existing reference
|
|
1323
|
+
* app shape) keep working without operator intervention.
|
|
1324
|
+
*
|
|
1325
|
+
* 15.1 ships the model + lookup helpers; 15.2 wires
|
|
1326
|
+
* collection queries through `siteId`; 15.3 ships the
|
|
1327
|
+
* super-admin UI for creating / managing sites. Until 15.2
|
|
1328
|
+
* lands, nothing in the existing pipeline knows or cares
|
|
1329
|
+
* about which site a row belongs to — the columns just
|
|
1330
|
+
* exist and the default site backfills.
|
|
1331
|
+
*/
|
|
1332
|
+
interface NpSite {
|
|
1333
|
+
id: string;
|
|
1334
|
+
name: string;
|
|
1335
|
+
hostname: string | null;
|
|
1336
|
+
description: string | null;
|
|
1337
|
+
settings: Record<string, unknown>;
|
|
1338
|
+
isDefault: boolean;
|
|
1339
|
+
createdAt: Date;
|
|
1340
|
+
updatedAt: Date;
|
|
1341
|
+
}
|
|
1342
|
+
/**
|
|
1343
|
+
* Idempotently create the default site if no sites exist.
|
|
1344
|
+
* Bootstrap calls this once during framework init; tests
|
|
1345
|
+
* that truncate `np_sites` between cases re-trigger it.
|
|
1346
|
+
*/
|
|
1347
|
+
declare function ensureDefaultSite(): Promise<NpSite>;
|
|
1348
|
+
declare function listSites(): Promise<NpSite[]>;
|
|
1349
|
+
declare function getSiteById(id: string): Promise<NpSite | null>;
|
|
1350
|
+
/**
|
|
1351
|
+
* Hostname-based lookup. Returns the matching site, or the
|
|
1352
|
+
* default site when no row matches (so a request hitting
|
|
1353
|
+
* an unconfigured host still gets served by the canonical
|
|
1354
|
+
* site rather than 404'ing). Case-insensitive on the host
|
|
1355
|
+
* string.
|
|
1356
|
+
*/
|
|
1357
|
+
declare function getSiteByHostname(hostname: string): Promise<NpSite | null>;
|
|
1358
|
+
declare function getDefaultSite(): Promise<NpSite | null>;
|
|
1359
|
+
/**
|
|
1360
|
+
* Resolve which site a request belongs to. Tries hostname
|
|
1361
|
+
* lookup first; falls back to the default site. Returns
|
|
1362
|
+
* `null` only when the database has no sites at all (which
|
|
1363
|
+
* shouldn't happen post-bootstrap).
|
|
1364
|
+
*/
|
|
1365
|
+
declare function resolveSiteForHostname(hostname: string | null | undefined): Promise<NpSite | null>;
|
|
1366
|
+
interface CreateSiteInput {
|
|
1367
|
+
id: string;
|
|
1368
|
+
name: string;
|
|
1369
|
+
hostname?: string | null;
|
|
1370
|
+
description?: string | null;
|
|
1371
|
+
settings?: Record<string, unknown>;
|
|
1372
|
+
}
|
|
1373
|
+
declare function createSite(input: CreateSiteInput): Promise<NpSite>;
|
|
1374
|
+
declare function updateSite(id: string, patch: Partial<Pick<NpSite, "name" | "hostname" | "description" | "settings">>): Promise<NpSite>;
|
|
1375
|
+
/**
|
|
1376
|
+
* Phase 15.9 — count of every site-scoped row attached to a
|
|
1377
|
+
* given site. Surfaces in the admin delete-site dialog so
|
|
1378
|
+
* operators see what they're about to nuke (or leave behind
|
|
1379
|
+
* as orphans, in the cascade=false path).
|
|
1380
|
+
*
|
|
1381
|
+
* Includes:
|
|
1382
|
+
* - per-collection row counts (codegen'd `np_c_*` tables)
|
|
1383
|
+
* - system tables that carry `site_id`: settings,
|
|
1384
|
+
* navigation, memberships, string overrides, plugin
|
|
1385
|
+
* storage (Issue #220)
|
|
1386
|
+
* - community tables that carry `site_id`: comments,
|
|
1387
|
+
* reactions, follows, mutes, notifications, reports,
|
|
1388
|
+
* audit events, bans, member roles (Issue #220)
|
|
1389
|
+
*
|
|
1390
|
+
* Does NOT include things that aren't site-scoped:
|
|
1391
|
+
* - users (`np_users` is global)
|
|
1392
|
+
* - members (`np_members` is global; per-site enrollment
|
|
1393
|
+
* happens through the site-scoped `bans` / `member_roles`
|
|
1394
|
+
* tables which DO appear in usage)
|
|
1395
|
+
* - media (`np_media` is global)
|
|
1396
|
+
* - audit events with `site_id IS NULL` — those are
|
|
1397
|
+
* intentional super-admin / background-job events that
|
|
1398
|
+
* don't belong to any tenant.
|
|
1399
|
+
*/
|
|
1400
|
+
interface NpSiteUsage {
|
|
1401
|
+
collections: Record<string, number>;
|
|
1402
|
+
settings: number;
|
|
1403
|
+
navigation: number;
|
|
1404
|
+
memberships: number;
|
|
1405
|
+
stringOverrides: number;
|
|
1406
|
+
/** Issue #220 — newly-included site-scoped tables. */
|
|
1407
|
+
pluginStorage: number;
|
|
1408
|
+
comments: number;
|
|
1409
|
+
reactions: number;
|
|
1410
|
+
follows: number;
|
|
1411
|
+
mutes: number;
|
|
1412
|
+
notifications: number;
|
|
1413
|
+
reports: number;
|
|
1414
|
+
auditEvents: number;
|
|
1415
|
+
bans: number;
|
|
1416
|
+
memberRoles: number;
|
|
1417
|
+
/** Sum of every count above. Convenience for "is anything here?" checks. */
|
|
1418
|
+
total: number;
|
|
1419
|
+
}
|
|
1420
|
+
declare function getSiteUsageSummary(id: string): Promise<NpSiteUsage>;
|
|
1421
|
+
interface NpDeleteSiteOptions {
|
|
1422
|
+
/**
|
|
1423
|
+
* Phase 15.9 — when `true`, cascade-delete every site-scoped
|
|
1424
|
+
* row (collection content, settings, navigation, memberships,
|
|
1425
|
+
* string overrides) before dropping the `np_sites` row.
|
|
1426
|
+
*
|
|
1427
|
+
* When `false` (default, safe), the call refuses if any
|
|
1428
|
+
* site-scoped data still exists. The admin UI uses this to
|
|
1429
|
+
* force operators to confirm cascade explicitly so an
|
|
1430
|
+
* accidental delete can't quietly orphan thousands of rows.
|
|
1431
|
+
*/
|
|
1432
|
+
cascade?: boolean;
|
|
1433
|
+
}
|
|
1434
|
+
/**
|
|
1435
|
+
* Delete a non-default site. The default site can't be
|
|
1436
|
+
* deleted (the framework's invariant is "at least one site
|
|
1437
|
+
* always exists"); operators who want to retire the default
|
|
1438
|
+
* promote a different site to default first.
|
|
1439
|
+
*
|
|
1440
|
+
* Phase 15.9 — `options.cascade` controls whether site-scoped
|
|
1441
|
+
* data is deleted alongside. Defaults to `false` for safety;
|
|
1442
|
+
* the admin UI surfaces a usage summary first so the operator
|
|
1443
|
+
* sees what cascade would touch.
|
|
1444
|
+
*/
|
|
1445
|
+
declare function deleteSite(id: string, options?: NpDeleteSiteOptions): Promise<void>;
|
|
1446
|
+
declare const NP_DEFAULT_SITE_ID = "default";
|
|
1447
|
+
|
|
1448
|
+
/**
|
|
1449
|
+
* Phase 15.1 — process-wide "current site" resolver hook.
|
|
1450
|
+
*
|
|
1451
|
+
* The pipeline doesn't know how to find the current request's
|
|
1452
|
+
* site on its own (the runtime layer does — it reads the
|
|
1453
|
+
* `x-np-site-id` header the middleware sets). This module
|
|
1454
|
+
* exposes a setter the runtime calls at boot:
|
|
1455
|
+
*
|
|
1456
|
+
* setCurrentSiteResolver(async () => {
|
|
1457
|
+
* const headerList = await headers();
|
|
1458
|
+
* return headerList.get("x-np-site-id") ?? null;
|
|
1459
|
+
* });
|
|
1460
|
+
*
|
|
1461
|
+
* Pipeline / hooks call `getCurrentSiteId()` to read the
|
|
1462
|
+
* resolved id (or `null` when no resolver is wired, e.g.
|
|
1463
|
+
* background workers, scripts).
|
|
1464
|
+
*
|
|
1465
|
+
* This is intentionally async — the canonical Next.js
|
|
1466
|
+
* resolver awaits `headers()`. Sync paths (CLI, tests) can
|
|
1467
|
+
* register a sync resolver by returning the value directly.
|
|
1468
|
+
*/
|
|
1469
|
+
type Resolver = () => string | null | Promise<string | null>;
|
|
1470
|
+
declare function setCurrentSiteResolver(fn: Resolver | null): void;
|
|
1471
|
+
declare function resetCurrentSiteResolver(): void;
|
|
1472
|
+
declare function getCurrentSiteId(): Promise<string | null>;
|
|
1473
|
+
/**
|
|
1474
|
+
* Tests / scripts that want to pin the current site id for the
|
|
1475
|
+
* duration of a block use the `withCurrentSite` helper — it swaps
|
|
1476
|
+
* in a constant resolver, runs `fn`, and restores the previous
|
|
1477
|
+
* resolver on exit.
|
|
1478
|
+
*
|
|
1479
|
+
* Contract — read this carefully (#320):
|
|
1480
|
+
*
|
|
1481
|
+
* `withCurrentSite` covers ONLY work that completes (synchronously
|
|
1482
|
+
* or via `await`) before `fn` returns. Any fire-and-forget async
|
|
1483
|
+
* work spawned inside `fn` runs AFTER the `finally` block has
|
|
1484
|
+
* already restored the previous resolver, so it sees the OUTER
|
|
1485
|
+
* site context — typically `null` for a CLI / job, or the wrong
|
|
1486
|
+
* site for a request that was acting on a different tenant.
|
|
1487
|
+
*
|
|
1488
|
+
* Concretely:
|
|
1489
|
+
* - `enqueueJob(...)` persists the row immediately but the
|
|
1490
|
+
* handler runs later in the worker. The worker has no
|
|
1491
|
+
* resolver wired, so `getCurrentSiteId()` returns `null`
|
|
1492
|
+
* and `requireSiteId()` throws — even though the enqueuer
|
|
1493
|
+
* was inside a `withCurrentSite` block.
|
|
1494
|
+
* - `void someAsyncFn()` patterns inside `fn` are similarly
|
|
1495
|
+
* exposed.
|
|
1496
|
+
*
|
|
1497
|
+
* How to do it safely:
|
|
1498
|
+
* - Stamp `siteId` explicitly onto every job payload at
|
|
1499
|
+
* enqueue time. The handler reads it back from the payload
|
|
1500
|
+
* and wraps its own work in `withCurrentSite(payload.siteId,
|
|
1501
|
+
* handlerBody)`.
|
|
1502
|
+
* - `await` everything that needs the site context inside
|
|
1503
|
+
* `fn`. Don't return from `fn` while a site-dependent
|
|
1504
|
+
* operation is still pending.
|
|
1505
|
+
*
|
|
1506
|
+
* This is a fundamental limit of plain module-scoped state. A
|
|
1507
|
+
* future refactor could switch the resolver to
|
|
1508
|
+
* `AsyncLocalStorage` so the site follows the async boundary
|
|
1509
|
+
* automatically — that's tracked under #320 but out of scope
|
|
1510
|
+
* for this helper today.
|
|
1511
|
+
*/
|
|
1512
|
+
declare function withCurrentSite<T>(siteId: string, fn: () => Promise<T>): Promise<T>;
|
|
1513
|
+
|
|
1514
|
+
/**
|
|
1515
|
+
* Phase 15.5 — per-site role memberships.
|
|
1516
|
+
*
|
|
1517
|
+
* `npUsers.role` stays the "global default role" (used by
|
|
1518
|
+
* existing single-tenant code and as a fallback when no
|
|
1519
|
+
* explicit membership exists for a given site). New
|
|
1520
|
+
* multi-tenant deployments grant explicit memberships via
|
|
1521
|
+
* the helpers here; the `isSuperAdmin` flag (also new in
|
|
1522
|
+
* 15.5) bypasses membership checks entirely so a super-admin
|
|
1523
|
+
* can administer every site without having to be enrolled
|
|
1524
|
+
* on each one individually.
|
|
1525
|
+
*
|
|
1526
|
+
* The framework's existing `hasRole(user, minRole)` keeps
|
|
1527
|
+
* working as a global check. New site-scoped checks should
|
|
1528
|
+
* use `hasRoleOnSite(user, siteId, minRole)`.
|
|
1529
|
+
*/
|
|
1530
|
+
interface SiteMembership {
|
|
1531
|
+
siteId: string;
|
|
1532
|
+
userId: string;
|
|
1533
|
+
role: NpUserRole;
|
|
1534
|
+
createdAt: Date;
|
|
1535
|
+
updatedAt: Date;
|
|
1536
|
+
}
|
|
1537
|
+
declare function listSiteMemberships(siteId: string): Promise<SiteMembership[]>;
|
|
1538
|
+
declare function listMembershipsForUser(userId: string): Promise<SiteMembership[]>;
|
|
1539
|
+
declare function getMembership(siteId: string, userId: string): Promise<SiteMembership | null>;
|
|
1540
|
+
declare function grantSiteMembership(siteId: string, userId: string, role: NpUserRole): Promise<SiteMembership>;
|
|
1541
|
+
declare function revokeSiteMembership(siteId: string, userId: string): Promise<void>;
|
|
1542
|
+
/**
|
|
1543
|
+
* Promote / demote a user's super-admin status. Super-admins
|
|
1544
|
+
* bypass per-site membership checks; this is the framework's
|
|
1545
|
+
* "I can do anything" gate so it should be granted sparingly
|
|
1546
|
+
* (one or two operators per deployment, typically).
|
|
1547
|
+
*/
|
|
1548
|
+
declare function setSuperAdmin(userId: string, isSuperAdmin: boolean): Promise<void>;
|
|
1549
|
+
/**
|
|
1550
|
+
* Resolve a user's effective role on a specific site:
|
|
1551
|
+
* 1. Super-admins always get `admin`.
|
|
1552
|
+
* 2. Explicit site membership wins over the global role.
|
|
1553
|
+
* 3. Fallback: the user's global `npUsers.role` (preserves
|
|
1554
|
+
* single-tenant behavior).
|
|
1555
|
+
*
|
|
1556
|
+
* Use this rather than `user.role` for any check that should
|
|
1557
|
+
* respect tenant boundaries.
|
|
1558
|
+
*/
|
|
1559
|
+
declare function resolveUserRoleOnSite(user: NpAuthUser, siteId: string): Promise<NpUserRole>;
|
|
1560
|
+
/**
|
|
1561
|
+
* Site-scoped variant of `hasRole`. Resolves the user's
|
|
1562
|
+
* effective role on the site (super-admin → admin, explicit
|
|
1563
|
+
* membership → that role, otherwise global default) and
|
|
1564
|
+
* compares against `minRole` using the same rank order
|
|
1565
|
+
* `hasRole` uses.
|
|
1566
|
+
*
|
|
1567
|
+
* Defaults to the current request site (or the framework's
|
|
1568
|
+
* `default` site when no resolver is wired) so callers don't
|
|
1569
|
+
* have to thread siteId everywhere.
|
|
1570
|
+
*/
|
|
1571
|
+
declare function hasRoleOnSite(user: NpAuthUser, minRole: NpUserRole, siteId?: string): Promise<boolean>;
|
|
1572
|
+
/**
|
|
1573
|
+
* Quick boolean check for super-admin status. Cheaper than
|
|
1574
|
+
* `resolveUserRoleOnSite` when the caller only needs to know
|
|
1575
|
+
* "can this user manage the framework as a whole?".
|
|
1576
|
+
*/
|
|
1577
|
+
declare function isSuperAdmin(user: NpAuthUser): Promise<boolean>;
|
|
1578
|
+
|
|
1579
|
+
declare function isPluginEnabled(pluginId: string): Promise<boolean>;
|
|
1580
|
+
declare function invalidatePluginEnabled(pluginId: string): void;
|
|
1581
|
+
|
|
1582
|
+
interface PluginHookHandler {
|
|
1583
|
+
pluginId: string;
|
|
1584
|
+
/**
|
|
1585
|
+
* Returns `void` for fire-and-forget hooks (most of them). Render / extension
|
|
1586
|
+
* hooks may return a value; `runHookAndCollect` gathers those, while
|
|
1587
|
+
* `runHook` ignores returns.
|
|
1588
|
+
*/
|
|
1589
|
+
handler: (data: Record<string, unknown>) => unknown;
|
|
1590
|
+
/**
|
|
1591
|
+
* Lower priority runs first. Default `100`, leaving headroom in both
|
|
1592
|
+
* directions: a plugin that wants to observe AFTER everyone else picks
|
|
1593
|
+
* `200`, one that needs to mutate the payload first picks `0`. Stable
|
|
1594
|
+
* ordering: ties keep registration order (which is itself topo-sorted by
|
|
1595
|
+
* plugin `requires`).
|
|
1596
|
+
*/
|
|
1597
|
+
priority: number;
|
|
1598
|
+
/**
|
|
1599
|
+
* Per-handler timeout in milliseconds. When the handler doesn't settle
|
|
1600
|
+
* within the budget, `dispatchHookHandler` treats it as a failure —
|
|
1601
|
+
* logged and reported the same way a thrown error is. The remaining
|
|
1602
|
+
* handlers continue. `undefined` means "no timeout enforced", which is
|
|
1603
|
+
* the default for fire-and-forget hooks (`runHook`); render-collecting
|
|
1604
|
+
* hooks may want a tighter budget (e.g. 250ms) so a slow plugin can't
|
|
1605
|
+
* stall page rendering.
|
|
1606
|
+
*/
|
|
1607
|
+
timeoutMs?: number;
|
|
1608
|
+
}
|
|
1609
|
+
interface PluginRouteHandler {
|
|
1610
|
+
pluginId: string;
|
|
1611
|
+
path: string;
|
|
1612
|
+
method: string;
|
|
1613
|
+
/** When true, the dispatcher must verify a staff session before
|
|
1614
|
+
* invoking `handler` and pass the resolved user as `req.user`.
|
|
1615
|
+
* When false (default), the route is publicly reachable. */
|
|
1616
|
+
auth: boolean;
|
|
1617
|
+
handler: (req: PluginRouteRequest) => Promise<PluginRouteResponse>;
|
|
1618
|
+
}
|
|
1619
|
+
interface PluginRouteRequest {
|
|
1620
|
+
method: string;
|
|
1621
|
+
path: string;
|
|
1622
|
+
params: Record<string, string>;
|
|
1623
|
+
query: Record<string, string>;
|
|
1624
|
+
body: unknown;
|
|
1625
|
+
headers: Record<string, string>;
|
|
1626
|
+
user?: {
|
|
1627
|
+
id: string;
|
|
1628
|
+
email: string;
|
|
1629
|
+
role: string;
|
|
1630
|
+
};
|
|
1631
|
+
}
|
|
1632
|
+
interface PluginRouteResponse {
|
|
1633
|
+
status: number;
|
|
1634
|
+
body?: unknown;
|
|
1635
|
+
headers?: Record<string, string>;
|
|
1636
|
+
}
|
|
1637
|
+
/**
|
|
1638
|
+
* Declarative admin extension snapshot stored per registration. Shape mirrors
|
|
1639
|
+
* `@nexpress/plugin-sdk`'s `NpAdminExtension` but kept structural here to
|
|
1640
|
+
* avoid a plugin-sdk → core cycle. The admin UI reads this via
|
|
1641
|
+
* `getPluginAdminExtension(id)` and renders it with its own primitives.
|
|
1642
|
+
*/
|
|
1643
|
+
interface PluginAdminExtension {
|
|
1644
|
+
settings?: {
|
|
1645
|
+
title?: string;
|
|
1646
|
+
description?: string;
|
|
1647
|
+
fields: NpFieldConfig[];
|
|
1648
|
+
};
|
|
1649
|
+
widgets?: Array<{
|
|
1650
|
+
id: string;
|
|
1651
|
+
label: string;
|
|
1652
|
+
kind: "metric" | "status";
|
|
1653
|
+
actionId: string;
|
|
1654
|
+
description?: string;
|
|
1655
|
+
}>;
|
|
1656
|
+
actions?: Array<{
|
|
1657
|
+
id: string;
|
|
1658
|
+
label: string;
|
|
1659
|
+
actionId: string;
|
|
1660
|
+
confirm?: string;
|
|
1661
|
+
description?: string;
|
|
1662
|
+
}>;
|
|
1663
|
+
tables?: Array<{
|
|
1664
|
+
id: string;
|
|
1665
|
+
label: string;
|
|
1666
|
+
columns: Array<{
|
|
1667
|
+
name: string;
|
|
1668
|
+
label: string;
|
|
1669
|
+
}>;
|
|
1670
|
+
rowsActionId: string;
|
|
1671
|
+
emptyMessage?: string;
|
|
1672
|
+
}>;
|
|
1673
|
+
collectionTabs?: Array<{
|
|
1674
|
+
id: string;
|
|
1675
|
+
label: string;
|
|
1676
|
+
collections: string[] | "*";
|
|
1677
|
+
widgets?: Array<{
|
|
1678
|
+
id: string;
|
|
1679
|
+
label: string;
|
|
1680
|
+
kind: "metric" | "status";
|
|
1681
|
+
actionId: string;
|
|
1682
|
+
description?: string;
|
|
1683
|
+
}>;
|
|
1684
|
+
actions?: Array<{
|
|
1685
|
+
id: string;
|
|
1686
|
+
label: string;
|
|
1687
|
+
actionId: string;
|
|
1688
|
+
confirm?: string;
|
|
1689
|
+
description?: string;
|
|
1690
|
+
}>;
|
|
1691
|
+
description?: string;
|
|
1692
|
+
}>;
|
|
1693
|
+
dashboardWidgets?: Array<{
|
|
1694
|
+
id: string;
|
|
1695
|
+
label: string;
|
|
1696
|
+
kind: "metric" | "status";
|
|
1697
|
+
actionId: string;
|
|
1698
|
+
description?: string;
|
|
1699
|
+
priority?: number;
|
|
1700
|
+
}>;
|
|
1701
|
+
}
|
|
1702
|
+
/**
|
|
1703
|
+
* Phase 19 — first-class plugin cron schedules. Plugins
|
|
1704
|
+
* declare `scheduled: [{ id, cron, handler }]` in their
|
|
1705
|
+
* definition; the host stores the list here and pg-boss
|
|
1706
|
+
* registers one recurring schedule per entry. The handler
|
|
1707
|
+
* runs in the same context shape `setup()` saw, so plugins
|
|
1708
|
+
* already familiar with `ctx.content` / `ctx.storage` /
|
|
1709
|
+
* `ctx.next` use the same surface from a cron tick.
|
|
1710
|
+
*/
|
|
1711
|
+
interface PluginScheduleHandler {
|
|
1712
|
+
pluginId: string;
|
|
1713
|
+
taskId: string;
|
|
1714
|
+
cron: string;
|
|
1715
|
+
description?: string;
|
|
1716
|
+
handler: (ctx: Record<string, unknown>) => unknown;
|
|
1717
|
+
}
|
|
1718
|
+
interface PluginRegistration {
|
|
1719
|
+
id: string;
|
|
1720
|
+
name: string;
|
|
1721
|
+
version?: string;
|
|
1722
|
+
description?: string;
|
|
1723
|
+
capabilities: readonly string[];
|
|
1724
|
+
allowedHosts: readonly string[];
|
|
1725
|
+
admin?: PluginAdminExtension;
|
|
1726
|
+
hooks: Map<string, PluginHookHandler[]>;
|
|
1727
|
+
routes: PluginRouteHandler[];
|
|
1728
|
+
actions: Map<string, (data: unknown) => Promise<{
|
|
1729
|
+
ok: boolean;
|
|
1730
|
+
data?: unknown;
|
|
1731
|
+
error?: string;
|
|
1732
|
+
}>>;
|
|
1733
|
+
schedules: Map<string, PluginScheduleHandler>;
|
|
1734
|
+
/**
|
|
1735
|
+
* G.1 — Zod schema describing the plugin's operator-tunable
|
|
1736
|
+
* config. Read by `getPluginConfig` (introspection +
|
|
1737
|
+
* validation) and the admin auto-form. `unknown` here so the
|
|
1738
|
+
* host doesn't pull a hard zod dep into its type surface;
|
|
1739
|
+
* narrowed at the call site (same pattern theme uses for
|
|
1740
|
+
* `settingsSchema`).
|
|
1741
|
+
*/
|
|
1742
|
+
configSchema?: unknown;
|
|
1743
|
+
/** G.1 — schema version (defaults to 1). See plugin-sdk types. */
|
|
1744
|
+
configVersion?: number;
|
|
1745
|
+
/** G.1 — migration callback for v(N-1) → current. */
|
|
1746
|
+
configMigrate?: (old: unknown, fromVersion: number) => unknown;
|
|
1747
|
+
/**
|
|
1748
|
+
* Plugin-contributed page routes (#623). Stored as the same
|
|
1749
|
+
* shape the SDK accepts (component + optional metadata as
|
|
1750
|
+
* `unknown`); the route-dispatcher in `@nexpress/next`
|
|
1751
|
+
* narrows them at render time. See
|
|
1752
|
+
* `docs/design/plugin-routes.md` for precedence + surface
|
|
1753
|
+
* semantics.
|
|
1754
|
+
*/
|
|
1755
|
+
pageRoutes: readonly PluginPageRouteEntry[];
|
|
1756
|
+
}
|
|
1757
|
+
interface PluginPageRouteEntry {
|
|
1758
|
+
pattern: string;
|
|
1759
|
+
component: unknown;
|
|
1760
|
+
metadata?: unknown;
|
|
1761
|
+
surface: "site" | "member";
|
|
1762
|
+
locale: "auto" | "none";
|
|
1763
|
+
}
|
|
1764
|
+
/**
|
|
1765
|
+
* Structural shape for plugins built via `@nexpress/plugin-sdk`'s
|
|
1766
|
+
* `definePlugin()`. Matches `NpResolvedPluginLike` in config/types.ts —
|
|
1767
|
+
* kept deliberately loose so `loadPlugins` can accept the same array
|
|
1768
|
+
* that `NpConfig.plugins` does without narrowing gymnastics.
|
|
1769
|
+
*/
|
|
1770
|
+
interface ResolvedPluginLike {
|
|
1771
|
+
manifest: {
|
|
1772
|
+
id: string;
|
|
1773
|
+
name: string;
|
|
1774
|
+
version?: string;
|
|
1775
|
+
description?: string;
|
|
1776
|
+
capabilities: readonly string[];
|
|
1777
|
+
allowedHosts?: readonly string[];
|
|
1778
|
+
/**
|
|
1779
|
+
* Compatibility range for the framework. The plugin loads only when
|
|
1780
|
+
* `nexpress.minVersion <= host <= nexpress.maxVersion?` (inclusive).
|
|
1781
|
+
* Optional here so legacy / hand-rolled plugins keep loading; the
|
|
1782
|
+
* plugin-sdk schema requires it for new plugins.
|
|
1783
|
+
*/
|
|
1784
|
+
nexpress?: {
|
|
1785
|
+
minVersion?: string;
|
|
1786
|
+
maxVersion?: string;
|
|
1787
|
+
};
|
|
1788
|
+
/**
|
|
1789
|
+
* IDs of other plugins that must load first. The host topo-sorts the
|
|
1790
|
+
* load list so this plugin's `setup()` can assume its prerequisites
|
|
1791
|
+
* have already registered hooks/actions/blocks.
|
|
1792
|
+
*/
|
|
1793
|
+
requires?: readonly string[];
|
|
1794
|
+
};
|
|
1795
|
+
hooks?: Record<string, unknown>;
|
|
1796
|
+
routes?: ReadonlyArray<{
|
|
1797
|
+
path: string;
|
|
1798
|
+
method: string;
|
|
1799
|
+
handler: unknown;
|
|
1800
|
+
description?: string;
|
|
1801
|
+
auth?: boolean;
|
|
1802
|
+
}>;
|
|
1803
|
+
admin?: PluginAdminExtension;
|
|
1804
|
+
/** G.1 — runtime zod schema for plugin config (auto-form). */
|
|
1805
|
+
configSchema?: unknown;
|
|
1806
|
+
/** G.1 — schema version for the lazy migration pipeline. */
|
|
1807
|
+
configVersion?: number;
|
|
1808
|
+
/** G.1 — old → current value migrator. */
|
|
1809
|
+
configMigrate?: (old: unknown, fromVersion: number) => unknown;
|
|
1810
|
+
}
|
|
1811
|
+
declare function loadPlugins(plugins: Array<NpPluginConfig | ResolvedPluginLike>): Promise<void>;
|
|
1812
|
+
declare function runHook(hookName: string, data: Record<string, unknown>): Promise<void>;
|
|
1813
|
+
/**
|
|
1814
|
+
* Like `runHook`, but collects every non-null/undefined return value from
|
|
1815
|
+
* registered handlers. Used by render extension points (`render:beforePage`,
|
|
1816
|
+
* etc.) where each plugin contributes structured data — head tags, scripts —
|
|
1817
|
+
* that the renderer aggregates into a single output.
|
|
1818
|
+
*
|
|
1819
|
+
* Handlers that throw are isolated (logged + reported, then skipped). A
|
|
1820
|
+
* broken plugin contributing meta tags is allowed to fail silently so the
|
|
1821
|
+
* page itself still ships — incomplete SEO output beats a 500.
|
|
1822
|
+
*/
|
|
1823
|
+
declare function runHookAndCollect<T>(hookName: string, data: Record<string, unknown>): Promise<T[]>;
|
|
1824
|
+
declare function getPluginRoutes(): PluginRouteHandler[];
|
|
1825
|
+
/**
|
|
1826
|
+
* Plugin page routes (#623). Returns the flat list of registered
|
|
1827
|
+
* routes from EVERY loaded plugin in registration order, regardless
|
|
1828
|
+
* of enabled state — call sites that care about enabled gating
|
|
1829
|
+
* (e.g. the route dispatcher) walk the list and re-check via
|
|
1830
|
+
* `isPluginEnabled(pluginId)`. Keeping the gate at the call site
|
|
1831
|
+
* means tests can assert the registered shape without mocking the
|
|
1832
|
+
* enabled-state singleton.
|
|
1833
|
+
*/
|
|
1834
|
+
declare function getPluginPageRoutes(): Array<{
|
|
1835
|
+
pluginId: string;
|
|
1836
|
+
route: PluginPageRouteEntry;
|
|
1837
|
+
}>;
|
|
1838
|
+
declare function getPluginRegistration(pluginId: string): PluginRegistration | undefined;
|
|
1839
|
+
declare function getAllPluginIds(): string[];
|
|
1840
|
+
declare function getPluginAdminExtension(pluginId: string): PluginAdminExtension | undefined;
|
|
1841
|
+
/**
|
|
1842
|
+
* Resolved collection-tab descriptor for the admin collection edit view.
|
|
1843
|
+
* Each entry carries pluginId + pluginName so the client component can
|
|
1844
|
+
* dispatch actions and label cards per-plugin.
|
|
1845
|
+
*/
|
|
1846
|
+
interface ResolvedCollectionTab {
|
|
1847
|
+
pluginId: string;
|
|
1848
|
+
pluginName: string;
|
|
1849
|
+
id: string;
|
|
1850
|
+
label: string;
|
|
1851
|
+
widgets?: NonNullable<PluginAdminExtension["collectionTabs"]>[number]["widgets"];
|
|
1852
|
+
actions?: NonNullable<PluginAdminExtension["collectionTabs"]>[number]["actions"];
|
|
1853
|
+
description?: string;
|
|
1854
|
+
}
|
|
1855
|
+
/**
|
|
1856
|
+
* Collects all `collectionTabs` entries declared by loaded plugins whose
|
|
1857
|
+
* `collections` filter matches the given slug (either `"*"` or includes it).
|
|
1858
|
+
* The returned array is already flattened and annotated with the source
|
|
1859
|
+
* plugin, ready to pass into the admin edit view.
|
|
1860
|
+
*/
|
|
1861
|
+
declare function getCollectionTabsForSlug(collectionSlug: string): ResolvedCollectionTab[];
|
|
1862
|
+
/**
|
|
1863
|
+
* Dashboard widget descriptor annotated with its source plugin. The admin
|
|
1864
|
+
* dashboard dispatches the widget's action with an empty payload — dashboard
|
|
1865
|
+
* widgets are global, not per-document.
|
|
1866
|
+
*/
|
|
1867
|
+
interface ResolvedDashboardWidget {
|
|
1868
|
+
pluginId: string;
|
|
1869
|
+
pluginName: string;
|
|
1870
|
+
id: string;
|
|
1871
|
+
label: string;
|
|
1872
|
+
kind: "metric" | "status";
|
|
1873
|
+
actionId: string;
|
|
1874
|
+
description?: string;
|
|
1875
|
+
priority?: number;
|
|
1876
|
+
}
|
|
1877
|
+
/**
|
|
1878
|
+
* Collects `dashboardWidgets` declared by every loaded plugin and returns
|
|
1879
|
+
* them in render order: `priority` asc (missing priority = Infinity, i.e.
|
|
1880
|
+
* rendered last), ties broken by plugin registration order.
|
|
1881
|
+
*/
|
|
1882
|
+
declare function getDashboardWidgetsFromPlugins(): ResolvedDashboardWidget[];
|
|
1883
|
+
/**
|
|
1884
|
+
* Dispatches a named action registered by the plugin via
|
|
1885
|
+
* `ctx.actions.register(actionId, handler)`. Admin widgets / actions / tables
|
|
1886
|
+
* call this via POST /api/plugins/:id/actions/:actionId — the handler is
|
|
1887
|
+
* responsible for returning `{ ok, data?, error? }`.
|
|
1888
|
+
*/
|
|
1889
|
+
declare function dispatchPluginAction(pluginId: string, actionId: string, data?: unknown): Promise<{
|
|
1890
|
+
ok: boolean;
|
|
1891
|
+
data?: unknown;
|
|
1892
|
+
error?: string;
|
|
1893
|
+
}>;
|
|
1894
|
+
declare function schedulePluginTask(pluginId: string, taskId: string): Promise<void>;
|
|
1895
|
+
/**
|
|
1896
|
+
* Phase 19 — return every registered schedule across loaded
|
|
1897
|
+
* plugins. The pg-boss adapter calls this from
|
|
1898
|
+
* `scheduleRecurring()` so each `definePlugin({ scheduled })`
|
|
1899
|
+
* entry becomes a real cron in `pgboss.schedule`.
|
|
1900
|
+
*/
|
|
1901
|
+
declare function getRegisteredPluginSchedules(): PluginScheduleHandler[];
|
|
1902
|
+
/**
|
|
1903
|
+
* Phase 19 — runs the handler for one plugin's scheduled task.
|
|
1904
|
+
* Called from the `plugin:scheduledTask` job handler when a
|
|
1905
|
+
* tick fires. Builds the same plugin context the `setup()`
|
|
1906
|
+
* call sees so handlers reuse `ctx.content` / `ctx.storage` /
|
|
1907
|
+
* etc. Throws when the plugin or task isn't registered so the
|
|
1908
|
+
* worker's retry policy surfaces the misconfiguration.
|
|
1909
|
+
*/
|
|
1910
|
+
declare function runPluginScheduledTask(pluginId: string, taskId: string): Promise<void>;
|
|
1911
|
+
declare function resetPlugins(): void;
|
|
1912
|
+
|
|
1913
|
+
interface NpPluginConfigResult {
|
|
1914
|
+
pluginId: string;
|
|
1915
|
+
/** Parsed config or schema defaults. Empty object when the plugin has
|
|
1916
|
+
* no configSchema. */
|
|
1917
|
+
value: unknown;
|
|
1918
|
+
/** True when there's a stored row, regardless of whether it passed
|
|
1919
|
+
* validation. */
|
|
1920
|
+
hasPersisted: boolean;
|
|
1921
|
+
/** Set when the persisted value failed `schema.parse()`. The admin
|
|
1922
|
+
* surface uses this to render a "settings were reset" banner. */
|
|
1923
|
+
parseError?: string;
|
|
1924
|
+
}
|
|
1925
|
+
/**
|
|
1926
|
+
* Read the persisted config for a plugin and parse it via the plugin's
|
|
1927
|
+
* `configSchema`. Returns the parsed value when valid; falls back to
|
|
1928
|
+
* schema defaults on parse failure (with the failure recorded for the
|
|
1929
|
+
* admin to surface, see `getPluginConfigWithStatus`).
|
|
1930
|
+
*
|
|
1931
|
+
* Return type is `unknown` because core can't type-narrow to the plugin's
|
|
1932
|
+
* `z.infer<typeof configSchema>` — the schema lives in the plugin
|
|
1933
|
+
* package, not in core. Plugin code that reads its own config should
|
|
1934
|
+
* cast at the call site, ideally against an exported type alias from the
|
|
1935
|
+
* plugin package itself:
|
|
1936
|
+
*
|
|
1937
|
+
* // packages/plugins/oauth-github/src/index.ts
|
|
1938
|
+
* export const configSchema = z.object({ ... });
|
|
1939
|
+
* export type GithubOauthConfig = z.infer<typeof configSchema>;
|
|
1940
|
+
*
|
|
1941
|
+
* // a plugin handler
|
|
1942
|
+
* const config = (await getPluginConfig("oauth-github")) as GithubOauthConfig;
|
|
1943
|
+
*/
|
|
1944
|
+
declare function getPluginConfig(pluginId: string): Promise<unknown>;
|
|
1945
|
+
declare function getPluginConfigWithStatus(pluginId: string): Promise<NpPluginConfigResult>;
|
|
1946
|
+
/**
|
|
1947
|
+
* Validate and persist a plugin's config. Throws `NpValidationError` when
|
|
1948
|
+
* `value` doesn't pass the schema — the admin form must surface
|
|
1949
|
+
* field-level errors before calling this.
|
|
1950
|
+
*
|
|
1951
|
+
* **Cache invalidation is the caller's responsibility.** This function
|
|
1952
|
+
* writes to `np_settings` only; it doesn't import `next/cache`. The
|
|
1953
|
+
* admin API route (`PUT /api/admin/plugins/[id]/config`) busts
|
|
1954
|
+
* `np:plugin:<id>` after a successful write.
|
|
1955
|
+
*
|
|
1956
|
+
* Mirrors `setThemeSettings` in `packages/core/src/themes/settings.ts`.
|
|
1957
|
+
*/
|
|
1958
|
+
declare function setPluginConfig(pluginId: string, value: unknown, updatedBy?: string | null): Promise<unknown>;
|
|
1959
|
+
/** Cache tag for a plugin's config invalidation. Per the prefix policy
|
|
1960
|
+
* in CLAUDE.md (Naming convention table) every framework-owned tag
|
|
1961
|
+
* uses the `np` prefix. Distinct from the legacy `nx:theme:<siteId>`
|
|
1962
|
+
* tag — see `docs/design/plugin-config-auto-form.md` § 7. */
|
|
1963
|
+
declare function pluginConfigCacheTag(pluginId: string): string;
|
|
1964
|
+
|
|
1965
|
+
/**
|
|
1966
|
+
* G.1 — `np_plugins` is now a lean meta row: `(id, enabled,
|
|
1967
|
+
* installed_at, updated_at)`. The legacy `config` jsonb column was
|
|
1968
|
+
* dropped in favor of `np_settings` rows keyed by `plugin.config:<id>`
|
|
1969
|
+
* (see `packages/core/src/plugins/config.ts` and decision E in
|
|
1970
|
+
* `docs/design/plugin-config-auto-form.md`). Read / write plugin
|
|
1971
|
+
* config through `getPluginConfig` / `setPluginConfig` from the
|
|
1972
|
+
* config module — `getPluginState` only knows about the enable flag.
|
|
1973
|
+
*/
|
|
1974
|
+
interface NpPluginState {
|
|
1975
|
+
id: string;
|
|
1976
|
+
enabled: boolean;
|
|
1977
|
+
installedAt: Date;
|
|
1978
|
+
updatedAt: Date;
|
|
1979
|
+
}
|
|
1980
|
+
interface NpPluginStateUpdate {
|
|
1981
|
+
enabled?: boolean;
|
|
1982
|
+
}
|
|
1983
|
+
declare function listPluginStates(db: NodePgDatabase<Record<string, unknown>>): Promise<NpPluginState[]>;
|
|
1984
|
+
declare function getPluginState(db: NodePgDatabase<Record<string, unknown>>, id: string): Promise<NpPluginState | null>;
|
|
1985
|
+
/**
|
|
1986
|
+
* Ensures every known plugin id has a row in `np_plugins`. Missing rows are
|
|
1987
|
+
* inserted with `enabled=true`. Existing rows are never touched — this is
|
|
1988
|
+
* called on boot and must not clobber operator edits.
|
|
1989
|
+
*
|
|
1990
|
+
* Uses a single INSERT … ON CONFLICT DO NOTHING so concurrent boots (multi-
|
|
1991
|
+
* process deployments) can all race safely without unique-key violations.
|
|
1992
|
+
*/
|
|
1993
|
+
declare function syncPluginRegistrations(db: NodePgDatabase<Record<string, unknown>>, pluginIds: readonly string[]): Promise<void>;
|
|
1994
|
+
declare function updatePluginState(db: NodePgDatabase<Record<string, unknown>>, id: string, patch: NpPluginStateUpdate): Promise<NpPluginState | null>;
|
|
1995
|
+
|
|
1996
|
+
/**
|
|
1997
|
+
* Plugin compatibility checks: framework semver range + inter-plugin
|
|
1998
|
+
* dependency ordering.
|
|
1999
|
+
*
|
|
2000
|
+
* The plugin manifest declares two compatibility hints:
|
|
2001
|
+
* 1. `nexpress.minVersion` / `nexpress.maxVersion` — the plugin must run
|
|
2002
|
+
* against a framework version inside this range. The host enforces it
|
|
2003
|
+
* at load time so an outdated plugin can't crash deeper in the call
|
|
2004
|
+
* stack with an unrelated `TypeError`.
|
|
2005
|
+
* 2. `requires` — other plugins this one depends on. Used to sort the
|
|
2006
|
+
* load order so a plugin's `setup()` can assume its prerequisites
|
|
2007
|
+
* have already registered hooks/actions.
|
|
2008
|
+
*
|
|
2009
|
+
* Both checks fail open by default — an incompatible plugin or one with
|
|
2010
|
+
* missing deps is logged and skipped, never thrown. Operators see the warn
|
|
2011
|
+
* lines in boot logs and decide whether to upgrade or pin a version.
|
|
2012
|
+
*/
|
|
2013
|
+
/**
|
|
2014
|
+
* Returns the running framework version, read from `@nexpress/core`'s
|
|
2015
|
+
* package.json at build time and inlined by tsup. Tests can override via
|
|
2016
|
+
* `setFrameworkVersionForTest()`.
|
|
2017
|
+
*/
|
|
2018
|
+
declare function getFrameworkVersion(): string;
|
|
2019
|
+
/** Returns -1 / 0 / 1 — same contract as `Array.prototype.sort` callbacks. */
|
|
2020
|
+
declare function compareSemver(a: string, b: string): number;
|
|
2021
|
+
interface NexpressCompatResult {
|
|
2022
|
+
compatible: boolean;
|
|
2023
|
+
reason?: string;
|
|
2024
|
+
}
|
|
2025
|
+
/**
|
|
2026
|
+
* Verifies that `frameworkVersion` falls inside `[minVersion, maxVersion]`
|
|
2027
|
+
* (inclusive). `maxVersion` is optional — a plugin that omits it claims to
|
|
2028
|
+
* support every later version of the framework.
|
|
2029
|
+
*/
|
|
2030
|
+
declare function checkNexpressCompat(manifest: {
|
|
2031
|
+
nexpress?: {
|
|
2032
|
+
minVersion?: string;
|
|
2033
|
+
maxVersion?: string;
|
|
2034
|
+
};
|
|
2035
|
+
}, framework?: string): NexpressCompatResult;
|
|
2036
|
+
|
|
2037
|
+
export { type CreateSiteInput, DEFAULT_THEME, LocalStorageAdapter, NP_DEFAULT_SITE_ID, type NexpressCompatResult, NoopEmailAdapter, NpAuthError, NpAuthUser, NpCollectionConfig, type NpCollectionTranslationProgress, NpConfig, NpConflictError, type NpDeleteSiteOptions, type NpEmailAdapter, type NpEmailMessage, type NpEmailTemplate, NpError, type NpErrorCode, type NpErrorCodeInput, NpFeedEntry, NpFieldConfig, NpFileMetadata, NpFindOptions, NpFindResult, NpForbiddenError, type NpListPendingDocsOptions, type NpListPendingDocsResult, NpNavItem, NpNotFoundError, type NpPasswordResetTemplateData, type NpPendingDocSummary, NpPluginConfig, type NpPluginConfigResult, type NpPluginState, type NpPluginStateUpdate, NpRateLimitError, NpRegisteredTheme, type NpRevision, type NpRevisionListOptions, type NpRevisionListResult, type NpRevisionStatus, type NpRevisionSummary, NpSaveOptions, NpSaveResult, type NpSearchAdapter, type NpSearchAdapterContext, type NpSearchVectorParts, type NpSite, NpSiteContextMissingError, type NpSiteUsage, NpSitemapEntry, NpStorageAdapter, type NpThemeColors, NpThemeFieldRequirement, NpThemeManifest, type NpThemeNavLocationDescriptor, type NpThemeRequirementMissingField, type NpThemeRequirementRelationConflict, type NpThemeRequirementResult, type NpThemeRequirementTypeConflict, type NpThemeSeoHooksExtracted, type NpThemeSettingsArrayField, type NpThemeSettingsBooleanField, type NpThemeSettingsColorField, type NpThemeSettingsEnumField, type NpThemeSettingsField, type NpThemeSettingsNumberField, type NpThemeSettingsObjectField, type NpThemeSettingsResult, type NpThemeSettingsTextField, type NpThemeSettingsTextareaField, type NpThemeSettingsUnsupportedField, type NpThemeSettingsUrlField, type NpThemeShape, type NpThemeTemplateSummary, type NpThemeTokens, type NpThemeTokensOverlay, type NpThemeTypography, type NpTranslationProgress, type NpTranslationProgressLocaleStats, NpUserRole, NpValidationError, type PluginAdminExtension, type PluginHookHandler, type PluginPageRouteEntry, type PluginRouteHandler, type PluginRouteRequest, type PluginRouteResponse, type PublishScheduledResult, type ReindexResult, type ResolvedCollectionTab, type ResolvedDashboardWidget, S3StorageAdapter, type SearchCollectionsOptions, type SearchResult, type SearchResultItem, type SiteMembership, SmtpEmailAdapter, type SmtpEmailAdapterOptions, activeThemeContributesSeo, autosaveRevision, buildInviteEmail, buildResetEmail, buildSearchVector, buildSearchVectorParts, buildWeightedSearchVectorSql, buildZodSchema, checkNexpressCompat, checkThemeRequirements, collectionConfigSchema, compareSemver, createMemberDocument, createSite, createStorageAdapter, createTranslation, defineCollection, defineConfig, deleteDocument, deleteMemberDocument, deleteSite, dispatchPluginAction, ensureDefaultSite, extractErrorComponent, extractMembersNotFoundComponent, extractNotFoundComponent, extractSeoHooks, findDocuments, findPosts, findSlugRedirect, findTranslations, getActiveTheme, getActiveThemeError, getActiveThemeId, getActiveThemeMembersNotFound, getActiveThemeNavLocations, getActiveThemeNotFound, getActiveThemeSeoHooks, getAllCollectionSlugs, getAllPageSlugs, getAllPluginIds, getCollectionConfig, getCollectionRegistration, getCollectionTable, getCollectionTabsForSlug, getCollectionZodSchema, getCurrentSiteId, getDashboardWidgetsFromPlugins, getDefaultSite, getDocumentById, getEmailAdapter, getFrameworkVersion, getMembership, getNavigation, getPageBySlug, getPluginAdminExtension, getPluginConfig, getPluginConfigWithStatus, getPluginPageRoutes, getPluginRegistration, getPluginRoutes, getPluginState, getPluginTemplatesForCollection, getPostBySlug, getRegisteredPluginSchedules, getRegisteredThemes, getRevision, getSearchAdapter, getSetting, getSiteByHostname, getSiteById, getSiteUsageSummary, getTheme, getThemeById, getThemeSettings, getThemeSettingsWithStatus, getThemeTemplateSummaries, getTranslationProgress, grantSiteMembership, hasRoleOnSite, introspectThemeSettingsSchema, invalidatePluginEnabled, isPluginEnabled, isSuperAdmin, listMembershipsForUser, listPendingMemberDocs, listPluginStates, listRevisions, listSiteMemberships, listSites, loadPlugins, npConfigSchema, pluginConfigCacheTag, promoteMemberDocument, publishScheduledDocuments, registerCollection, registerPluginTemplates, registerThemes, reindexCollection, resetCurrentSiteResolver, resetEmailAdapter, resetPluginTemplates, resetPlugins, resetSearchAdapter, resetThemes, resolveSiteForHostname, resolveTemplateComponent, resolveUserRoleOnSite, restoreRevision, revokeSiteMembership, runHook, runHookAndCollect, runPluginScheduledTask, sanitizeTokenValue, saveDocument, schedulePluginTask, searchCollections, setActiveThemeId, setCurrentSiteResolver, setEmailAdapter, setPluginConfig, setSearchAdapter, setSuperAdmin, setThemeSettings, syncPluginRegistrations, updateMemberDocument, updatePluginState, updateSite, withCurrentSite };
|