@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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/cms.js +272 -22
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@edgedev/firebase",
3
- "version": "2.2.82",
3
+ "version": "2.2.83",
4
4
  "description": "Vue 3 / Nuxt 3 Plugin or Nuxt 3 plugin for firebase authentication and firestore.",
5
5
  "main": "index.ts",
6
6
  "scripts": {
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.includes('localhost'))
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
- return { project: '' }
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 { project: CLOUDFLARE_PAGES_PROJECT }
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 syncDomains = Array.from(new Set(
2145
- filteredDomains
2146
- .map(domain => getCloudflarePagesDomain(domain))
2147
- .filter(domain => shouldSyncCloudflareDomain(domain)),
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
- .map(domain => getCloudflarePagesDomain(domain))
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
- if (!syncDomains.length) {
2158
- if (!conflictDomains.length && siteData.domainError && !domainErrorChanged) {
2159
- await siteRef.set({ domainError: Firestore.FieldValue.delete() }, { merge: true })
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
- const results = await Promise.all(syncDomains.map(domain => addCloudflarePagesDomain(domain, { orgId, siteId })))
2165
- const failed = results
2166
- .map((result, index) => ({ result, domain: syncDomains[index] }))
2167
- .filter(item => !item.result?.ok)
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 && !domainErrorChanged) {
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.domain)
2426
+ const errorDomains = failed.map(item => item.wwwDomain)
2177
2427
  const errorDetails = failed
2178
- .map(item => item.result?.error)
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()