@blinkk/root-cms 3.1.4 → 3.1.6

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 (118) hide show
  1. package/dist/app.js +3 -2
  2. package/dist/{chunk-AFBSITEO.js → chunk-PLSNGCYU.js} +200 -22
  3. package/dist/{chunk-MYN76V3L.js → chunk-U4TMO2LI.js} +2 -2
  4. package/dist/{chunk-NC3CRIFA.js → chunk-Z2CTQVYR.js} +1 -1
  5. package/dist/cli.js +1 -1
  6. package/dist/client.js +1 -1
  7. package/dist/core/client.d.ts +30 -2
  8. package/dist/core/cron-schedule.d.ts +63 -0
  9. package/dist/core/plugin.d.ts +6 -1
  10. package/dist/core/schema.d.ts +88 -3
  11. package/dist/core.js +1 -1
  12. package/dist/functions.js +2 -2
  13. package/dist/plugin.js +7 -5
  14. package/dist/richtext.js +1 -1
  15. package/dist/shared/embed-protocol.d.ts +19 -0
  16. package/dist/ui/AIPage-EO5WH25R.js +1 -0
  17. package/dist/ui/{AssetsPage-QUL2NBWK.js → AssetsPage-ESLB4R3I.js} +1 -1
  18. package/dist/ui/CollectionPage-MXN7XKUX.js +1 -0
  19. package/dist/ui/{ComparePage-DCP5QHDE.js → ComparePage-TY7VJVE3.js} +1 -1
  20. package/dist/ui/{DataPage-TUVFBALT.js → DataPage-YEIS7HBN.js} +1 -1
  21. package/dist/ui/{DataSourcePage-YE2ZOHYI.js → DataSourcePage-VD6YBDRW.js} +1 -1
  22. package/dist/ui/{DocTranslationsPage-3ILTQOP2.js → DocTranslationsPage-6XLREBPQ.js} +1 -1
  23. package/dist/ui/DocumentPage-2HNNEGRI.js +1 -0
  24. package/dist/ui/{EditDataSourcePage-WHPZDMD4.js → EditDataSourcePage-E4PSKSG3.js} +1 -1
  25. package/dist/ui/EditReleasePage-DOMXJXCU.js +1 -0
  26. package/dist/ui/EmbeddedAIPage-U5LONYH2.js +1 -0
  27. package/dist/ui/EmbeddedDocumentPage-HYV5GPK6.js +1 -0
  28. package/dist/ui/{ImageEditorDialog-Q44YTLB5.js → ImageEditorDialog-YDKOGVJV.js} +1 -1
  29. package/dist/ui/LogsPage-6KV3CQLH.js +1 -0
  30. package/dist/ui/NewDataSourcePage-NJJDHGPX.js +1 -0
  31. package/dist/ui/NewReleasePage-FXHPFUVG.js +1 -0
  32. package/dist/ui/{NotFoundPage-RMNPVSD3.js → NotFoundPage-CA7XVXUO.js} +1 -1
  33. package/dist/ui/{ProjectPage-VY4EJX6J.js → ProjectPage-XUBJSCWB.js} +1 -1
  34. package/dist/ui/ReleasePage-NLGRYJX7.js +1 -0
  35. package/dist/ui/{ReleasesPage-A4BLUNJE.js → ReleasesPage-OY7OKC3C.js} +1 -1
  36. package/dist/ui/{SettingsPage-MP3YHQDP.js → SettingsPage-5EQUY52A.js} +1 -1
  37. package/dist/ui/SidebarToolsPage-FWG6TIDI.js +1 -0
  38. package/dist/ui/TaskPage-APNRUOYG.js +1 -0
  39. package/dist/ui/TasksPage-FTEELXVZ.js +1 -0
  40. package/dist/ui/{TranslationsArbPage-B7TIRREV.js → TranslationsArbPage-PG5USRYB.js} +1 -1
  41. package/dist/ui/{TranslationsEditPage-VQG4YHA6.js → TranslationsEditPage-GV3ZJXTY.js} +1 -1
  42. package/dist/ui/{TranslationsPage-6VCVMNXI.js → TranslationsPage-3AKSMCNF.js} +1 -1
  43. package/dist/ui/{chunk-5SY2SVEX.js → chunk-235TRWJF.js} +2 -2
  44. package/dist/ui/{chunk-EYVQCOAD.js → chunk-2PTLQRTU.js} +1 -1
  45. package/dist/ui/{chunk-FF4CLAZS.js → chunk-2RRM4HEQ.js} +1 -1
  46. package/dist/ui/{chunk-MAZJRNAL.js → chunk-34W6H4YG.js} +1 -1
  47. package/dist/ui/{chunk-AZ2SK2ZK.js → chunk-3LEYSXZP.js} +1 -1
  48. package/dist/ui/{chunk-L3O3CLWO.js → chunk-44ZU5WMI.js} +1 -1
  49. package/dist/ui/{chunk-MYKJ6VAM.js → chunk-535JU7FU.js} +1 -1
  50. package/dist/ui/chunk-56NMZYX6.js +19 -0
  51. package/dist/ui/{chunk-3WQCVKK4.js → chunk-6WJQBFN5.js} +1 -1
  52. package/dist/ui/{chunk-UHK5N75L.js → chunk-75NMQ3LW.js} +1 -1
  53. package/dist/ui/{chunk-4N6CPIOQ.js → chunk-A7IZAHBZ.js} +1 -1
  54. package/dist/ui/{chunk-2PDMFZBF.js → chunk-C5FRMXDS.js} +1 -1
  55. package/dist/ui/chunk-EYJ6GKZR.js +1 -0
  56. package/dist/ui/{chunk-VB5VMEN7.js → chunk-F7RQT4XN.js} +1 -1
  57. package/dist/ui/chunk-G5647K25.js +1 -0
  58. package/dist/ui/{chunk-74OB6FM4.js → chunk-H37RXS4R.js} +1 -1
  59. package/dist/ui/{chunk-LC6WLVRS.js → chunk-HV3JJ5UT.js} +1 -1
  60. package/dist/ui/{chunk-K3UM5OQF.js → chunk-HXSB6YB2.js} +1 -1
  61. package/dist/ui/chunk-ISUGBMXC.js +1 -0
  62. package/dist/ui/{chunk-WN7HUDGY.js → chunk-JBLSUM6W.js} +1 -1
  63. package/dist/ui/chunk-LAXQZPE3.js +31 -0
  64. package/dist/ui/chunk-MYPLMHFW.js +1 -0
  65. package/dist/ui/{chunk-KBJ4AK5O.js → chunk-NCZD5IWS.js} +1 -1
  66. package/dist/ui/{chunk-XJUQOX4E.js → chunk-O7ESOZQO.js} +1 -1
  67. package/dist/ui/chunk-OWYV43BT.js +1 -0
  68. package/dist/ui/{chunk-XM35ZJBQ.js → chunk-PIJ3E47R.js} +1 -1
  69. package/dist/ui/{chunk-WAT22UHZ.js → chunk-PLYSJ7W5.js} +1 -1
  70. package/dist/ui/{chunk-H3PYPGNR.js → chunk-QFDU6FXP.js} +1 -1
  71. package/dist/ui/{chunk-JSWRHHTZ.js → chunk-QGAPMQEJ.js} +1 -1
  72. package/dist/ui/{chunk-W3ICOHMF.js → chunk-QLIIVG5Q.js} +1 -1
  73. package/dist/ui/{chunk-ELKXEMOU.js → chunk-QRAFYIAW.js} +1 -1
  74. package/dist/ui/{chunk-VRRRFAIV.js → chunk-QWB6MAPJ.js} +1 -1
  75. package/dist/ui/{chunk-GCMX5V3P.js → chunk-RB23LLKL.js} +1 -1
  76. package/dist/ui/{chunk-W2ZAVRAT.js → chunk-ST6QNIWT.js} +57 -57
  77. package/dist/ui/{chunk-76PG55CM.js → chunk-U7LSFUHT.js} +1 -1
  78. package/dist/ui/{chunk-HVYNDEA7.js → chunk-UCYWQEAS.js} +1 -1
  79. package/dist/ui/{chunk-6P75AZ3B.js → chunk-UJX25CXD.js} +1 -1
  80. package/dist/ui/chunk-UOKVF54Y.js +3 -0
  81. package/dist/ui/{chunk-CSUKZZK5.js → chunk-VJRMR2JM.js} +1 -1
  82. package/dist/ui/{chunk-WS37MDM3.js → chunk-WASID5UU.js} +1 -1
  83. package/dist/ui/chunk-WRA24P66.js +7 -0
  84. package/dist/ui/{chunk-ETPZ3TXX.js.LEGAL.txt → chunk-WRA24P66.js.LEGAL.txt} +4 -1
  85. package/dist/ui/{chunk-QHOZXW7Y.js → chunk-Y5EOMMNI.js} +1 -1
  86. package/dist/ui/{chunk-EIMRPYCD.js → chunk-YBHFGQ3Z.js} +1 -1
  87. package/dist/ui/{chunk-62VPTZJL.js → chunk-YP6ZH3MT.js} +1 -1
  88. package/dist/ui/chunk-Z2CRNO3I.js +1 -0
  89. package/dist/ui/{chunk-PR7NB5VK.js → chunk-Z3ZJJWFT.js} +1 -1
  90. package/dist/ui/{chunk-F4F5MH7A.js → chunk-ZAJPNVYS.js} +1 -1
  91. package/dist/ui/ui.css +1 -1
  92. package/dist/ui/ui.js +1 -1
  93. package/package.json +3 -3
  94. package/dist/ui/AIPage-4W4MFELY.js +0 -1
  95. package/dist/ui/CollectionPage-7VU6SUUP.js +0 -1
  96. package/dist/ui/DocumentPage-JFQ3Y2GH.js +0 -1
  97. package/dist/ui/EditReleasePage-QKWHIURV.js +0 -1
  98. package/dist/ui/EmbeddedAIPage-SQLOV4IT.js +0 -1
  99. package/dist/ui/EmbeddedDocumentPage-6FYHUHTE.js +0 -1
  100. package/dist/ui/LogsPage-RC5O722W.js +0 -1
  101. package/dist/ui/NewDataSourcePage-QXW54CHQ.js +0 -1
  102. package/dist/ui/NewReleasePage-TKYWVTOH.js +0 -1
  103. package/dist/ui/ReleasePage-VZAL4KUI.js +0 -1
  104. package/dist/ui/SidebarToolsPage-S5DMIEQ6.js +0 -1
  105. package/dist/ui/TaskPage-LCBDMA4I.js +0 -1
  106. package/dist/ui/TasksPage-72KVSUQY.js +0 -1
  107. package/dist/ui/chunk-ETPZ3TXX.js +0 -7
  108. package/dist/ui/chunk-IMXZTYAA.js +0 -3
  109. package/dist/ui/chunk-J5KDO4RM.js +0 -1
  110. package/dist/ui/chunk-NEA3XU6N.js +0 -19
  111. package/dist/ui/chunk-RN4XVVWD.js +0 -1
  112. package/dist/ui/chunk-SLLTYDVX.js +0 -1
  113. package/dist/ui/chunk-SWTNOWNE.js +0 -1
  114. package/dist/ui/chunk-V3G3554T.js +0 -31
  115. package/dist/ui/chunk-YV5SGR24.js +0 -1
  116. /package/dist/ui/{ImageEditorDialog-Q44YTLB5.js.LEGAL.txt → ImageEditorDialog-YDKOGVJV.js.LEGAL.txt} +0 -0
  117. /package/dist/ui/{chunk-5SY2SVEX.js.LEGAL.txt → chunk-235TRWJF.js.LEGAL.txt} +0 -0
  118. /package/dist/ui/{chunk-W2ZAVRAT.js.LEGAL.txt → chunk-ST6QNIWT.js.LEGAL.txt} +0 -0
package/dist/app.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  getServerVersion,
3
3
  serializeAiConfig
4
- } from "./chunk-MYN76V3L.js";
4
+ } from "./chunk-U4TMO2LI.js";
5
5
  import {
6
6
  getCollectionSchema,
7
7
  getProjectSchemas
@@ -177,7 +177,8 @@ function serializeCollection(collection) {
177
177
  autoSlug: collection.autoSlug,
178
178
  autolock: collection.autolock,
179
179
  autolockReason: collection.autolockReason,
180
- sortOptions: collection.sortOptions
180
+ sortOptions: collection.sortOptions,
181
+ viewOptions: collection.viewOptions
181
182
  };
182
183
  }
183
184
  function isFunction(value) {
@@ -9,6 +9,155 @@ import {
9
9
  Timestamp as Timestamp2
10
10
  } from "firebase-admin/firestore";
11
11
 
12
+ // core/cron-schedule.ts
13
+ var ONE_MINUTE_MS = 60 * 1e3;
14
+ var MAX_LOOKBACK_MS = 366 * 24 * 60 * 60 * 1e3;
15
+ var FIELD_RANGES = [
16
+ { min: 0, max: 59 },
17
+ // minute
18
+ { min: 0, max: 23 },
19
+ // hour
20
+ { min: 1, max: 31 },
21
+ // day of month
22
+ { min: 1, max: 12 },
23
+ // month
24
+ { min: 0, max: 7 }
25
+ // day of week (0 and 7 both mean Sunday)
26
+ ];
27
+ function parseField(field, range) {
28
+ const values = /* @__PURE__ */ new Set();
29
+ const parts = field.split(",");
30
+ for (const part of parts) {
31
+ const trimmed = part.trim();
32
+ if (!trimmed) {
33
+ throw new Error(`invalid cron field: "${field}"`);
34
+ }
35
+ let rangePart = trimmed;
36
+ let step = 1;
37
+ if (trimmed.includes("/")) {
38
+ const [rangeStr, stepStr] = trimmed.split("/");
39
+ rangePart = rangeStr;
40
+ step = Number(stepStr);
41
+ if (!Number.isInteger(step) || step <= 0) {
42
+ throw new Error(`invalid cron step: "${trimmed}"`);
43
+ }
44
+ }
45
+ let start;
46
+ let end;
47
+ if (rangePart === "*") {
48
+ start = range.min;
49
+ end = range.max;
50
+ } else if (rangePart.includes("-")) {
51
+ const [startStr, endStr] = rangePart.split("-");
52
+ start = Number(startStr);
53
+ end = Number(endStr);
54
+ } else {
55
+ start = Number(rangePart);
56
+ end = start;
57
+ }
58
+ if (!Number.isInteger(start) || !Number.isInteger(end) || start < range.min || end > range.max || start > end) {
59
+ throw new Error(`invalid cron field: "${field}"`);
60
+ }
61
+ for (let value = start; value <= end; value += step) {
62
+ values.add(value);
63
+ }
64
+ }
65
+ return values;
66
+ }
67
+ function parseCronExpression(expression) {
68
+ const fields = expression.trim().split(/\s+/);
69
+ if (fields.length !== 5) {
70
+ throw new Error(
71
+ `invalid cron expression: "${expression}" (expected 5 fields)`
72
+ );
73
+ }
74
+ const [minute, hour, dayOfMonth, month, dayOfWeek] = fields;
75
+ const parsedDayOfWeek = parseField(dayOfWeek, FIELD_RANGES[4]);
76
+ if (parsedDayOfWeek.has(7)) {
77
+ parsedDayOfWeek.delete(7);
78
+ parsedDayOfWeek.add(0);
79
+ }
80
+ return {
81
+ minute: parseField(minute, FIELD_RANGES[0]),
82
+ hour: parseField(hour, FIELD_RANGES[1]),
83
+ dayOfMonth: parseField(dayOfMonth, FIELD_RANGES[2]),
84
+ month: parseField(month, FIELD_RANGES[3]),
85
+ dayOfWeek: parsedDayOfWeek,
86
+ dayOfMonthRestricted: dayOfMonth.trim() !== "*",
87
+ dayOfWeekRestricted: dayOfWeek.trim() !== "*"
88
+ };
89
+ }
90
+ function getZonedFields(date, timeZone) {
91
+ const dtf = new Intl.DateTimeFormat("en-US", {
92
+ timeZone,
93
+ hour12: false,
94
+ year: "numeric",
95
+ month: "2-digit",
96
+ day: "2-digit",
97
+ hour: "2-digit",
98
+ minute: "2-digit"
99
+ });
100
+ const parts = {};
101
+ for (const part of dtf.formatToParts(date)) {
102
+ parts[part.type] = part.value;
103
+ }
104
+ const year = Number(parts.year);
105
+ const month = Number(parts.month);
106
+ const day = Number(parts.day);
107
+ let hour = Number(parts.hour);
108
+ if (hour === 24) {
109
+ hour = 0;
110
+ }
111
+ const minute = Number(parts.minute);
112
+ const dayOfWeek = new Date(Date.UTC(year, month - 1, day)).getUTCDay();
113
+ return { minute, hour, dayOfMonth: day, month, dayOfWeek };
114
+ }
115
+ function matchesCron(parsed, fields) {
116
+ if (!parsed.minute.has(fields.minute)) {
117
+ return false;
118
+ }
119
+ if (!parsed.hour.has(fields.hour)) {
120
+ return false;
121
+ }
122
+ if (!parsed.month.has(fields.month)) {
123
+ return false;
124
+ }
125
+ const domMatch = parsed.dayOfMonth.has(fields.dayOfMonth);
126
+ const dowMatch = parsed.dayOfWeek.has(fields.dayOfWeek);
127
+ if (parsed.dayOfMonthRestricted && parsed.dayOfWeekRestricted) {
128
+ return domMatch || dowMatch;
129
+ }
130
+ if (parsed.dayOfMonthRestricted) {
131
+ return domMatch;
132
+ }
133
+ if (parsed.dayOfWeekRestricted) {
134
+ return dowMatch;
135
+ }
136
+ return true;
137
+ }
138
+ function isCronDue(options) {
139
+ const { expression, now } = options;
140
+ const timezone = options.timezone || "UTC";
141
+ const parsed = parseCronExpression(expression);
142
+ let lastSyncMs = options.lastSyncMs;
143
+ if (lastSyncMs > now) {
144
+ lastSyncMs = now;
145
+ }
146
+ const nowMinute = Math.floor(now / ONE_MINUTE_MS) * ONE_MINUTE_MS;
147
+ let lowerBound;
148
+ if (lastSyncMs > 0) {
149
+ lowerBound = Math.max(lastSyncMs, now - MAX_LOOKBACK_MS);
150
+ } else {
151
+ lowerBound = nowMinute - ONE_MINUTE_MS;
152
+ }
153
+ for (let t = nowMinute; t > lowerBound; t -= ONE_MINUTE_MS) {
154
+ if (matchesCron(parsed, getZonedFields(new Date(t), timezone))) {
155
+ return true;
156
+ }
157
+ }
158
+ return false;
159
+ }
160
+
12
161
  // core/translations-manager.ts
13
162
  import {
14
163
  FieldValue,
@@ -670,6 +819,7 @@ function setValueAtPath(obj, path, value) {
670
819
  }
671
820
 
672
821
  // core/client.ts
822
+ var FIRESTORE_DOC_ID_MAX_BYTES = 1500;
673
823
  var RootCMSClient = class {
674
824
  constructor(rootConfig) {
675
825
  this.rootConfig = rootConfig;
@@ -708,6 +858,9 @@ var RootCMSClient = class {
708
858
  }
709
859
  const modeCollection = this.getModeCollection(options.mode);
710
860
  slug = normalizeSlug(slug);
861
+ if (Buffer.byteLength(slug, "utf8") > FIRESTORE_DOC_ID_MAX_BYTES) {
862
+ return null;
863
+ }
711
864
  const dbPath = `Projects/${this.projectId}/Collections/${collectionId}/${modeCollection}/${slug}`;
712
865
  const docRef = this.db.doc(dbPath);
713
866
  const doc = await docRef.get();
@@ -1298,32 +1451,35 @@ ${errorMessages}`);
1298
1451
  for (const snapshot of querySnapshot.docs) {
1299
1452
  const dataSource = snapshot.data();
1300
1453
  const cron = dataSource.cron;
1301
- if (!cron || !cron.enabled || typeof cron.interval !== "number" || !cron.unit) {
1302
- continue;
1303
- }
1304
- const interval = Number(cron.interval);
1305
- if (!Number.isFinite(interval) || interval <= 0 || !Number.isInteger(interval)) {
1454
+ if (!cron || !cron.enabled) {
1306
1455
  continue;
1307
1456
  }
1308
- let intervalMs = interval;
1309
- switch (cron.unit) {
1310
- case "minutes":
1311
- intervalMs *= 60 * 1e3;
1312
- break;
1313
- case "hours":
1314
- intervalMs *= 60 * 60 * 1e3;
1315
- break;
1316
- case "days":
1317
- intervalMs *= 24 * 60 * 60 * 1e3;
1318
- break;
1319
- default:
1457
+ const lastSyncMs = dataSource.syncedAt ? dataSource.syncedAt.toMillis() : 0;
1458
+ const schedule = cron.schedule || "interval";
1459
+ let due;
1460
+ if (schedule === "interval") {
1461
+ due = isIntervalDue(cron, lastSyncMs, now);
1462
+ } else {
1463
+ const expression = (cron.expression || "").trim();
1464
+ if (!expression) {
1320
1465
  continue;
1466
+ }
1467
+ try {
1468
+ due = isCronDue({
1469
+ expression,
1470
+ timezone: cron.timezone,
1471
+ lastSyncMs,
1472
+ now
1473
+ });
1474
+ } catch (err) {
1475
+ console.error(
1476
+ `cron: invalid cron expression for data source ${dataSource.id}: "${expression}":`,
1477
+ String(err)
1478
+ );
1479
+ continue;
1480
+ }
1321
1481
  }
1322
- let lastSyncMs = dataSource.syncedAt ? dataSource.syncedAt.toMillis() : 0;
1323
- if (lastSyncMs > now) {
1324
- lastSyncMs = now;
1325
- }
1326
- if (now - lastSyncMs < intervalMs) {
1482
+ if (!due) {
1327
1483
  continue;
1328
1484
  }
1329
1485
  try {
@@ -2147,6 +2303,28 @@ function unmarshalArray(arrObject) {
2147
2303
  function isObject(data) {
2148
2304
  return typeof data === "object" && !Array.isArray(data) && data !== null;
2149
2305
  }
2306
+ function isIntervalDue(cron, lastSyncMs, now) {
2307
+ const interval = Number(cron.interval);
2308
+ if (!cron.unit || !Number.isFinite(interval) || interval <= 0 || !Number.isInteger(interval)) {
2309
+ return false;
2310
+ }
2311
+ let intervalMs = interval;
2312
+ switch (cron.unit) {
2313
+ case "minutes":
2314
+ intervalMs *= 60 * 1e3;
2315
+ break;
2316
+ case "hours":
2317
+ intervalMs *= 60 * 60 * 1e3;
2318
+ break;
2319
+ case "days":
2320
+ intervalMs *= 24 * 60 * 60 * 1e3;
2321
+ break;
2322
+ default:
2323
+ return false;
2324
+ }
2325
+ const normalizedLastSyncMs = lastSyncMs > now ? now : lastSyncMs;
2326
+ return now - normalizedLastSyncMs >= intervalMs;
2327
+ }
2150
2328
  function parseSpreadsheetUrl(url) {
2151
2329
  if (!url.startsWith("https://docs.google.com/spreadsheets/d/")) {
2152
2330
  return null;
@@ -552,7 +552,7 @@ async function generateImage(rootConfig, options) {
552
552
  // package.json
553
553
  var package_default = {
554
554
  name: "@blinkk/root-cms",
555
- version: "3.1.4",
555
+ version: "3.1.6",
556
556
  author: "s@blinkk.com",
557
557
  license: "MIT",
558
558
  engines: {
@@ -727,7 +727,7 @@ var package_default = {
727
727
  yjs: "13.6.27"
728
728
  },
729
729
  peerDependencies: {
730
- "@blinkk/root": "3.1.4",
730
+ "@blinkk/root": "3.1.6",
731
731
  "firebase-admin": ">=11",
732
732
  "firebase-functions": ">=4"
733
733
  },
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  RootCMSClient,
3
3
  getCmsPlugin
4
- } from "./chunk-AFBSITEO.js";
4
+ } from "./chunk-PLSNGCYU.js";
5
5
 
6
6
  // core/cron.ts
7
7
  import { Timestamp as Timestamp3 } from "firebase-admin/firestore";
package/dist/cli.js CHANGED
@@ -5,7 +5,7 @@ import {
5
5
  RootCMSClient,
6
6
  getCmsPlugin,
7
7
  unmarshalData
8
- } from "./chunk-AFBSITEO.js";
8
+ } from "./chunk-PLSNGCYU.js";
9
9
  import "./chunk-J72JK4KR.js";
10
10
  import "./chunk-MLKGABMK.js";
11
11
 
package/dist/client.js CHANGED
@@ -12,7 +12,7 @@ import {
12
12
  translationsForLocale,
13
13
  unmarshalArray,
14
14
  unmarshalData
15
- } from "./chunk-AFBSITEO.js";
15
+ } from "./chunk-PLSNGCYU.js";
16
16
  import "./chunk-J72JK4KR.js";
17
17
  import "./chunk-MLKGABMK.js";
18
18
  export {
@@ -39,10 +39,38 @@ export type DocMode = 'draft' | 'published';
39
39
  export type UserRole = 'ADMIN' | 'EDITOR' | 'CONTRIBUTOR' | 'VIEWER';
40
40
  export type HttpMethod = 'GET' | 'POST';
41
41
  export type CronUnit = 'minutes' | 'hours' | 'days';
42
+ /**
43
+ * Data source sync schedule type.
44
+ *
45
+ * - `interval`: sync every N minutes/hours/days (uses `interval` + `unit`).
46
+ * - `daily` / `weekly` / `custom`: sync on a specific cron schedule (uses
47
+ * `expression` + `timezone`). `daily` and `weekly` are UI presets that are
48
+ * stored as standard cron expressions; `custom` allows an arbitrary
49
+ * expression.
50
+ */
51
+ export type CronScheduleType = 'interval' | 'daily' | 'weekly' | 'custom';
42
52
  export interface DataSourceCron {
43
53
  enabled: boolean;
44
- interval: number;
45
- unit: CronUnit;
54
+ /**
55
+ * Scheduling mode. Defaults to `interval` when unset (for backwards
56
+ * compatibility with data sources created before specific schedules were
57
+ * supported).
58
+ */
59
+ schedule?: CronScheduleType;
60
+ /** Interval value, used when `schedule` is `interval`. */
61
+ interval?: number;
62
+ /** Interval unit, used when `schedule` is `interval`. */
63
+ unit?: CronUnit;
64
+ /**
65
+ * Standard 5-field cron expression, used when `schedule` is `daily`,
66
+ * `weekly`, or `custom`, e.g. `0 19 * * *` for "every day at 7pm".
67
+ */
68
+ expression?: string;
69
+ /**
70
+ * IANA timezone used to evaluate `expression`, e.g. `America/New_York`.
71
+ * Defaults to UTC when unset.
72
+ */
73
+ timezone?: string;
46
74
  autoPublish?: boolean;
47
75
  }
48
76
  export interface DataSource {
@@ -0,0 +1,63 @@
1
+ /**
2
+ * Utilities for evaluating standard cron expressions for data source
3
+ * scheduling.
4
+ *
5
+ * Supports the standard 5-field cron format:
6
+ *
7
+ * ┌───────────── minute (0 - 59)
8
+ * │ ┌───────────── hour (0 - 23)
9
+ * │ │ ┌───────────── day of month (1 - 31)
10
+ * │ │ │ ┌───────────── month (1 - 12)
11
+ * │ │ │ │ ┌───────────── day of week (0 - 6, where 0 and 7 are Sunday)
12
+ * │ │ │ │ │
13
+ * * * * * *
14
+ *
15
+ * Each field supports `*`, single values, ranges (`a-b`), lists (`a,b,c`),
16
+ * steps (`* /n`, `a-b/n`), and combinations thereof. Expressions are evaluated
17
+ * against a specific IANA timezone so that schedules like "every day at 7pm"
18
+ * fire at the expected wall-clock time.
19
+ */
20
+ interface ParsedCron {
21
+ minute: Set<number>;
22
+ hour: Set<number>;
23
+ dayOfMonth: Set<number>;
24
+ month: Set<number>;
25
+ dayOfWeek: Set<number>;
26
+ /** Whether the day-of-month field was restricted (not `*`). */
27
+ dayOfMonthRestricted: boolean;
28
+ /** Whether the day-of-week field was restricted (not `*`). */
29
+ dayOfWeekRestricted: boolean;
30
+ }
31
+ /**
32
+ * Parses a standard 5-field cron expression. Throws if the expression is
33
+ * malformed.
34
+ */
35
+ export declare function parseCronExpression(expression: string): ParsedCron;
36
+ /**
37
+ * Returns true if the given cron expression is syntactically valid.
38
+ */
39
+ export declare function isValidCronExpression(expression: string): boolean;
40
+ export interface IsCronDueOptions {
41
+ /** Standard 5-field cron expression, e.g. "0 19 * * *". */
42
+ expression: string;
43
+ /** IANA timezone used to evaluate the expression. Defaults to "UTC". */
44
+ timezone?: string;
45
+ /** Timestamp (ms) of the last successful sync, or 0 if never synced. */
46
+ lastSyncMs: number;
47
+ /** Current time (ms). */
48
+ now: number;
49
+ }
50
+ /**
51
+ * Determines whether a cron-scheduled data source is due for sync.
52
+ *
53
+ * Uses catch-up semantics: the source is due if there is any scheduled minute
54
+ * in the window `(lastSyncMs, now]` that matches the cron expression. This
55
+ * ensures a missed cron tick (e.g. the scheduler was briefly down at the
56
+ * scheduled minute) still triggers a sync on a later tick.
57
+ *
58
+ * When the source has never synced (`lastSyncMs <= 0`), only the current minute
59
+ * is considered so that enabling a schedule does not trigger an unexpected
60
+ * immediate backfill — the source will first sync at its next scheduled time.
61
+ */
62
+ export declare function isCronDue(options: IsCronDueOptions): boolean;
63
+ export {};
@@ -3,7 +3,7 @@ import { App } from 'firebase-admin/app';
3
3
  import { Firestore } from 'firebase-admin/firestore';
4
4
  import { type AiConfig } from './ai.js';
5
5
  import { type CMSCheck } from './checks.js';
6
- import { Action } from './client.js';
6
+ import { Action, UserRole } from './client.js';
7
7
  import { type CMSNotificationService } from './services-notifications.js';
8
8
  import { type CMSTranslationService } from './translations.js';
9
9
  export type { AiConfig, AiModelConfig, AiProvider } from './ai.js';
@@ -19,6 +19,11 @@ export type { CMSTranslationService, RootLocale, TranslationExportResult, Transl
19
19
  export type CMSBuiltInSidebarTool = 'home' | 'content' | 'releases' | 'data' | 'assets' | 'translations' | 'ai' | 'settings';
20
20
  export interface CMSUser {
21
21
  email: string;
22
+ /**
23
+ * The user's role within the project's ACL, or `null` if unassigned. This is
24
+ * populated on `req.user` for every authenticated request.
25
+ */
26
+ role?: UserRole | null;
22
27
  }
23
28
  /**
24
29
  * @deprecated The `experiments.ai` flag is now used only as a sidebar toggle.
@@ -17,6 +17,64 @@ export interface CommonFieldProps {
17
17
  /** Hides just the field label in the CMS UI. */
18
18
  hideLabel?: boolean;
19
19
  }
20
+ /**
21
+ * Sources the selectable values for a field from a field within another CMS
22
+ * document.
23
+ *
24
+ * Use this when a set of allowed values is managed centrally in the CMS (e.g. a
25
+ * list of feature flags defined in a global module) and you want other fields
26
+ * to pick from that list instead of typing free-form strings.
27
+ *
28
+ * Example: source a `flag` select's options from the `name` of each item in
29
+ * the `flags` array of the `GlobalModules/flags` document.
30
+ *
31
+ * ```ts
32
+ * schema.select({
33
+ * id: 'flag',
34
+ * source: {
35
+ * doc: 'GlobalModules/flags',
36
+ * field: 'flags',
37
+ * valueKey: 'name',
38
+ * helpKey: 'description',
39
+ * },
40
+ * creatable: true,
41
+ * });
42
+ * ```
43
+ */
44
+ export interface DocumentSource {
45
+ /**
46
+ * The document to source values from, in `<collection>/<slug>` format, e.g.
47
+ * `GlobalModules/flags`.
48
+ */
49
+ doc: string;
50
+ /**
51
+ * The field within the document that holds the values, e.g. `flags`. This is
52
+ * usually an array; for an array of objects use `valueKey`/`labelKey` to map
53
+ * each item, and for an array of strings each string is used directly.
54
+ */
55
+ field: string;
56
+ /**
57
+ * The key on each item to use as the stored value, e.g. `name`. Dot-paths are
58
+ * supported for nested keys (e.g. `meta.id`). Omit for arrays of strings.
59
+ */
60
+ valueKey?: string;
61
+ /**
62
+ * The key on each item to use as the display label, e.g. `name`. Dot-paths
63
+ * are supported. Defaults to `valueKey`.
64
+ */
65
+ labelKey?: string;
66
+ /**
67
+ * The key on each item to use as secondary help text, shown in a smaller,
68
+ * dimmed line beneath the label in the dropdown, e.g. `description`. Dot-paths
69
+ * are supported.
70
+ */
71
+ helpKey?: string;
72
+ }
73
+ /**
74
+ * The source of selectable values for a field. Currently only a CMS document
75
+ * source is supported; other source types may be added in the future.
76
+ */
77
+ export type FieldValueSource = DocumentSource;
20
78
  export type StringField = CommonFieldProps & {
21
79
  type: 'string';
22
80
  default?: string;
@@ -67,19 +125,27 @@ export declare function boolean(field: Omit<BooleanField, 'type'>): BooleanField
67
125
  export type SelectField = CommonFieldProps & {
68
126
  type: 'select';
69
127
  default?: string;
128
+ /** A static list of options to choose from. */
70
129
  options?: Array<{
71
130
  value: string;
72
131
  label?: string;
73
132
  }> | string[];
133
+ /**
134
+ * Dynamically source the options from a field in another CMS document,
135
+ * instead of the static `options` list. See {@link DocumentSource}.
136
+ */
137
+ source?: FieldValueSource;
138
+ /**
139
+ * Whether to allow the user to enter arbitrary values that aren't in the
140
+ * options/source list. Defaults to `false`.
141
+ */
142
+ creatable?: boolean;
74
143
  translate?: boolean;
75
144
  searchable?: boolean;
76
145
  };
77
146
  export declare function select(field: Omit<SelectField, 'type'>): SelectField;
78
147
  export type MultiSelectField = Omit<SelectField, 'type'> & {
79
148
  type: 'multiselect';
80
- /** Set to `true` to allow users to create arbitrary values. */
81
- creatable?: boolean;
82
- translate?: boolean;
83
149
  };
84
150
  export declare function multiselect(field: Omit<MultiSelectField, 'type'>): MultiSelectField;
85
151
  export type ImageField = CommonFieldProps & {
@@ -416,6 +482,25 @@ export type Collection = SchemaWithTypes & {
416
482
  /** Sort direction. Defaults to ascending. */
417
483
  direction?: 'asc' | 'desc';
418
484
  }>;
485
+ /**
486
+ * Options that control how the document listing for this collection is
487
+ * rendered in the CMS.
488
+ */
489
+ viewOptions?: {
490
+ /**
491
+ * Forces the document listing to always use the "compact" view, which
492
+ * renders a condensed table (image, doc id, title, badges, and
493
+ * created/modified timestamps) instead of the default cards with large
494
+ * titles and preview URLs. Useful for collections that don't have
495
+ * meaningful preview metadata (e.g. redirects).
496
+ *
497
+ * When set, the listing is locked to the compact layout and the CMS
498
+ * "View" density control is disabled. When unset, users can choose the
499
+ * listing density (their choice is remembered globally and stays sticky as
500
+ * they navigate between collections).
501
+ */
502
+ compact?: boolean;
503
+ };
419
504
  };
420
505
  export declare function defineCollection(collection: Omit<Collection, 'id'>): Omit<Collection, 'id'>;
421
506
  export declare const collection: typeof defineCollection;
package/dist/core.js CHANGED
@@ -15,7 +15,7 @@ import {
15
15
  translationsForLocale,
16
16
  unmarshalArray,
17
17
  unmarshalData
18
- } from "./chunk-AFBSITEO.js";
18
+ } from "./chunk-PLSNGCYU.js";
19
19
  import {
20
20
  normalizeSlug
21
21
  } from "./chunk-J72JK4KR.js";
package/dist/functions.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  runCronJobs
3
- } from "./chunk-NC3CRIFA.js";
4
- import "./chunk-AFBSITEO.js";
3
+ } from "./chunk-Z2CTQVYR.js";
4
+ import "./chunk-PLSNGCYU.js";
5
5
  import "./chunk-J72JK4KR.js";
6
6
  import "./chunk-MLKGABMK.js";
7
7
 
package/dist/plugin.js CHANGED
@@ -12,16 +12,16 @@ import {
12
12
  serializeAiConfig,
13
13
  summarizeDiff,
14
14
  translateString
15
- } from "./chunk-MYN76V3L.js";
15
+ } from "./chunk-U4TMO2LI.js";
16
16
  import {
17
17
  SearchIndexService,
18
18
  runCronJobs
19
- } from "./chunk-NC3CRIFA.js";
19
+ } from "./chunk-Z2CTQVYR.js";
20
20
  import {
21
21
  RootCMSClient,
22
22
  parseDocId,
23
23
  unmarshalData
24
- } from "./chunk-AFBSITEO.js";
24
+ } from "./chunk-PLSNGCYU.js";
25
25
  import "./chunk-J72JK4KR.js";
26
26
  import "./chunk-MLKGABMK.js";
27
27
 
@@ -1389,7 +1389,8 @@ function cmsPlugin(options) {
1389
1389
  console.log("session failed: user is not in the firestore acl list");
1390
1390
  return null;
1391
1391
  }
1392
- return { email: jwt.email };
1392
+ const role = await cmsClient.getUserRole(jwt.email).catch(() => null);
1393
+ return { email: jwt.email, role };
1393
1394
  }
1394
1395
  try {
1395
1396
  const jwt = await auth.verifySessionCookie(sessionCookie, true);
@@ -1416,7 +1417,8 @@ function cmsPlugin(options) {
1416
1417
  console.log("session failed: user is not in the firestore acl list");
1417
1418
  return null;
1418
1419
  }
1419
- return { email: jwt.email };
1420
+ const role = await cmsClient.getUserRole(jwt.email).catch(() => null);
1421
+ return { email: jwt.email, role };
1420
1422
  } catch (err) {
1421
1423
  console.error("failed to verify jwt token");
1422
1424
  console.error(err);
package/dist/richtext.js CHANGED
@@ -63,7 +63,7 @@ var ALLOWED_ATTRIBUTES = {
63
63
  img: ["src", "alt", "title", "width", "height"]
64
64
  };
65
65
  var COMMON_OPTIONS = {
66
- allowedSchemes: ["http", "https", "mailto"],
66
+ allowedSchemes: ["http", "https", "mailto", "tel", "sms"],
67
67
  allowedSchemesByTag: {
68
68
  img: ["http", "https", "data"]
69
69
  },
@@ -44,6 +44,23 @@ export interface RootEmbedMessage {
44
44
  error?: string;
45
45
  };
46
46
  }
47
+ /**
48
+ * Posted from an iframed sidebar tool to the parent CMS window to report the
49
+ * tool's current location. The CMS mirrors this into its own address bar so
50
+ * the tool's sub-path, query params, and hash survive a refresh and can be
51
+ * deep-linked/shared.
52
+ *
53
+ * Same-origin tools are synced automatically (the CMS reads
54
+ * `contentWindow.location` directly), so this message only needs to be posted
55
+ * by cross-origin tools, which the browser prevents the CMS from reading.
56
+ */
57
+ export interface RootToolLocationMessage {
58
+ rootTool: {
59
+ type: 'locationchange';
60
+ /** The tool's current URL, absolute or relative to the tool's origin. */
61
+ url: string;
62
+ };
63
+ }
47
64
  /** Requests that the doc editor scroll to (focus) a specific field. */
48
65
  export interface ScrollToDeeplinkMessage {
49
66
  scrollToDeeplink: {
@@ -66,6 +83,8 @@ export interface HighlightNodeMessage {
66
83
  }
67
84
  /** Returns whether a postMessage payload is a {@link RootEmbedMessage}. */
68
85
  export declare function isRootEmbedMessage(data: unknown): data is RootEmbedMessage;
86
+ /** Returns whether a postMessage payload is a {@link RootToolLocationMessage}. */
87
+ export declare function isRootToolLocationMessage(data: unknown): data is RootToolLocationMessage;
69
88
  /** Returns whether a postMessage payload is a {@link ScrollToDeeplinkMessage}. */
70
89
  export declare function isScrollToDeeplinkMessage(data: unknown): data is ScrollToDeeplinkMessage;
71
90
  /** Returns whether a postMessage payload is a {@link HighlightNodeMessage}. */
@@ -0,0 +1 @@
1
+ import{a}from"./chunk-56NMZYX6.js";import"./chunk-LAXQZPE3.js";import"./chunk-QWB6MAPJ.js";import"./chunk-235TRWJF.js";import"./chunk-AAR7DBEN.js";import"./chunk-YDO2EDVY.js";import"./chunk-5MV6XVGA.js";import{a as i}from"./chunk-QLIIVG5Q.js";import{c as o}from"./chunk-A7IZAHBZ.js";import"./chunk-AAHM6RYJ.js";import"./chunk-ISUGBMXC.js";import"./chunk-DZYYWCHX.js";import"./chunk-XOWWPE7X.js";import"./chunk-WASID5UU.js";import{Ga as t}from"./chunk-WRA24P66.js";import"./chunk-KUCVLANF.js";import"./chunk-7SK47HHZ.js";import"./chunk-DTMZLUNY.js";import"./chunk-PVAE643M.js";function n(r){return i("AI"),t(o,{children:t(a,{variant:"page",initialChatId:r.chatId})})}export{n as AIPage};