@edgedev/firebase 2.2.82 → 2.2.83
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 +1 -1
- package/src/cms.js +272 -22
package/package.json
CHANGED
package/src/cms.js
CHANGED
|
@@ -235,16 +235,76 @@ const normalizeDomain = (value) => {
|
|
|
235
235
|
}
|
|
236
236
|
}
|
|
237
237
|
normalized = normalized.split('/')[0] || ''
|
|
238
|
+
if (normalized.startsWith('[')) {
|
|
239
|
+
const closingIndex = normalized.indexOf(']')
|
|
240
|
+
if (closingIndex !== -1)
|
|
241
|
+
normalized = normalized.slice(0, closingIndex + 1)
|
|
242
|
+
}
|
|
238
243
|
if (normalized.includes(':') && !normalized.startsWith('[')) {
|
|
239
244
|
normalized = normalized.split(':')[0] || ''
|
|
240
245
|
}
|
|
241
246
|
return normalized.replace(/\.+$/g, '')
|
|
242
247
|
}
|
|
243
248
|
|
|
249
|
+
const stripIpv6Brackets = (value) => {
|
|
250
|
+
const text = String(value || '').trim()
|
|
251
|
+
if (text.startsWith('[') && text.endsWith(']'))
|
|
252
|
+
return text.slice(1, -1)
|
|
253
|
+
return text
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
const isIpv4Address = (value) => {
|
|
257
|
+
const parts = String(value || '').split('.')
|
|
258
|
+
if (parts.length !== 4)
|
|
259
|
+
return false
|
|
260
|
+
return parts.every((part) => {
|
|
261
|
+
if (!/^\d{1,3}$/.test(part))
|
|
262
|
+
return false
|
|
263
|
+
const num = Number(part)
|
|
264
|
+
return num >= 0 && num <= 255
|
|
265
|
+
})
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
const isIpv6Address = (value) => {
|
|
269
|
+
const normalized = String(value || '').toLowerCase()
|
|
270
|
+
if (!normalized.includes(':'))
|
|
271
|
+
return false
|
|
272
|
+
return /^[0-9a-f:]+$/.test(normalized)
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
const isIpAddress = (value) => {
|
|
276
|
+
if (!value)
|
|
277
|
+
return false
|
|
278
|
+
const normalized = stripIpv6Brackets(value)
|
|
279
|
+
return isIpv4Address(normalized) || isIpv6Address(normalized)
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
const getCloudflareApexDomain = (domain) => {
|
|
283
|
+
if (!domain)
|
|
284
|
+
return ''
|
|
285
|
+
if (domain.startsWith('www.'))
|
|
286
|
+
return domain.slice(4)
|
|
287
|
+
return domain
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
const shouldDisplayDomainDnsRecords = (domain) => {
|
|
291
|
+
const normalizedDomain = normalizeDomain(domain)
|
|
292
|
+
const apexDomain = getCloudflareApexDomain(normalizedDomain)
|
|
293
|
+
if (!apexDomain)
|
|
294
|
+
return false
|
|
295
|
+
if (apexDomain === 'localhost' || apexDomain.endsWith('.localhost'))
|
|
296
|
+
return false
|
|
297
|
+
if (isIpAddress(apexDomain))
|
|
298
|
+
return false
|
|
299
|
+
if (apexDomain.endsWith('.dev'))
|
|
300
|
+
return false
|
|
301
|
+
return true
|
|
302
|
+
}
|
|
303
|
+
|
|
244
304
|
const shouldSyncCloudflareDomain = (domain) => {
|
|
245
305
|
if (!domain)
|
|
246
306
|
return false
|
|
247
|
-
if (domain
|
|
307
|
+
if (!shouldDisplayDomainDnsRecords(domain))
|
|
248
308
|
return false
|
|
249
309
|
if (CLOUDFLARE_PAGES_PROJECT) {
|
|
250
310
|
const pagesDomain = `${CLOUDFLARE_PAGES_PROJECT}.pages.dev`
|
|
@@ -262,6 +322,44 @@ const getCloudflarePagesDomain = (domain) => {
|
|
|
262
322
|
return `www.${domain}`
|
|
263
323
|
}
|
|
264
324
|
|
|
325
|
+
const getCloudflarePagesTarget = () => {
|
|
326
|
+
if (!CLOUDFLARE_PAGES_PROJECT)
|
|
327
|
+
return ''
|
|
328
|
+
return `${CLOUDFLARE_PAGES_PROJECT}.pages.dev`
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
const buildDomainDnsPayload = (domain, pagesTarget = '') => {
|
|
332
|
+
const normalizedDomain = normalizeDomain(domain)
|
|
333
|
+
const apexDomain = getCloudflareApexDomain(normalizedDomain)
|
|
334
|
+
const wwwDomain = getCloudflarePagesDomain(apexDomain)
|
|
335
|
+
const target = pagesTarget || getCloudflarePagesTarget()
|
|
336
|
+
const dnsEligible = shouldDisplayDomainDnsRecords(apexDomain)
|
|
337
|
+
|
|
338
|
+
return {
|
|
339
|
+
domain: normalizedDomain,
|
|
340
|
+
apexDomain,
|
|
341
|
+
wwwDomain,
|
|
342
|
+
dnsEligible,
|
|
343
|
+
dnsRecords: {
|
|
344
|
+
target,
|
|
345
|
+
www: {
|
|
346
|
+
type: 'CNAME',
|
|
347
|
+
name: 'www',
|
|
348
|
+
host: wwwDomain,
|
|
349
|
+
value: target,
|
|
350
|
+
enabled: dnsEligible && !!target,
|
|
351
|
+
},
|
|
352
|
+
apex: {
|
|
353
|
+
type: 'CNAME',
|
|
354
|
+
name: '@',
|
|
355
|
+
host: apexDomain,
|
|
356
|
+
value: target,
|
|
357
|
+
enabled: dnsEligible && !!target,
|
|
358
|
+
},
|
|
359
|
+
},
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
265
363
|
const isCloudflareDomainAlreadyExistsError = (status, errors = [], message = '') => {
|
|
266
364
|
if (status === 409)
|
|
267
365
|
return true
|
|
@@ -1917,13 +2015,64 @@ exports.updateSeoFromAi = onCall({ timeoutSeconds: 180 }, async (request) => {
|
|
|
1917
2015
|
|
|
1918
2016
|
exports.getCloudflarePagesProject = onCall(async (request) => {
|
|
1919
2017
|
assertCallableUser(request)
|
|
2018
|
+
const data = request.data || {}
|
|
2019
|
+
const orgId = String(data.orgId || '').trim()
|
|
2020
|
+
const siteId = String(data.siteId || '').trim()
|
|
2021
|
+
const rawDomains = Array.isArray(data.domains) ? data.domains : []
|
|
2022
|
+
const normalizedDomains = Array.from(new Set(rawDomains.map(normalizeDomain).filter(Boolean)))
|
|
2023
|
+
const pagesTarget = getCloudflarePagesTarget()
|
|
1920
2024
|
|
|
1921
|
-
if (!CLOUDFLARE_PAGES_PROJECT)
|
|
2025
|
+
if (!CLOUDFLARE_PAGES_PROJECT)
|
|
1922
2026
|
logger.warn('CLOUDFLARE_PAGES_PROJECT is not set.')
|
|
1923
|
-
|
|
2027
|
+
|
|
2028
|
+
const domainRegistry = {}
|
|
2029
|
+
if (orgId && siteId && normalizedDomains.length) {
|
|
2030
|
+
const allowed = await permissionCheck(request.auth.uid, 'read', `organizations/${orgId}/sites`)
|
|
2031
|
+
if (!allowed)
|
|
2032
|
+
throw new HttpsError('permission-denied', 'Not allowed to read site settings')
|
|
2033
|
+
|
|
2034
|
+
await Promise.all(normalizedDomains.map(async (domain) => {
|
|
2035
|
+
const registryRef = db.collection(DOMAIN_REGISTRY_COLLECTION).doc(domain)
|
|
2036
|
+
const registrySnap = await registryRef.get()
|
|
2037
|
+
const fallback = buildDomainDnsPayload(domain, pagesTarget)
|
|
2038
|
+
if (!registrySnap.exists) {
|
|
2039
|
+
domainRegistry[domain] = {
|
|
2040
|
+
...fallback,
|
|
2041
|
+
apexAttempted: false,
|
|
2042
|
+
apexAdded: false,
|
|
2043
|
+
apexError: '',
|
|
2044
|
+
dnsGuidance: fallback.dnsEligible
|
|
2045
|
+
? 'Add the www CNAME. Apex is unavailable; forward apex to www.'
|
|
2046
|
+
: 'DNS records are not shown for localhost, IP addresses, or .dev domains.',
|
|
2047
|
+
}
|
|
2048
|
+
return
|
|
2049
|
+
}
|
|
2050
|
+
|
|
2051
|
+
const value = registrySnap.data() || {}
|
|
2052
|
+
domainRegistry[domain] = {
|
|
2053
|
+
...fallback,
|
|
2054
|
+
...value,
|
|
2055
|
+
dnsRecords: {
|
|
2056
|
+
...fallback.dnsRecords,
|
|
2057
|
+
...(value.dnsRecords || {}),
|
|
2058
|
+
www: {
|
|
2059
|
+
...fallback.dnsRecords.www,
|
|
2060
|
+
...(value?.dnsRecords?.www || {}),
|
|
2061
|
+
},
|
|
2062
|
+
apex: {
|
|
2063
|
+
...fallback.dnsRecords.apex,
|
|
2064
|
+
...(value?.dnsRecords?.apex || {}),
|
|
2065
|
+
},
|
|
2066
|
+
},
|
|
2067
|
+
}
|
|
2068
|
+
}))
|
|
1924
2069
|
}
|
|
1925
2070
|
|
|
1926
|
-
return {
|
|
2071
|
+
return {
|
|
2072
|
+
project: CLOUDFLARE_PAGES_PROJECT || '',
|
|
2073
|
+
pagesDomain: pagesTarget,
|
|
2074
|
+
domainRegistry,
|
|
2075
|
+
}
|
|
1927
2076
|
})
|
|
1928
2077
|
|
|
1929
2078
|
exports.generateBlockFields = onCall({ timeoutSeconds: 180 }, async (request) => {
|
|
@@ -2071,7 +2220,6 @@ exports.ensurePublishedSiteDomains = onDocumentWritten(
|
|
|
2071
2220
|
const siteRef = change.after.ref
|
|
2072
2221
|
const siteData = change.after.data() || {}
|
|
2073
2222
|
const beforeData = change.before?.data?.() || {}
|
|
2074
|
-
const domainErrorChanged = beforeData?.domainError !== siteData?.domainError
|
|
2075
2223
|
const rawDomains = Array.isArray(siteData.domains) ? siteData.domains : []
|
|
2076
2224
|
const normalizedDomains = Array.from(new Set(rawDomains.map(normalizeDomain).filter(Boolean)))
|
|
2077
2225
|
const beforeRawDomains = Array.isArray(beforeData.domains) ? beforeData.domains : []
|
|
@@ -2141,41 +2289,143 @@ exports.ensurePublishedSiteDomains = onDocumentWritten(
|
|
|
2141
2289
|
filteredDomains = normalizedDomains.filter(domain => !conflictSet.has(domain))
|
|
2142
2290
|
}
|
|
2143
2291
|
|
|
2144
|
-
const
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
2292
|
+
const pagesTarget = getCloudflarePagesTarget()
|
|
2293
|
+
const registryStateByDomain = new Map()
|
|
2294
|
+
const syncPlanMap = new Map()
|
|
2295
|
+
for (const domain of filteredDomains) {
|
|
2296
|
+
const dnsPayload = buildDomainDnsPayload(domain, pagesTarget)
|
|
2297
|
+
registryStateByDomain.set(domain, {
|
|
2298
|
+
...dnsPayload,
|
|
2299
|
+
wwwAdded: false,
|
|
2300
|
+
wwwError: '',
|
|
2301
|
+
apexAttempted: false,
|
|
2302
|
+
apexAdded: false,
|
|
2303
|
+
apexError: '',
|
|
2304
|
+
dnsGuidance: dnsPayload.dnsEligible
|
|
2305
|
+
? 'Add the www CNAME record. Apex is unavailable; forward apex to www.'
|
|
2306
|
+
: 'DNS records are not shown for localhost, IP addresses, or .dev domains.',
|
|
2307
|
+
})
|
|
2308
|
+
|
|
2309
|
+
const apexDomain = dnsPayload.apexDomain
|
|
2310
|
+
if (!apexDomain)
|
|
2311
|
+
continue
|
|
2312
|
+
const existingPlan = syncPlanMap.get(apexDomain) || {
|
|
2313
|
+
apexDomain,
|
|
2314
|
+
wwwDomain: dnsPayload.wwwDomain,
|
|
2315
|
+
domains: new Set(),
|
|
2316
|
+
}
|
|
2317
|
+
existingPlan.domains.add(domain)
|
|
2318
|
+
syncPlanMap.set(apexDomain, existingPlan)
|
|
2319
|
+
}
|
|
2320
|
+
|
|
2321
|
+
const syncPlans = Array.from(syncPlanMap.values())
|
|
2322
|
+
.filter(plan => shouldSyncCloudflareDomain(plan.wwwDomain))
|
|
2323
|
+
.map(plan => ({ ...plan, domains: Array.from(plan.domains) }))
|
|
2324
|
+
|
|
2149
2325
|
const removeDomains = Array.from(new Set(
|
|
2150
2326
|
removedOwnedDomains
|
|
2151
|
-
.
|
|
2327
|
+
.flatMap((domain) => {
|
|
2328
|
+
const apexDomain = getCloudflareApexDomain(domain)
|
|
2329
|
+
const wwwDomain = getCloudflarePagesDomain(apexDomain)
|
|
2330
|
+
return [wwwDomain, apexDomain]
|
|
2331
|
+
})
|
|
2152
2332
|
.filter(domain => shouldSyncCloudflareDomain(domain)),
|
|
2153
2333
|
))
|
|
2154
2334
|
if (removeDomains.length) {
|
|
2155
2335
|
await Promise.all(removeDomains.map(domain => removeCloudflarePagesDomain(domain, { orgId, siteId })))
|
|
2156
2336
|
}
|
|
2157
|
-
|
|
2158
|
-
|
|
2159
|
-
|
|
2337
|
+
|
|
2338
|
+
const syncResults = await Promise.all(syncPlans.map(async (plan) => {
|
|
2339
|
+
const wwwResult = await addCloudflarePagesDomain(plan.wwwDomain, { orgId, siteId, variant: 'www' })
|
|
2340
|
+
let apexAttempted = false
|
|
2341
|
+
let apexResult = { ok: false, error: '' }
|
|
2342
|
+
if (shouldSyncCloudflareDomain(plan.apexDomain)) {
|
|
2343
|
+
apexAttempted = true
|
|
2344
|
+
apexResult = await addCloudflarePagesDomain(plan.apexDomain, { orgId, siteId, variant: 'apex' })
|
|
2345
|
+
}
|
|
2346
|
+
return {
|
|
2347
|
+
...plan,
|
|
2348
|
+
apexAttempted,
|
|
2349
|
+
wwwResult,
|
|
2350
|
+
apexResult,
|
|
2351
|
+
}
|
|
2352
|
+
}))
|
|
2353
|
+
|
|
2354
|
+
for (const plan of syncResults) {
|
|
2355
|
+
const wwwAdded = !!plan.wwwResult?.ok
|
|
2356
|
+
const wwwError = wwwAdded ? '' : String(plan.wwwResult?.error || 'Failed to add www domain.')
|
|
2357
|
+
const apexAdded = !!plan.apexResult?.ok
|
|
2358
|
+
const apexError = apexAdded
|
|
2359
|
+
? ''
|
|
2360
|
+
: (plan.apexAttempted ? String(plan.apexResult?.error || 'Failed to add apex domain.') : '')
|
|
2361
|
+
|
|
2362
|
+
for (const domain of plan.domains) {
|
|
2363
|
+
const current = registryStateByDomain.get(domain) || buildDomainDnsPayload(domain, pagesTarget)
|
|
2364
|
+
const dnsGuidance = !current.dnsEligible
|
|
2365
|
+
? 'DNS records are not shown for localhost, IP addresses, or .dev domains.'
|
|
2366
|
+
: (apexAdded
|
|
2367
|
+
? 'Apex and www were added to Cloudflare Pages. Add both DNS records if your provider requires manual setup.'
|
|
2368
|
+
: 'Add the www CNAME record. Apex is unavailable; forward apex to www.')
|
|
2369
|
+
const nextDnsRecords = {
|
|
2370
|
+
...(current.dnsRecords || {}),
|
|
2371
|
+
apex: {
|
|
2372
|
+
...(current?.dnsRecords?.apex || {}),
|
|
2373
|
+
enabled: !!current.dnsEligible && !!current?.dnsRecords?.apex?.value && apexAdded,
|
|
2374
|
+
},
|
|
2375
|
+
www: {
|
|
2376
|
+
...(current?.dnsRecords?.www || {}),
|
|
2377
|
+
enabled: !!current.dnsEligible && !!current?.dnsRecords?.www?.value,
|
|
2378
|
+
},
|
|
2379
|
+
}
|
|
2380
|
+
|
|
2381
|
+
registryStateByDomain.set(domain, {
|
|
2382
|
+
...current,
|
|
2383
|
+
dnsRecords: nextDnsRecords,
|
|
2384
|
+
wwwAdded,
|
|
2385
|
+
wwwError,
|
|
2386
|
+
apexAttempted: !!plan.apexAttempted,
|
|
2387
|
+
apexAdded,
|
|
2388
|
+
apexError,
|
|
2389
|
+
dnsGuidance,
|
|
2390
|
+
})
|
|
2160
2391
|
}
|
|
2161
|
-
return
|
|
2162
2392
|
}
|
|
2163
2393
|
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
|
|
2167
|
-
|
|
2394
|
+
if (registryStateByDomain.size) {
|
|
2395
|
+
for (const [domain, value] of registryStateByDomain.entries()) {
|
|
2396
|
+
const registryRef = db.collection(DOMAIN_REGISTRY_COLLECTION).doc(domain)
|
|
2397
|
+
const payload = {
|
|
2398
|
+
domain,
|
|
2399
|
+
orgId,
|
|
2400
|
+
siteId,
|
|
2401
|
+
sitePath: siteRef.path,
|
|
2402
|
+
updatedAt: Firestore.FieldValue.serverTimestamp(),
|
|
2403
|
+
apexDomain: value.apexDomain || '',
|
|
2404
|
+
wwwDomain: value.wwwDomain || '',
|
|
2405
|
+
dnsEligible: !!value.dnsEligible,
|
|
2406
|
+
apexAttempted: !!value.apexAttempted,
|
|
2407
|
+
apexAdded: !!value.apexAdded,
|
|
2408
|
+
wwwAdded: !!value.wwwAdded,
|
|
2409
|
+
dnsRecords: value.dnsRecords || {},
|
|
2410
|
+
dnsGuidance: value.dnsGuidance || '',
|
|
2411
|
+
}
|
|
2412
|
+
payload.apexError = value.apexError ? value.apexError : Firestore.FieldValue.delete()
|
|
2413
|
+
payload.wwwError = value.wwwError ? value.wwwError : Firestore.FieldValue.delete()
|
|
2414
|
+
await registryRef.set(payload, { merge: true })
|
|
2415
|
+
}
|
|
2416
|
+
}
|
|
2168
2417
|
|
|
2418
|
+
const failed = syncResults.filter(item => !item.wwwResult?.ok)
|
|
2169
2419
|
if (!failed.length) {
|
|
2170
|
-
if (!conflictDomains.length && siteData.domainError
|
|
2420
|
+
if (!conflictDomains.length && siteData.domainError) {
|
|
2171
2421
|
await siteRef.set({ domainError: Firestore.FieldValue.delete() }, { merge: true })
|
|
2172
2422
|
}
|
|
2173
2423
|
return
|
|
2174
2424
|
}
|
|
2175
2425
|
|
|
2176
|
-
const errorDomains = failed.map(item => item.
|
|
2426
|
+
const errorDomains = failed.map(item => item.wwwDomain)
|
|
2177
2427
|
const errorDetails = failed
|
|
2178
|
-
.map(item => item.
|
|
2428
|
+
.map(item => item.wwwResult?.error)
|
|
2179
2429
|
.filter(Boolean)
|
|
2180
2430
|
.join('; ')
|
|
2181
2431
|
const cloudflareMessage = `Cloudflare domain sync failed for "${errorDomains.join(', ')}". ${errorDetails || 'Check function logs.'}`.trim()
|