@edgedev/firebase 2.2.82 → 2.2.84
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 +276 -23
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
|
|
@@ -994,8 +1092,11 @@ exports.onSiteWritten = createKvMirrorHandler({
|
|
|
994
1092
|
document: 'organizations/{orgId}/published-site-settings/{siteId}',
|
|
995
1093
|
makeCanonicalKey: ({ orgId, siteId }) =>
|
|
996
1094
|
`sites:${orgId}:${siteId}`,
|
|
997
|
-
makeIndexKeys: ({ orgId }, data) => {
|
|
1095
|
+
makeIndexKeys: ({ orgId, siteId }, data) => {
|
|
998
1096
|
const keys = []
|
|
1097
|
+
const siteDocId = slug(siteId)
|
|
1098
|
+
if (siteDocId)
|
|
1099
|
+
keys.push(`idx:sites:docId:${orgId}:${siteDocId}:${siteId}`)
|
|
999
1100
|
const domains = Array.isArray(data?.domains) ? data.domains : []
|
|
1000
1101
|
for (const domain of domains) {
|
|
1001
1102
|
const st = slug(domain)
|
|
@@ -1917,13 +2018,64 @@ exports.updateSeoFromAi = onCall({ timeoutSeconds: 180 }, async (request) => {
|
|
|
1917
2018
|
|
|
1918
2019
|
exports.getCloudflarePagesProject = onCall(async (request) => {
|
|
1919
2020
|
assertCallableUser(request)
|
|
2021
|
+
const data = request.data || {}
|
|
2022
|
+
const orgId = String(data.orgId || '').trim()
|
|
2023
|
+
const siteId = String(data.siteId || '').trim()
|
|
2024
|
+
const rawDomains = Array.isArray(data.domains) ? data.domains : []
|
|
2025
|
+
const normalizedDomains = Array.from(new Set(rawDomains.map(normalizeDomain).filter(Boolean)))
|
|
2026
|
+
const pagesTarget = getCloudflarePagesTarget()
|
|
1920
2027
|
|
|
1921
|
-
if (!CLOUDFLARE_PAGES_PROJECT)
|
|
2028
|
+
if (!CLOUDFLARE_PAGES_PROJECT)
|
|
1922
2029
|
logger.warn('CLOUDFLARE_PAGES_PROJECT is not set.')
|
|
1923
|
-
|
|
2030
|
+
|
|
2031
|
+
const domainRegistry = {}
|
|
2032
|
+
if (orgId && siteId && normalizedDomains.length) {
|
|
2033
|
+
const allowed = await permissionCheck(request.auth.uid, 'read', `organizations/${orgId}/sites`)
|
|
2034
|
+
if (!allowed)
|
|
2035
|
+
throw new HttpsError('permission-denied', 'Not allowed to read site settings')
|
|
2036
|
+
|
|
2037
|
+
await Promise.all(normalizedDomains.map(async (domain) => {
|
|
2038
|
+
const registryRef = db.collection(DOMAIN_REGISTRY_COLLECTION).doc(domain)
|
|
2039
|
+
const registrySnap = await registryRef.get()
|
|
2040
|
+
const fallback = buildDomainDnsPayload(domain, pagesTarget)
|
|
2041
|
+
if (!registrySnap.exists) {
|
|
2042
|
+
domainRegistry[domain] = {
|
|
2043
|
+
...fallback,
|
|
2044
|
+
apexAttempted: false,
|
|
2045
|
+
apexAdded: false,
|
|
2046
|
+
apexError: '',
|
|
2047
|
+
dnsGuidance: fallback.dnsEligible
|
|
2048
|
+
? 'Add the www CNAME. Apex is unavailable; forward apex to www.'
|
|
2049
|
+
: 'DNS records are not shown for localhost, IP addresses, or .dev domains.',
|
|
2050
|
+
}
|
|
2051
|
+
return
|
|
2052
|
+
}
|
|
2053
|
+
|
|
2054
|
+
const value = registrySnap.data() || {}
|
|
2055
|
+
domainRegistry[domain] = {
|
|
2056
|
+
...fallback,
|
|
2057
|
+
...value,
|
|
2058
|
+
dnsRecords: {
|
|
2059
|
+
...fallback.dnsRecords,
|
|
2060
|
+
...(value.dnsRecords || {}),
|
|
2061
|
+
www: {
|
|
2062
|
+
...fallback.dnsRecords.www,
|
|
2063
|
+
...(value?.dnsRecords?.www || {}),
|
|
2064
|
+
},
|
|
2065
|
+
apex: {
|
|
2066
|
+
...fallback.dnsRecords.apex,
|
|
2067
|
+
...(value?.dnsRecords?.apex || {}),
|
|
2068
|
+
},
|
|
2069
|
+
},
|
|
2070
|
+
}
|
|
2071
|
+
}))
|
|
1924
2072
|
}
|
|
1925
2073
|
|
|
1926
|
-
return {
|
|
2074
|
+
return {
|
|
2075
|
+
project: CLOUDFLARE_PAGES_PROJECT || '',
|
|
2076
|
+
pagesDomain: pagesTarget,
|
|
2077
|
+
domainRegistry,
|
|
2078
|
+
}
|
|
1927
2079
|
})
|
|
1928
2080
|
|
|
1929
2081
|
exports.generateBlockFields = onCall({ timeoutSeconds: 180 }, async (request) => {
|
|
@@ -2071,7 +2223,6 @@ exports.ensurePublishedSiteDomains = onDocumentWritten(
|
|
|
2071
2223
|
const siteRef = change.after.ref
|
|
2072
2224
|
const siteData = change.after.data() || {}
|
|
2073
2225
|
const beforeData = change.before?.data?.() || {}
|
|
2074
|
-
const domainErrorChanged = beforeData?.domainError !== siteData?.domainError
|
|
2075
2226
|
const rawDomains = Array.isArray(siteData.domains) ? siteData.domains : []
|
|
2076
2227
|
const normalizedDomains = Array.from(new Set(rawDomains.map(normalizeDomain).filter(Boolean)))
|
|
2077
2228
|
const beforeRawDomains = Array.isArray(beforeData.domains) ? beforeData.domains : []
|
|
@@ -2141,41 +2292,143 @@ exports.ensurePublishedSiteDomains = onDocumentWritten(
|
|
|
2141
2292
|
filteredDomains = normalizedDomains.filter(domain => !conflictSet.has(domain))
|
|
2142
2293
|
}
|
|
2143
2294
|
|
|
2144
|
-
const
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
2295
|
+
const pagesTarget = getCloudflarePagesTarget()
|
|
2296
|
+
const registryStateByDomain = new Map()
|
|
2297
|
+
const syncPlanMap = new Map()
|
|
2298
|
+
for (const domain of filteredDomains) {
|
|
2299
|
+
const dnsPayload = buildDomainDnsPayload(domain, pagesTarget)
|
|
2300
|
+
registryStateByDomain.set(domain, {
|
|
2301
|
+
...dnsPayload,
|
|
2302
|
+
wwwAdded: false,
|
|
2303
|
+
wwwError: '',
|
|
2304
|
+
apexAttempted: false,
|
|
2305
|
+
apexAdded: false,
|
|
2306
|
+
apexError: '',
|
|
2307
|
+
dnsGuidance: dnsPayload.dnsEligible
|
|
2308
|
+
? 'Add the www CNAME record. Apex is unavailable; forward apex to www.'
|
|
2309
|
+
: 'DNS records are not shown for localhost, IP addresses, or .dev domains.',
|
|
2310
|
+
})
|
|
2311
|
+
|
|
2312
|
+
const apexDomain = dnsPayload.apexDomain
|
|
2313
|
+
if (!apexDomain)
|
|
2314
|
+
continue
|
|
2315
|
+
const existingPlan = syncPlanMap.get(apexDomain) || {
|
|
2316
|
+
apexDomain,
|
|
2317
|
+
wwwDomain: dnsPayload.wwwDomain,
|
|
2318
|
+
domains: new Set(),
|
|
2319
|
+
}
|
|
2320
|
+
existingPlan.domains.add(domain)
|
|
2321
|
+
syncPlanMap.set(apexDomain, existingPlan)
|
|
2322
|
+
}
|
|
2323
|
+
|
|
2324
|
+
const syncPlans = Array.from(syncPlanMap.values())
|
|
2325
|
+
.filter(plan => shouldSyncCloudflareDomain(plan.wwwDomain))
|
|
2326
|
+
.map(plan => ({ ...plan, domains: Array.from(plan.domains) }))
|
|
2327
|
+
|
|
2149
2328
|
const removeDomains = Array.from(new Set(
|
|
2150
2329
|
removedOwnedDomains
|
|
2151
|
-
.
|
|
2330
|
+
.flatMap((domain) => {
|
|
2331
|
+
const apexDomain = getCloudflareApexDomain(domain)
|
|
2332
|
+
const wwwDomain = getCloudflarePagesDomain(apexDomain)
|
|
2333
|
+
return [wwwDomain, apexDomain]
|
|
2334
|
+
})
|
|
2152
2335
|
.filter(domain => shouldSyncCloudflareDomain(domain)),
|
|
2153
2336
|
))
|
|
2154
2337
|
if (removeDomains.length) {
|
|
2155
2338
|
await Promise.all(removeDomains.map(domain => removeCloudflarePagesDomain(domain, { orgId, siteId })))
|
|
2156
2339
|
}
|
|
2157
|
-
|
|
2158
|
-
|
|
2159
|
-
|
|
2340
|
+
|
|
2341
|
+
const syncResults = await Promise.all(syncPlans.map(async (plan) => {
|
|
2342
|
+
const wwwResult = await addCloudflarePagesDomain(plan.wwwDomain, { orgId, siteId, variant: 'www' })
|
|
2343
|
+
let apexAttempted = false
|
|
2344
|
+
let apexResult = { ok: false, error: '' }
|
|
2345
|
+
if (shouldSyncCloudflareDomain(plan.apexDomain)) {
|
|
2346
|
+
apexAttempted = true
|
|
2347
|
+
apexResult = await addCloudflarePagesDomain(plan.apexDomain, { orgId, siteId, variant: 'apex' })
|
|
2348
|
+
}
|
|
2349
|
+
return {
|
|
2350
|
+
...plan,
|
|
2351
|
+
apexAttempted,
|
|
2352
|
+
wwwResult,
|
|
2353
|
+
apexResult,
|
|
2354
|
+
}
|
|
2355
|
+
}))
|
|
2356
|
+
|
|
2357
|
+
for (const plan of syncResults) {
|
|
2358
|
+
const wwwAdded = !!plan.wwwResult?.ok
|
|
2359
|
+
const wwwError = wwwAdded ? '' : String(plan.wwwResult?.error || 'Failed to add www domain.')
|
|
2360
|
+
const apexAdded = !!plan.apexResult?.ok
|
|
2361
|
+
const apexError = apexAdded
|
|
2362
|
+
? ''
|
|
2363
|
+
: (plan.apexAttempted ? String(plan.apexResult?.error || 'Failed to add apex domain.') : '')
|
|
2364
|
+
|
|
2365
|
+
for (const domain of plan.domains) {
|
|
2366
|
+
const current = registryStateByDomain.get(domain) || buildDomainDnsPayload(domain, pagesTarget)
|
|
2367
|
+
const dnsGuidance = !current.dnsEligible
|
|
2368
|
+
? 'DNS records are not shown for localhost, IP addresses, or .dev domains.'
|
|
2369
|
+
: (apexAdded
|
|
2370
|
+
? 'Apex and www were added to Cloudflare Pages. Add both DNS records if your provider requires manual setup.'
|
|
2371
|
+
: 'Add the www CNAME record. Apex is unavailable; forward apex to www.')
|
|
2372
|
+
const nextDnsRecords = {
|
|
2373
|
+
...(current.dnsRecords || {}),
|
|
2374
|
+
apex: {
|
|
2375
|
+
...(current?.dnsRecords?.apex || {}),
|
|
2376
|
+
enabled: !!current.dnsEligible && !!current?.dnsRecords?.apex?.value && apexAdded,
|
|
2377
|
+
},
|
|
2378
|
+
www: {
|
|
2379
|
+
...(current?.dnsRecords?.www || {}),
|
|
2380
|
+
enabled: !!current.dnsEligible && !!current?.dnsRecords?.www?.value,
|
|
2381
|
+
},
|
|
2382
|
+
}
|
|
2383
|
+
|
|
2384
|
+
registryStateByDomain.set(domain, {
|
|
2385
|
+
...current,
|
|
2386
|
+
dnsRecords: nextDnsRecords,
|
|
2387
|
+
wwwAdded,
|
|
2388
|
+
wwwError,
|
|
2389
|
+
apexAttempted: !!plan.apexAttempted,
|
|
2390
|
+
apexAdded,
|
|
2391
|
+
apexError,
|
|
2392
|
+
dnsGuidance,
|
|
2393
|
+
})
|
|
2160
2394
|
}
|
|
2161
|
-
return
|
|
2162
2395
|
}
|
|
2163
2396
|
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
|
|
2167
|
-
|
|
2397
|
+
if (registryStateByDomain.size) {
|
|
2398
|
+
for (const [domain, value] of registryStateByDomain.entries()) {
|
|
2399
|
+
const registryRef = db.collection(DOMAIN_REGISTRY_COLLECTION).doc(domain)
|
|
2400
|
+
const payload = {
|
|
2401
|
+
domain,
|
|
2402
|
+
orgId,
|
|
2403
|
+
siteId,
|
|
2404
|
+
sitePath: siteRef.path,
|
|
2405
|
+
updatedAt: Firestore.FieldValue.serverTimestamp(),
|
|
2406
|
+
apexDomain: value.apexDomain || '',
|
|
2407
|
+
wwwDomain: value.wwwDomain || '',
|
|
2408
|
+
dnsEligible: !!value.dnsEligible,
|
|
2409
|
+
apexAttempted: !!value.apexAttempted,
|
|
2410
|
+
apexAdded: !!value.apexAdded,
|
|
2411
|
+
wwwAdded: !!value.wwwAdded,
|
|
2412
|
+
dnsRecords: value.dnsRecords || {},
|
|
2413
|
+
dnsGuidance: value.dnsGuidance || '',
|
|
2414
|
+
}
|
|
2415
|
+
payload.apexError = value.apexError ? value.apexError : Firestore.FieldValue.delete()
|
|
2416
|
+
payload.wwwError = value.wwwError ? value.wwwError : Firestore.FieldValue.delete()
|
|
2417
|
+
await registryRef.set(payload, { merge: true })
|
|
2418
|
+
}
|
|
2419
|
+
}
|
|
2168
2420
|
|
|
2421
|
+
const failed = syncResults.filter(item => !item.wwwResult?.ok)
|
|
2169
2422
|
if (!failed.length) {
|
|
2170
|
-
if (!conflictDomains.length && siteData.domainError
|
|
2423
|
+
if (!conflictDomains.length && siteData.domainError) {
|
|
2171
2424
|
await siteRef.set({ domainError: Firestore.FieldValue.delete() }, { merge: true })
|
|
2172
2425
|
}
|
|
2173
2426
|
return
|
|
2174
2427
|
}
|
|
2175
2428
|
|
|
2176
|
-
const errorDomains = failed.map(item => item.
|
|
2429
|
+
const errorDomains = failed.map(item => item.wwwDomain)
|
|
2177
2430
|
const errorDetails = failed
|
|
2178
|
-
.map(item => item.
|
|
2431
|
+
.map(item => item.wwwResult?.error)
|
|
2179
2432
|
.filter(Boolean)
|
|
2180
2433
|
.join('; ')
|
|
2181
2434
|
const cloudflareMessage = `Cloudflare domain sync failed for "${errorDomains.join(', ')}". ${errorDetails || 'Check function logs.'}`.trim()
|