create-nextblock 0.10.5 → 0.10.7
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/package.json
CHANGED
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
import {
|
|
2
|
-
decryptOpenRouterApiKey,
|
|
3
2
|
encryptOpenRouterApiKey,
|
|
4
3
|
getMaskedOpenRouterKey,
|
|
5
4
|
getOpenRouterKeyEnvelopeStatus,
|
|
6
5
|
type EncryptedOpenRouterKeyEnvelope,
|
|
7
6
|
} from './ai-key-crypto';
|
|
7
|
+
import {
|
|
8
|
+
hasSecretEncryptionKey,
|
|
9
|
+
resolveSecretEncryptionKey,
|
|
10
|
+
tryDecryptWithEnvKey,
|
|
11
|
+
} from '@nextblock-cms/db/server';
|
|
8
12
|
|
|
9
13
|
const SERVER_ONLY_ERROR_MESSAGE =
|
|
10
14
|
'Cortex AI configuration can only be imported from server-side code.';
|
|
@@ -33,17 +37,24 @@ export function getCortexAiEnvConfig() {
|
|
|
33
37
|
return {
|
|
34
38
|
encryptionKey: readEnvValue('CORTEX_AI_ENCRYPTION_KEY'),
|
|
35
39
|
freemiusSandboxKey: readEnvValue('FREEMIUS_AI_SANDBOX_KEY'),
|
|
36
|
-
|
|
40
|
+
// True when ANY usable key exists: an explicit env key OR the service-role-derived
|
|
41
|
+
// fallback — so BYOK works on a one-click Vercel deploy with no extra env var.
|
|
42
|
+
hasEncryptionKey: hasSecretEncryptionKey(),
|
|
37
43
|
hasOpenRouterEnvKey: Boolean(openRouterApiKey),
|
|
38
44
|
openRouterEnvKeyLast4: openRouterApiKey ? openRouterApiKey.slice(-4) : null,
|
|
39
45
|
};
|
|
40
46
|
}
|
|
41
47
|
|
|
42
48
|
function requireEncryptionKey() {
|
|
43
|
-
|
|
49
|
+
// Resolve via the shared chain: NEXTBLOCK_ENCRYPTION_KEY -> CORTEX_AI_ENCRYPTION_KEY ->
|
|
50
|
+
// a stable key derived from the Supabase service-role key. The derived fallback lets
|
|
51
|
+
// BYOK work out-of-the-box on hosted installs (e.g. one-click Vercel).
|
|
52
|
+
const encryptionKey = resolveSecretEncryptionKey();
|
|
44
53
|
|
|
45
54
|
if (!encryptionKey) {
|
|
46
|
-
throw new Error(
|
|
55
|
+
throw new Error(
|
|
56
|
+
'An encryption key (NEXTBLOCK_ENCRYPTION_KEY, CORTEX_AI_ENCRYPTION_KEY, or a Supabase service-role key) is required to manage stored OpenRouter keys.'
|
|
57
|
+
);
|
|
47
58
|
}
|
|
48
59
|
|
|
49
60
|
return encryptionKey;
|
|
@@ -57,10 +68,16 @@ export function encryptStoredOpenRouterApiKey(apiKey: string) {
|
|
|
57
68
|
}
|
|
58
69
|
|
|
59
70
|
export function decryptStoredOpenRouterApiKey(encryptedKey: unknown) {
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
71
|
+
// Try every candidate key (explicit env keys + the derived fallback). This keeps a key
|
|
72
|
+
// stored under one key readable if another is added later, and matches the SMTP/payment
|
|
73
|
+
// secret behaviour. The envelope is byte-compatible with the shared secret-crypto format.
|
|
74
|
+
const result = tryDecryptWithEnvKey(encryptedKey);
|
|
75
|
+
|
|
76
|
+
if (result === null) {
|
|
77
|
+
throw new Error('Failed to decrypt stored OpenRouter key.');
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return result;
|
|
64
81
|
}
|
|
65
82
|
|
|
66
83
|
export function getStoredOpenRouterKeyStatus(value: unknown) {
|
|
@@ -22,11 +22,34 @@ export type OnboardingStatus = {
|
|
|
22
22
|
dismissed: boolean;
|
|
23
23
|
};
|
|
24
24
|
|
|
25
|
-
|
|
25
|
+
// Seeded defaults (libs/db migrations 008 + 010). The branding/copyright steps count as
|
|
26
|
+
// "done" only when the value has been customized away from these — a fresh install ships
|
|
27
|
+
// with the seeds, so a plain presence check would mark every step complete immediately.
|
|
28
|
+
const SEEDED_SITE_TITLE = 'NextBlock™ CMS';
|
|
29
|
+
const SEEDED_LOGO_OBJECT_KEY = 'images/nextblock-logo-small.webp';
|
|
30
|
+
const SEEDED_COPYRIGHT: Record<string, string> = {
|
|
31
|
+
en: '© {year} Nextblock CMS. All rights reserved.',
|
|
32
|
+
fr: '© {year} Nextblock CMS. Tous droits réservés.',
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
/** True when at least one language's copyright text is set AND differs from the seed. */
|
|
36
|
+
function hasCustomCopyright(value: unknown): boolean {
|
|
26
37
|
if (!value || typeof value !== 'object') return false;
|
|
27
|
-
return Object.
|
|
28
|
-
(
|
|
29
|
-
|
|
38
|
+
return Object.entries(value as Record<string, unknown>).some(([lang, v]) => {
|
|
39
|
+
if (typeof v !== 'string') return false;
|
|
40
|
+
const trimmed = v.trim();
|
|
41
|
+
return trimmed.length > 0 && trimmed !== (SEEDED_COPYRIGHT[lang] ?? '');
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Pull the active logo's media object_key from the (to-one) embedded relation. */
|
|
46
|
+
function extractLogoObjectKey(logoRow: unknown): string | null {
|
|
47
|
+
if (!logoRow || typeof logoRow !== 'object') return null;
|
|
48
|
+
const media = (logoRow as { media?: unknown }).media;
|
|
49
|
+
const record = Array.isArray(media) ? media[0] : media;
|
|
50
|
+
if (!record || typeof record !== 'object') return null;
|
|
51
|
+
const key = (record as { object_key?: unknown }).object_key;
|
|
52
|
+
return typeof key === 'string' ? key : null;
|
|
30
53
|
}
|
|
31
54
|
|
|
32
55
|
export async function getOnboardingStatus(opts: {
|
|
@@ -38,8 +61,13 @@ export async function getOnboardingStatus(opts: {
|
|
|
38
61
|
supabase
|
|
39
62
|
.from('site_settings')
|
|
40
63
|
.select('key, value')
|
|
41
|
-
.in('key', ['footer_copyright', 'bot_protection_public', 'onboarding_state']),
|
|
42
|
-
supabase
|
|
64
|
+
.in('key', ['footer_copyright', 'bot_protection_public', 'onboarding_state', 'site_title']),
|
|
65
|
+
supabase
|
|
66
|
+
.from('logos')
|
|
67
|
+
.select('media:media_id(object_key)')
|
|
68
|
+
.order('created_at', { ascending: false })
|
|
69
|
+
.limit(1)
|
|
70
|
+
.maybeSingle(),
|
|
43
71
|
getEmailPublicSettings(),
|
|
44
72
|
getPrivacySettings(),
|
|
45
73
|
]);
|
|
@@ -48,8 +76,16 @@ export async function getOnboardingStatus(opts: {
|
|
|
48
76
|
const botPublic = (rows.get('bot_protection_public') ?? {}) as Record<string, unknown>;
|
|
49
77
|
const onboardingState = (rows.get('onboarding_state') ?? {}) as Record<string, unknown>;
|
|
50
78
|
|
|
51
|
-
const
|
|
52
|
-
const
|
|
79
|
+
const siteTitleRaw = rows.get('site_title');
|
|
80
|
+
const siteTitle = typeof siteTitleRaw === 'string' ? siteTitleRaw.trim() : '';
|
|
81
|
+
const siteTitleCustomized = siteTitle.length > 0 && siteTitle !== SEEDED_SITE_TITLE;
|
|
82
|
+
const logoObjectKey = extractLogoObjectKey(logoRow);
|
|
83
|
+
const logoCustomized = Boolean(logoObjectKey) && logoObjectKey !== SEEDED_LOGO_OBJECT_KEY;
|
|
84
|
+
|
|
85
|
+
// Branding is "done" once the user renames the site or uploads their own logo — not merely
|
|
86
|
+
// because the seeded NextBlock title/logo exist.
|
|
87
|
+
const brandingDone = siteTitleCustomized || logoCustomized;
|
|
88
|
+
const copyrightDone = hasCustomCopyright(rows.get('footer_copyright'));
|
|
53
89
|
const emailDone = Boolean(emailPublic.host) || Boolean(process.env['SMTP_HOST']);
|
|
54
90
|
const analyticsDone = Boolean(privacy.gtm_id);
|
|
55
91
|
const botProvider = typeof botPublic['provider'] === 'string' ? (botPublic['provider'] as string) : 'none';
|