@fleox-agent-crm/agentcrm 0.0.0-stage → 0.3.0

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 (129) hide show
  1. package/LICENSE +143 -0
  2. package/README.md +223 -2
  3. package/changelog.json +57 -0
  4. package/dist/admin/admin-files.json +26 -0
  5. package/dist/admin/assets/chartsImpl-CE2akJat.js +91 -0
  6. package/dist/admin/assets/firebase-BHSUggRo.js +2 -0
  7. package/dist/admin/assets/gitJson-BulGh2KV.js +3 -0
  8. package/dist/admin/assets/index-DidVVK7L.js +1181 -0
  9. package/dist/admin/assets/index-hMXwHSvN.css +1 -0
  10. package/dist/admin/assets/index.esm-BFc7vwnl.js +400 -0
  11. package/dist/admin/assets/index.esm-CtEQZRb1.js +1651 -0
  12. package/dist/admin/assets/index.esm-Dum0J07z.js +16 -0
  13. package/dist/admin/assets/index.esm2017-EOzKbpUS.js +390 -0
  14. package/dist/admin/assets/lucide-react.min-DEDlHCPj.js +6 -0
  15. package/dist/admin/assets/sdk-CnrLnlRo.js +1717 -0
  16. package/dist/admin/config.js +3 -0
  17. package/dist/admin/favicon.svg +1 -0
  18. package/dist/admin/index.html +139 -0
  19. package/dist/admin/runtime/agentcrm-runtime.js +104 -0
  20. package/dist/admin/runtime/lucide-react.js +791 -0
  21. package/dist/admin/runtime/react-dom-client.js +17 -0
  22. package/dist/admin/runtime/react-dom.js +20 -0
  23. package/dist/admin/runtime/react-i18next.js +19 -0
  24. package/dist/admin/runtime/react-jsx-runtime.js +17 -0
  25. package/dist/admin/runtime/react.js +22 -0
  26. package/dist/agent/agentcrm.mjs +3932 -0
  27. package/dist/agent/chunks/_commonjsHelpers-ClciMIcB.mjs +14 -0
  28. package/dist/agent/chunks/activities-Cj_SIOay.mjs +227 -0
  29. package/dist/agent/chunks/api-B6OqNVWp.mjs +97 -0
  30. package/dist/agent/chunks/appearance-BaShelzt.mjs +174 -0
  31. package/dist/agent/chunks/automations-BWrvLqP6.mjs +698 -0
  32. package/dist/agent/chunks/backup-ClSmxmFZ.mjs +103 -0
  33. package/dist/agent/chunks/batch-CVETiJj5.mjs +142 -0
  34. package/dist/agent/chunks/compact-CsFPaAAq.mjs +69 -0
  35. package/dist/agent/chunks/context-CxFfqS4D.mjs +65 -0
  36. package/dist/agent/chunks/core-Dy7vY9H6.mjs +255 -0
  37. package/dist/agent/chunks/dedupe-BepmsaHv.mjs +123 -0
  38. package/dist/agent/chunks/encoding-C9MzdP95.mjs +36 -0
  39. package/dist/agent/chunks/exportCsv-B5hRvkNm.mjs +95 -0
  40. package/dist/agent/chunks/extCheck--7lmVvl7.mjs +193 -0
  41. package/dist/agent/chunks/extPackage-BGl5Oqs8.mjs +115 -0
  42. package/dist/agent/chunks/extScaffold-DqxI7R1e.mjs +320 -0
  43. package/dist/agent/chunks/extensionActions-D2HtpQz4.mjs +48 -0
  44. package/dist/agent/chunks/extensions-BP1k60md.mjs +151 -0
  45. package/dist/agent/chunks/featureRegistry-C9nbVShp.mjs +149 -0
  46. package/dist/agent/chunks/fields-B0bitgmr.mjs +476 -0
  47. package/dist/agent/chunks/files-CjsPJSR6.mjs +158 -0
  48. package/dist/agent/chunks/files-q8Xu5bft.mjs +367 -0
  49. package/dist/agent/chunks/firebase-DxGVQV9E.mjs +454 -0
  50. package/dist/agent/chunks/firebaseRules-iori-s7U.mjs +221 -0
  51. package/dist/agent/chunks/forms-BKU5F9WF.mjs +847 -0
  52. package/dist/agent/chunks/gitJson-fjr8mcHe.mjs +528 -0
  53. package/dist/agent/chunks/hosting-Bme7LxPc.mjs +144 -0
  54. package/dist/agent/chunks/http-Bk9ICb7I.mjs +127 -0
  55. package/dist/agent/chunks/ids-CvnmrMJX.mjs +59 -0
  56. package/dist/agent/chunks/ids-u-noYcy5.mjs +14 -0
  57. package/dist/agent/chunks/importCsv-DmE_sBch.mjs +334 -0
  58. package/dist/agent/chunks/index-B-4W0-P4.mjs +535 -0
  59. package/dist/agent/chunks/index-BM8qeGOb.mjs +2489 -0
  60. package/dist/agent/chunks/index-Bnmav-Oh.mjs +311 -0
  61. package/dist/agent/chunks/index-CRkw3_iF.mjs +147 -0
  62. package/dist/agent/chunks/index-CWrrF1wl.mjs +111 -0
  63. package/dist/agent/chunks/index-DUSdTBkz.mjs +2988 -0
  64. package/dist/agent/chunks/index-RCmo-uld.mjs +26 -0
  65. package/dist/agent/chunks/index-w03ReRs4.mjs +9780 -0
  66. package/dist/agent/chunks/install-CpHoUFtL.mjs +188 -0
  67. package/dist/agent/chunks/jsx-runtime-Ca2uwGDr.mjs +76 -0
  68. package/dist/agent/chunks/loader-C9nm3iwx.mjs +184 -0
  69. package/dist/agent/chunks/lucide-react.min-Bt3gtepY.mjs +64 -0
  70. package/dist/agent/chunks/memory-WNR3VHUJ.mjs +56 -0
  71. package/dist/agent/chunks/migrate-CsClnnN8.mjs +182 -0
  72. package/dist/agent/chunks/modules-CqdVQVJb.mjs +177 -0
  73. package/dist/agent/chunks/nodeImport-BPCt5uMu.mjs +38 -0
  74. package/dist/agent/chunks/package-DWbn3t2R.mjs +194 -0
  75. package/dist/agent/chunks/papaparse-DjBYnQM2.mjs +1285 -0
  76. package/dist/agent/chunks/pipelines-DCdRBNBV.mjs +234 -0
  77. package/dist/agent/chunks/plan-C0TBrU92.mjs +254 -0
  78. package/dist/agent/chunks/pluginRegistry-DyHtlkcr.mjs +83 -0
  79. package/dist/agent/chunks/query-C5nJB_3C.mjs +192 -0
  80. package/dist/agent/chunks/records-DoqiT9xR.mjs +2191 -0
  81. package/dist/agent/chunks/registry-Ceexawxl.mjs +182 -0
  82. package/dist/agent/chunks/registry-DijAKZ24.mjs +431 -0
  83. package/dist/agent/chunks/relations-B9TynBmA.mjs +191 -0
  84. package/dist/agent/chunks/reports-BB59N6ke.mjs +189 -0
  85. package/dist/agent/chunks/runtime-DHo7ZEEO.mjs +14852 -0
  86. package/dist/agent/chunks/runtimeConfig-CCOFg1RM.mjs +250 -0
  87. package/dist/agent/chunks/s3-Dz50-NuA.mjs +903 -0
  88. package/dist/agent/chunks/schema-7-4-qtSS.mjs +203 -0
  89. package/dist/agent/chunks/schema-DlHt2PxI.mjs +367 -0
  90. package/dist/agent/chunks/sdk-whvfg786.mjs +43644 -0
  91. package/dist/agent/chunks/seed-CugPGYZL.mjs +523 -0
  92. package/dist/agent/chunks/server-ISwlDRCN.mjs +242 -0
  93. package/dist/agent/chunks/settings-BtvR_jdm.mjs +76 -0
  94. package/dist/agent/chunks/standardObjects-CheqfDqa.mjs +243 -0
  95. package/dist/agent/chunks/stats-CfeJssSS.mjs +51 -0
  96. package/dist/agent/chunks/store-5gqRSKXg.mjs +122 -0
  97. package/dist/agent/chunks/themes-DPZFv3yc.mjs +43 -0
  98. package/dist/agent/chunks/tools-Bew8lxKx.mjs +844 -0
  99. package/dist/agent/chunks/types-BOy9PUl7.mjs +44 -0
  100. package/dist/agent/chunks/users-DI1BvS0f.mjs +107 -0
  101. package/dist/agent/chunks/utils-DmI8FtXP.mjs +83 -0
  102. package/dist/agent/chunks/views-fO_BM5U_.mjs +109 -0
  103. package/dist/version.json +7 -0
  104. package/package.json +97 -4
  105. package/plugin/.claude-plugin/plugin.json +17 -0
  106. package/plugin/skills/agentcrm-admin/SKILL.md +191 -0
  107. package/plugin/skills/agentcrm-admin/references/appearance.md +26 -0
  108. package/plugin/skills/agentcrm-admin/references/import.md +16 -0
  109. package/plugin/skills/agentcrm-admin/references/records.md +30 -0
  110. package/plugin/skills/agentcrm-admin/references/schema.md +59 -0
  111. package/plugin/skills/agentcrm-extensions/SKILL.md +93 -0
  112. package/plugin/skills/agentcrm-extensions/references/module-recipes.md +213 -0
  113. package/plugin/skills/agentcrm-extensions/references/runtime-api.md +41 -0
  114. package/plugin/skills/agentcrm-extensions/references/testing.md +76 -0
  115. package/plugin/skills/agentcrm-extensions/references/theme-recipes.md +65 -0
  116. package/plugin/skills/agentcrm-install/SKILL.md +116 -0
  117. package/plugin/skills/agentcrm-install/references/firebase.md +18 -0
  118. package/plugin/skills/agentcrm-install/references/github.md +20 -0
  119. package/plugin/skills/agentcrm-install/references/mcp.md +17 -0
  120. package/plugin/skills/agentcrm-install/references/plans.md +45 -0
  121. package/plugin/skills/agentcrm-install/references/s3.md +24 -0
  122. package/plugin/skills/agentcrm-manage/SKILL.md +108 -0
  123. package/plugin/skills/agentcrm-manage/references/cron.md +11 -0
  124. package/plugin/skills/agentcrm-manage/references/doctor.md +18 -0
  125. package/plugin/skills/agentcrm-sales/SKILL.md +92 -0
  126. package/plugin/skills/agentcrm-sales/references/enrichment.md +13 -0
  127. package/plugin/skills/agentcrm-sales/references/playbooks.md +29 -0
  128. package/plugin/skills/agentcrm-sales/references/weekly-report.md +27 -0
  129. package/types/agentcrm-runtime.d.ts +894 -0
@@ -0,0 +1,454 @@
1
+ import { createRequire as __acrmRequire } from "node:module";
2
+ import { fileURLToPath as __acrmPath } from "node:url";
3
+ import { dirname as __acrmDirname } from "node:path";
4
+ const require2 = __acrmRequire(import.meta.url);
5
+ const __filename = __acrmPath(import.meta.url);
6
+ const __dirname = __acrmDirname(__filename);
7
+ import "./jsx-runtime-Ca2uwGDr.mjs";
8
+ import { r as reactExports } from "./index-Bnmav-Oh.mjs";
9
+ import { c as createStoreCore, p as plainCopy, e as emptySnapshot, n as normalizeDoc } from "./core-Dy7vY9H6.mjs";
10
+ import { w as withFileStore, F as FileStoreUnavailableError } from "./index-RCmo-uld.mjs";
11
+ import { COLLECTIONS } from "./types-BOy9PUl7.mjs";
12
+ import { a as assertFileId } from "./ids-u-noYcy5.mjs";
13
+ import "./_commonjsHelpers-ClciMIcB.mjs";
14
+ const adapters = /* @__PURE__ */ new Map();
15
+ function registerAuthAdapter(backend, adapter) {
16
+ adapters.set(backend, adapter);
17
+ }
18
+ reactExports.createContext(null);
19
+ const DELETE_FIELD = Symbol("deleteField");
20
+ function errorCode(err) {
21
+ return err?.code ?? "";
22
+ }
23
+ function isPermissionDenied(err) {
24
+ const code = errorCode(err);
25
+ return code === "permission-denied" || code === "firestore/permission-denied";
26
+ }
27
+ const FIRESTORE_BATCH_LIMIT = 500;
28
+ function chunk(items, size) {
29
+ const out = [];
30
+ for (let i = 0; i < items.length; i += size) out.push(items.slice(i, i + size));
31
+ return out;
32
+ }
33
+ const INVITES_COLLECTION = "invites";
34
+ const EXTENSION_CHUNKS_COLLECTION = "extensionChunks";
35
+ const EXTENSION_CHUNK_CHARS = 3e5;
36
+ const INBOX_COLLECTION = "inbox";
37
+ const ROLES = ["admin", "member", "viewer"];
38
+ class FirebaseAccessError extends Error {
39
+ constructor(reason, message, email) {
40
+ super(message);
41
+ this.reason = reason;
42
+ this.email = email;
43
+ this.name = "FirebaseAccessError";
44
+ }
45
+ }
46
+ function isFirebaseStore(store) {
47
+ return store.kind === "firebase" && typeof store.accessError === "function";
48
+ }
49
+ function toData(doc) {
50
+ const { id: _id, ...rest } = plainCopy(doc);
51
+ return rest;
52
+ }
53
+ function fromData(d) {
54
+ return normalizeDoc({ ...d.data, id: d.id });
55
+ }
56
+ function isRole(v) {
57
+ return typeof v === "string" && ROLES.includes(v);
58
+ }
59
+ function emailKey(email) {
60
+ return typeof email === "string" ? email.trim().toLowerCase() : "";
61
+ }
62
+ function isPlaceholder(doc) {
63
+ return !!doc && !doc.authUid && !!emailKey(doc.email);
64
+ }
65
+ function inviteOps(mutations, before, after) {
66
+ const ops = /* @__PURE__ */ new Map();
67
+ for (const m of mutations) {
68
+ if (m.collection !== "users") continue;
69
+ const id = m.op === "set" ? m.doc.id : m.id;
70
+ const prev = before(id);
71
+ const next = after.users.get(id);
72
+ const prevEmail = isPlaceholder(prev) ? emailKey(prev.email) : "";
73
+ const nextEmail = isPlaceholder(next) ? emailKey(next.email) : "";
74
+ if (prevEmail && prevEmail !== nextEmail) ops.set(prevEmail, { type: "delete", collection: INVITES_COLLECTION, id: prevEmail });
75
+ if (nextEmail && next) {
76
+ if (next.disabled) ops.set(nextEmail, { type: "delete", collection: INVITES_COLLECTION, id: nextEmail });
77
+ else ops.set(nextEmail, { type: "set", collection: INVITES_COLLECTION, id: nextEmail, data: { role: isRole(next.role) ? next.role : "member", userId: id, email: nextEmail } });
78
+ }
79
+ }
80
+ return [...ops.values()];
81
+ }
82
+ const chunkId = (ext, file, i) => `${ext}__${file}__${i}`;
83
+ function chunkCounts(doc) {
84
+ const files = doc?.files ?? {};
85
+ const out = {};
86
+ for (const [k, v] of Object.entries(files)) if (typeof v === "string") out[k] = Math.ceil(v.length / EXTENSION_CHUNK_CHARS);
87
+ return out;
88
+ }
89
+ function extensionSetOps(doc, previous) {
90
+ const files = doc.files ?? {};
91
+ const size = Object.values(files).reduce((n, v) => n + (typeof v === "string" ? v.length : 0), 0);
92
+ const ops = [];
93
+ const counts = {};
94
+ const data = toData(doc);
95
+ if (size > EXTENSION_CHUNK_CHARS) {
96
+ for (const [file, v] of Object.entries(files)) {
97
+ if (typeof v !== "string") continue;
98
+ const n = Math.ceil(v.length / EXTENSION_CHUNK_CHARS);
99
+ counts[file] = n;
100
+ for (let i = 0; i < n; i++) {
101
+ ops.push({ type: "set", collection: EXTENSION_CHUNKS_COLLECTION, id: chunkId(doc.id, file, i), data: { ext: doc.id, file, index: i, data: v.slice(i * EXTENSION_CHUNK_CHARS, (i + 1) * EXTENSION_CHUNK_CHARS) } });
102
+ }
103
+ }
104
+ data.files = {};
105
+ data.chunks = counts;
106
+ }
107
+ for (const [file, n] of Object.entries(chunkCounts(previous))) {
108
+ for (let i = counts[file] ?? 0; i < n; i++) ops.push({ type: "delete", collection: EXTENSION_CHUNKS_COLLECTION, id: chunkId(doc.id, file, i) });
109
+ }
110
+ ops.unshift({ type: "set", collection: "extensions", id: doc.id, data });
111
+ return ops;
112
+ }
113
+ function assembleExtensionDocs(docs, chunks) {
114
+ const byExt = /* @__PURE__ */ new Map();
115
+ for (const c of chunks) {
116
+ const ext = String(c.data.ext ?? "");
117
+ byExt.set(ext, [...byExt.get(ext) ?? [], c]);
118
+ }
119
+ return docs.map((d) => {
120
+ const counts = d.data.chunks;
121
+ if (!counts || typeof counts !== "object") return d;
122
+ const files = {};
123
+ const parts = byExt.get(d.id) ?? [];
124
+ for (const [file, n] of Object.entries(counts)) {
125
+ const pieces = parts.filter((p) => p.data.file === file).sort((a, b) => Number(a.data.index) - Number(b.data.index));
126
+ if (pieces.length !== n) continue;
127
+ files[file] = pieces.map((p) => String(p.data.data ?? "")).join("");
128
+ }
129
+ const { chunks: _chunks, ...rest } = d.data;
130
+ return { id: d.id, data: { ...rest, files } };
131
+ });
132
+ }
133
+ function mutationOps(mutations, before) {
134
+ const ops = [];
135
+ for (const m of mutations) {
136
+ if (m.op === "set" && m.collection === "extensions") ops.push(...extensionSetOps(m.doc, before.extensions.get(m.doc.id)));
137
+ else if (m.op === "set") ops.push({ type: "set", collection: m.collection, id: m.doc.id, data: toData(m.doc) });
138
+ else if (m.op === "delete") {
139
+ ops.push({ type: "delete", collection: m.collection, id: m.id });
140
+ if (m.collection === "extensions") {
141
+ for (const [file, n] of Object.entries(chunkCounts(before.extensions.get(m.id)))) for (let i = 0; i < n; i++) ops.push({ type: "delete", collection: EXTENSION_CHUNKS_COLLECTION, id: chunkId(m.id, file, i) });
142
+ }
143
+ } else {
144
+ if (!before[m.collection].has(m.id)) continue;
145
+ const data = {};
146
+ for (const [k, v] of Object.entries(m.fields)) {
147
+ if (k === "id" || k.startsWith("id.")) continue;
148
+ data[k] = v === void 0 ? DELETE_FIELD : plainCopy(v);
149
+ }
150
+ if (Object.keys(data).length) ops.push({ type: "update", collection: m.collection, id: m.id, data });
151
+ }
152
+ }
153
+ return ops;
154
+ }
155
+ const STORAGE_FILES_DIR = "files";
156
+ const STORAGE_NOT_ENABLED = "Firebase Storage isn't enabled for this project: enable it in the Firebase console (Build → Storage), add `storageBucket` to the web config and deploy the storage rules (docs/files.md).";
157
+ function firebaseFileStore(api) {
158
+ const storage = () => {
159
+ if (!api.storage) throw new FileStoreUnavailableError(STORAGE_NOT_ENABLED);
160
+ return api.storage;
161
+ };
162
+ const path = (id) => {
163
+ assertFileId(id);
164
+ return `${STORAGE_FILES_DIR}/${id}`;
165
+ };
166
+ const wrap = async (fn) => {
167
+ try {
168
+ return await fn();
169
+ } catch (err) {
170
+ const code = errorCode(err);
171
+ if (code === "storage/unknown" || code === "storage/bucket-not-found" || code === "storage/no-default-bucket") throw new FileStoreUnavailableError(STORAGE_NOT_ENABLED);
172
+ throw err;
173
+ }
174
+ };
175
+ return {
176
+ kind: "firebase",
177
+ put: (id, bytes, meta) => wrap(() => storage().put(path(id), bytes, meta?.mime)),
178
+ async putMany(items) {
179
+ for (const it of items) await wrap(() => storage().put(path(it.id), it.bytes, it.meta?.mime));
180
+ },
181
+ get: (id) => wrap(() => storage().get(path(id))),
182
+ delete: (id) => wrap(() => storage().delete(path(id)))
183
+ };
184
+ }
185
+ function createFirebaseStoreWith(opts) {
186
+ const api = opts.api;
187
+ const requireVerified = opts.requireVerifiedEmail !== false;
188
+ const now = opts.now ?? Date.now;
189
+ let identityPromise = null;
190
+ let currentIdentity = null;
191
+ let lastAccessError = null;
192
+ let rawUsers = /* @__PURE__ */ new Map();
193
+ let reconciling = false;
194
+ const offAuth = api.auth.onChange((u) => {
195
+ if (!u || u.uid !== currentIdentity?.uid) {
196
+ identityPromise = null;
197
+ currentIdentity = null;
198
+ }
199
+ });
200
+ function buildSnapshot(raw, chunks) {
201
+ const s = emptySnapshot();
202
+ for (const c of COLLECTIONS) {
203
+ const docs = c === "extensions" ? assembleExtensionDocs(raw.get(c) ?? [], chunks) : raw.get(c) ?? [];
204
+ for (const d of docs) s[c].set(d.id, fromData(d));
205
+ }
206
+ rawUsers = new Map(s.users);
207
+ for (const u of [...s.users.values()]) {
208
+ if (typeof u.replaces === "string" && u.replaces !== u.id) s.users.delete(u.replaces);
209
+ }
210
+ return s;
211
+ }
212
+ async function readCollection(c) {
213
+ try {
214
+ return await api.listCollection(c);
215
+ } catch (err) {
216
+ if (isPermissionDenied(err)) return [];
217
+ throw err;
218
+ }
219
+ }
220
+ async function ensureUserRecord(user) {
221
+ const email = emailKey(user.email);
222
+ let existing = null;
223
+ try {
224
+ existing = await api.getDoc("users", user.uid);
225
+ } catch (err) {
226
+ if (!isPermissionDenied(err)) throw err;
227
+ }
228
+ if (existing) return isRole(existing.role) ? existing.role : "viewer";
229
+ if (requireVerified && !user.emailVerified) throw new FirebaseAccessError("unverified", "Verify your email address to continue.", email);
230
+ const base = {
231
+ email,
232
+ name: user.displayName || email.split("@")[0] || "User",
233
+ authUid: user.uid,
234
+ createdAt: now(),
235
+ updatedAt: now(),
236
+ ...user.photoURL ? { avatarUrl: user.photoURL } : {}
237
+ };
238
+ const create = (role2, extra = {}) => api.commit([{ type: "set", collection: "users", id: user.uid, data: { ...base, ...extra, role: role2 } }]);
239
+ try {
240
+ await create("admin");
241
+ return "admin";
242
+ } catch (err) {
243
+ if (!isPermissionDenied(err)) throw err;
244
+ }
245
+ let invite = null;
246
+ try {
247
+ invite = email ? await api.getDoc(INVITES_COLLECTION, email) : null;
248
+ } catch (err) {
249
+ if (!isPermissionDenied(err)) throw err;
250
+ }
251
+ const role = invite && isRole(invite.role) ? invite.role : "member";
252
+ try {
253
+ await create(role, invite && typeof invite.userId === "string" ? { replaces: invite.userId } : {});
254
+ return role;
255
+ } catch (err) {
256
+ if (!isPermissionDenied(err)) throw err;
257
+ }
258
+ throw new FirebaseAccessError("notInvited", "This account has not been invited to this CRM. Ask an admin to add your email in Settings → Users.", email);
259
+ }
260
+ async function resolveIdentity() {
261
+ const user = await api.auth.currentUser();
262
+ if (!user) {
263
+ lastAccessError = null;
264
+ return null;
265
+ }
266
+ try {
267
+ const role = await ensureUserRecord(user);
268
+ lastAccessError = null;
269
+ return { uid: user.uid, email: emailKey(user.email), displayName: user.displayName || emailKey(user.email), role };
270
+ } catch (err) {
271
+ lastAccessError = err instanceof FirebaseAccessError ? err : new FirebaseAccessError("error", err.message, emailKey(user.email));
272
+ return null;
273
+ }
274
+ }
275
+ async function persist(mutations, before, after) {
276
+ const ops = mutationOps(mutations, before);
277
+ if (currentIdentity?.role === "admin") ops.push(...inviteOps(mutations, (id) => before.users.get(id) ?? rawUsers.get(id), after));
278
+ for (const part of chunk(ops, FIRESTORE_BATCH_LIMIT)) await api.commit(part);
279
+ for (const m of mutations) {
280
+ if (m.collection !== "users") continue;
281
+ const id = m.op === "set" ? m.doc.id : m.id;
282
+ const doc = after.users.get(id);
283
+ if (doc) rawUsers.set(id, doc);
284
+ else rawUsers.delete(id);
285
+ }
286
+ }
287
+ const core = createStoreCore({
288
+ kind: "firebase",
289
+ async load() {
290
+ const raw = /* @__PURE__ */ new Map();
291
+ let chunks = [];
292
+ await Promise.all([...COLLECTIONS.map(async (c) => raw.set(c, await readCollection(c))), readCollection(EXTENSION_CHUNKS_COLLECTION).then((d) => chunks = d)]);
293
+ const snap = buildSnapshot(raw, chunks);
294
+ scheduleReconcile();
295
+ return snap;
296
+ },
297
+ persist: (mutations, before, after) => persist(mutations, before, after),
298
+ watch(onRemote) {
299
+ const raw = /* @__PURE__ */ new Map();
300
+ let chunks = null;
301
+ const emit = () => {
302
+ if (raw.size < COLLECTIONS.length || chunks === null) return;
303
+ onRemote(buildSnapshot(raw, chunks));
304
+ scheduleReconcile();
305
+ };
306
+ const offs = COLLECTIONS.map(
307
+ (c) => api.watchCollection(
308
+ c,
309
+ (docs) => {
310
+ raw.set(c, docs);
311
+ emit();
312
+ },
313
+ (err) => {
314
+ if (isPermissionDenied(err)) raw.set(c, []);
315
+ else console.warn(`[firebase] live updates of ${c} stopped:`, err);
316
+ }
317
+ )
318
+ );
319
+ offs.push(
320
+ api.watchCollection(
321
+ EXTENSION_CHUNKS_COLLECTION,
322
+ (docs) => {
323
+ chunks = docs;
324
+ emit();
325
+ },
326
+ (err) => {
327
+ if (isPermissionDenied(err)) chunks = [];
328
+ else console.warn("[firebase] live updates of extension chunks stopped:", err);
329
+ }
330
+ )
331
+ );
332
+ return () => offs.forEach((off) => off());
333
+ },
334
+ identity() {
335
+ if (!identityPromise) {
336
+ identityPromise = resolveIdentity().then((id) => {
337
+ currentIdentity = id;
338
+ if (!id) identityPromise = null;
339
+ return id;
340
+ });
341
+ }
342
+ return identityPromise;
343
+ },
344
+ close() {
345
+ offAuth();
346
+ }
347
+ });
348
+ function scheduleReconcile() {
349
+ if (currentIdentity?.role !== "admin" || reconciling) return;
350
+ const joined = [...rawUsers.values()].filter((u) => typeof u.replaces === "string");
351
+ if (!joined.length) return;
352
+ reconciling = true;
353
+ setTimeout(() => {
354
+ void reconcile(joined).finally(() => reconciling = false);
355
+ }, 0);
356
+ }
357
+ async function reconcile(joined) {
358
+ const mutations = [];
359
+ const inviteDeletes = [];
360
+ const objects = core.list("objects");
361
+ const userFields = new Map(
362
+ objects.map((o) => [o.id, (o.fields ?? []).filter((f) => f.type === "user").map((f) => f.id)])
363
+ );
364
+ for (const u of joined) {
365
+ const from = u.replaces;
366
+ const to = u.id;
367
+ const placeholder = rawUsers.get(from);
368
+ for (const r of core.list("records")) {
369
+ const fields = {};
370
+ if (r.ownerId === from) fields.ownerId = to;
371
+ if (r.createdBy === from) fields.createdBy = to;
372
+ for (const f of userFields.get(r.object) ?? []) {
373
+ const v = r.fields?.[f];
374
+ if (v === from) fields[`fields.${f}`] = to;
375
+ else if (Array.isArray(v) && v.includes(from)) fields[`fields.${f}`] = v.map((x) => x === from ? to : x);
376
+ }
377
+ if (Object.keys(fields).length) mutations.push({ op: "patch", collection: "records", id: r.id, fields });
378
+ }
379
+ for (const a of core.list("activities")) {
380
+ if (a.assigneeId === from) mutations.push({ op: "patch", collection: "activities", id: a.id, fields: { assigneeId: to } });
381
+ }
382
+ const settings = core.get("settings", "general");
383
+ if (settings?.ownerUserId === from) mutations.push({ op: "patch", collection: "settings", id: "general", fields: { ownerUserId: to } });
384
+ if (placeholder) {
385
+ mutations.push({ op: "delete", collection: "users", id: from });
386
+ if (emailKey(placeholder.email)) inviteDeletes.push({ type: "delete", collection: INVITES_COLLECTION, id: emailKey(placeholder.email) });
387
+ }
388
+ mutations.push({ op: "patch", collection: "users", id: to, fields: { replaces: void 0, ...placeholder && placeholder.role !== u.role && isRole(placeholder.role) ? { role: placeholder.role } : {} } });
389
+ }
390
+ try {
391
+ await core.write(mutations, { message: "Link invited users" });
392
+ if (inviteDeletes.length) await api.commit(inviteDeletes);
393
+ } catch (err) {
394
+ console.warn("[firebase] could not link invited users:", err);
395
+ }
396
+ }
397
+ withFileStore(core, firebaseFileStore(api));
398
+ return Object.assign(core, {
399
+ api,
400
+ accessError: () => lastAccessError,
401
+ resetSession() {
402
+ identityPromise = null;
403
+ currentIdentity = null;
404
+ lastAccessError = null;
405
+ }
406
+ });
407
+ }
408
+ const apis = /* @__PURE__ */ new Map();
409
+ function firebaseApiFor(config) {
410
+ const key = config.firebase.projectId ?? "default";
411
+ let api = apis.get(key);
412
+ if (!api) {
413
+ api = import("./sdk-whvfg786.mjs").then((n) => n.s).then((m) => m.createSdkFirebaseApi(config.firebase));
414
+ apis.set(key, api);
415
+ api.catch(() => apis.delete(key));
416
+ }
417
+ return api;
418
+ }
419
+ let activeApi = null;
420
+ registerAuthAdapter("firebase", {
421
+ async signOut() {
422
+ await activeApi?.auth.signOut();
423
+ }
424
+ });
425
+ async function createFirebaseStore(config, _opts = {}) {
426
+ const cfg = config;
427
+ if (!cfg.firebase?.apiKey || !cfg.firebase.projectId) throw new Error("The Firebase web config (apiKey, projectId) is missing.");
428
+ const api = await firebaseApiFor(cfg);
429
+ activeApi = api;
430
+ return createFirebaseStoreWith({ api });
431
+ }
432
+ export {
433
+ DELETE_FIELD,
434
+ EXTENSION_CHUNKS_COLLECTION,
435
+ EXTENSION_CHUNK_CHARS,
436
+ FIRESTORE_BATCH_LIMIT,
437
+ FirebaseAccessError,
438
+ INBOX_COLLECTION,
439
+ INVITES_COLLECTION,
440
+ STORAGE_FILES_DIR,
441
+ STORAGE_NOT_ENABLED,
442
+ assembleExtensionDocs,
443
+ chunk,
444
+ createFirebaseStore,
445
+ createFirebaseStoreWith,
446
+ emailKey,
447
+ errorCode,
448
+ firebaseApiFor,
449
+ firebaseFileStore,
450
+ inviteOps,
451
+ isFirebaseStore,
452
+ isPermissionDenied,
453
+ mutationOps
454
+ };
@@ -0,0 +1,221 @@
1
+ import { createRequire as __acrmRequire } from "node:module";
2
+ import { fileURLToPath as __acrmPath } from "node:url";
3
+ import { dirname as __acrmDirname } from "node:path";
4
+ const require2 = __acrmRequire(import.meta.url);
5
+ const __filename = __acrmPath(import.meta.url);
6
+ const __dirname = __acrmDirname(__filename);
7
+ const INBOX_LIMITS = {
8
+ maxFields: 60,
9
+ maxMetaKeys: 10,
10
+ formIdPattern: "^[A-Za-z0-9_-]{1,64}$",
11
+ // Accepted clock skew of `createdAt` vs the server (visitors' clocks).
12
+ clockSkewMs: 864e5
13
+ };
14
+ const EMAIL_RE = /^[a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,}$/;
15
+ const DOMAIN_RE = /^[a-z0-9-]+(\.[a-z0-9-]+)+$/;
16
+ function normalizeRulesOptions(opts) {
17
+ const adminEmail = opts.adminEmail.trim().toLowerCase();
18
+ if (!EMAIL_RE.test(adminEmail)) throw new Error(`"${opts.adminEmail}" is not a valid admin email for the rules.`);
19
+ const allowedDomains = (opts.allowedDomains ?? []).map((d) => d.trim().toLowerCase().replace(/^@/, "")).filter(Boolean);
20
+ for (const d of allowedDomains) if (!DOMAIN_RE.test(d)) throw new Error(`"${d}" is not a valid domain.`);
21
+ const signup = opts.signup ?? (allowedDomains.length ? "domain" : "invite");
22
+ if (signup === "domain" && !allowedDomains.length) throw new Error('Sign-up "domain" needs at least one allowed domain.');
23
+ return { adminEmail, signup, allowedDomains, requireVerifiedEmail: opts.requireVerifiedEmail !== false };
24
+ }
25
+ const ADMIN_WRITE = ["objects", "pipelines", "forms", "settings", "config", "extensions"];
26
+ const MEMBER_WRITE = ["records", "activities"];
27
+ function buildFirestoreRules(input) {
28
+ const o = normalizeRulesOptions(input);
29
+ const verified = o.requireVerifiedEmail ? " && request.auth.token.email_verified == true" : "";
30
+ const signupRule = o.signup === "open" ? "true" : o.signup === "domain" ? `callerEmail().matches('^[^@]+@(${o.allowedDomains.map((d) => d.replace(/\./g, "[.]")).join("|")})$')` : "false";
31
+ const lines = [
32
+ "rules_version = '2';",
33
+ "",
34
+ "// Generated by AgentCRM (src/ops/firebaseRules.ts) — see docs/firebase.md.",
35
+ `// Sign-up: ${o.signup}${o.signup === "domain" ? ` (${o.allowedDomains.join(", ")})` : ""}. Re-generate rather than edit by hand.`,
36
+ "service cloud.firestore {",
37
+ " match /databases/{database}/documents {",
38
+ "",
39
+ " // ─── Identity & roles ─────────────────────────────────────────────",
40
+ " function bootstrapAdminEmail() {",
41
+ ` return "${o.adminEmail}";`,
42
+ " }",
43
+ "",
44
+ " function signedIn() {",
45
+ " return request.auth != null;",
46
+ " }",
47
+ "",
48
+ " function hasEmail() {",
49
+ ` return signedIn() && request.auth.token.email != null${verified};`,
50
+ " }",
51
+ "",
52
+ " function callerEmail() {",
53
+ " return request.auth.token.email.lower();",
54
+ " }",
55
+ "",
56
+ " function isBootstrapAdmin() {",
57
+ " return hasEmail() && callerEmail() == bootstrapAdminEmail();",
58
+ " }",
59
+ "",
60
+ " function selfPath() {",
61
+ " return /databases/$(database)/documents/users/$(request.auth.uid);",
62
+ " }",
63
+ "",
64
+ " // admin | member | viewer | none (no record / disabled).",
65
+ " function role() {",
66
+ " return isBootstrapAdmin() ? 'admin'",
67
+ " : !exists(selfPath()) ? 'none'",
68
+ " : get(selfPath()).data.get('disabled', false) == true ? 'none'",
69
+ " : get(selfPath()).data.get('role', 'none');",
70
+ " }",
71
+ "",
72
+ " function isAdmin() {",
73
+ " return signedIn() && role() == 'admin';",
74
+ " }",
75
+ "",
76
+ " function isMember() {",
77
+ " return signedIn() && role() in ['admin', 'member'];",
78
+ " }",
79
+ "",
80
+ " function isActive() {",
81
+ " return signedIn() && role() in ['admin', 'member', 'viewer'];",
82
+ " }",
83
+ "",
84
+ " function validRole(r) {",
85
+ " return r in ['admin', 'member', 'viewer'];",
86
+ " }",
87
+ "",
88
+ " // ─── invites/{email} ──────────────────────────────────────────────",
89
+ " // Mirrored by the admin from the users it adds (Settings → Users).",
90
+ " function invitePath() {",
91
+ " return /databases/$(database)/documents/invites/$(callerEmail());",
92
+ " }",
93
+ "",
94
+ " match /invites/{email} {",
95
+ " allow read: if isAdmin() || (hasEmail() && callerEmail() == email);",
96
+ " allow create, update: if isAdmin() && email == email.lower()",
97
+ " && validRole(request.resource.data.role);",
98
+ " allow delete: if isAdmin();",
99
+ " }",
100
+ "",
101
+ " // ─── users/{uid} ──────────────────────────────────────────────────",
102
+ " function signupAllowed() {",
103
+ ` return ${signupRule};`,
104
+ " }",
105
+ "",
106
+ " // First sign-in: the caller creates its own record.",
107
+ " function selfCreate(uid) {",
108
+ " let d = request.resource.data;",
109
+ " return hasEmail() && request.auth.uid == uid",
110
+ " && d.email == callerEmail()",
111
+ " && d.get('authUid', '') == uid",
112
+ " && d.get('disabled', false) == false",
113
+ " && (",
114
+ " (isBootstrapAdmin() && d.role == 'admin')",
115
+ " || (exists(invitePath()) && d.role == get(invitePath()).data.role",
116
+ " && d.get('replaces', get(invitePath()).data.get('userId', '')) == get(invitePath()).data.get('userId', ''))",
117
+ " || (!exists(invitePath()) && signupAllowed() && d.role == 'member' && !('replaces' in d))",
118
+ " );",
119
+ " }",
120
+ "",
121
+ " match /users/{uid} {",
122
+ " allow read: if isActive() || (signedIn() && request.auth.uid == uid);",
123
+ " allow create: if (isAdmin() && validRole(request.resource.data.role)) || selfCreate(uid);",
124
+ " // Nobody changes their own role / email / disabled flag.",
125
+ " allow update: if (isAdmin() && validRole(request.resource.data.role))",
126
+ " || (isActive() && request.auth.uid == uid",
127
+ " && request.resource.data.diff(resource.data).affectedKeys().hasOnly(['name', 'avatarUrl', 'updatedAt']));",
128
+ " allow delete: if isAdmin();",
129
+ " }",
130
+ "",
131
+ " // ─── CRM data ─────────────────────────────────────────────────────"
132
+ ];
133
+ for (const c of MEMBER_WRITE) {
134
+ lines.push(` match /${c}/{id} {`, " allow read: if isActive();", " allow create, update, delete: if isMember();", " }", "");
135
+ }
136
+ lines.push(
137
+ " // Views: members manage their own private views; shared views are admin-only.",
138
+ " match /views/{id} {",
139
+ " allow read: if isActive();",
140
+ " allow create: if isAdmin()",
141
+ " || (isMember() && request.resource.data.get('ownerId', '') == request.auth.uid",
142
+ " && request.resource.data.get('shared', false) == false);",
143
+ " allow update: if isAdmin()",
144
+ " || (isMember() && resource.data.get('ownerId', '') == request.auth.uid",
145
+ " && request.resource.data.get('ownerId', '') == request.auth.uid",
146
+ " && request.resource.data.get('shared', false) == false);",
147
+ " allow delete: if isAdmin() || (isMember() && resource.data.get('ownerId', '') == request.auth.uid);",
148
+ " }",
149
+ "",
150
+ " // Automations: admins edit; members running them may stamp lastRunAt.",
151
+ " match /automations/{id} {",
152
+ " allow read: if isActive();",
153
+ " allow create, delete: if isAdmin();",
154
+ " allow update: if isAdmin()",
155
+ " || (isMember() && request.resource.data.diff(resource.data).affectedKeys().hasOnly(['lastRunAt', 'updatedAt']));",
156
+ " }",
157
+ "",
158
+ " match /automationRuns/{id} {",
159
+ " allow read: if isActive();",
160
+ " allow create, update: if isMember();",
161
+ " allow delete: if isAdmin();",
162
+ " }",
163
+ "",
164
+ " // File metadata (bytes in Firebase Storage, see storage.rules): members",
165
+ " // upload / attach / soft-delete; only admins purge (compact).",
166
+ " match /files/{id} {",
167
+ " allow read: if isActive();",
168
+ " allow create, update: if isMember();",
169
+ " allow delete: if isAdmin();",
170
+ " }",
171
+ ""
172
+ );
173
+ for (const c of ADMIN_WRITE) {
174
+ lines.push(` match /${c}/{id} {`, " allow read: if isActive();", " allow write: if isAdmin();", " }", "");
175
+ }
176
+ lines.push(
177
+ " // Pieces of large extension bundles (src/data/firebase.ts).",
178
+ " match /extensionChunks/{id} {",
179
+ " allow read: if isActive();",
180
+ " allow write: if isAdmin();",
181
+ " }",
182
+ "",
183
+ " // ─── inbox/{id}: public lead-form submissions ──────────────────────",
184
+ " function validSubmission(d) {",
185
+ " return d.keys().hasAll(['formId', 'data', 'createdAt'])",
186
+ " && d.keys().hasOnly(['formId', 'data', 'createdAt', 'meta'])",
187
+ ` && d.formId is string && d.formId.matches('${INBOX_LIMITS.formIdPattern}')`,
188
+ " && exists(/databases/$(database)/documents/forms/$(d.formId))",
189
+ ` && d.data is map && d.data.size() <= ${INBOX_LIMITS.maxFields}`,
190
+ ` && (!('meta' in d) || (d.meta is map && d.meta.size() <= ${INBOX_LIMITS.maxMetaKeys}))`,
191
+ " && d.createdAt is int",
192
+ ` && d.createdAt > request.time.toMillis() - ${INBOX_LIMITS.clockSkewMs}`,
193
+ ` && d.createdAt < request.time.toMillis() + ${INBOX_LIMITS.clockSkewMs};`,
194
+ " }",
195
+ "",
196
+ " match /inbox/{id} {",
197
+ " // Public: anyone (the static lead form) may create, nobody else reads.",
198
+ " allow create: if validSubmission(request.resource.data);",
199
+ " // Ingested (then deleted) by members — the admin or the CLI.",
200
+ " allow read, delete: if isMember();",
201
+ " // Failed submissions are only marked {status, error}.",
202
+ " allow update: if isMember()",
203
+ " && request.resource.data.diff(resource.data).affectedKeys().hasOnly(['status', 'error']);",
204
+ " }",
205
+ "",
206
+ " // Anything else: closed.",
207
+ " match /{document=**} {",
208
+ " allow read, write: if false;",
209
+ " }",
210
+ " }",
211
+ "}",
212
+ ""
213
+ );
214
+ return lines.join("\n");
215
+ }
216
+ [...MEMBER_WRITE, "views", "automations", "automationRuns", "files", ...ADMIN_WRITE, "users"];
217
+ export {
218
+ INBOX_LIMITS,
219
+ buildFirestoreRules,
220
+ normalizeRulesOptions
221
+ };