@fleox-agent-crm/agentcrm 0.3.3 → 0.5.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.
- package/README.md +16 -16
- package/changelog.json +22 -0
- package/dist/admin/admin-files.json +7 -7
- package/dist/admin/assets/{chartsImpl-aBuZ1X4W.js → chartsImpl-zjsXDhuy.js} +1 -1
- package/dist/admin/assets/firebase-C3h9mF8O.js +2 -0
- package/dist/admin/assets/{gitJson-DzX3eZ2P.js → gitJson-nrOYgX0x.js} +1 -1
- package/dist/admin/assets/{index-DLy5RpPl.js → index-0ejjkW0z.js} +6 -6
- package/dist/admin/assets/{lucide-react.min-BgcmukkH.js → lucide-react.min-CI5dCsSm.js} +1 -1
- package/dist/admin/assets/{sdk-DtP9zqdi.js → sdk-mepKXtVl.js} +135 -120
- package/dist/admin/index.html +1 -1
- package/dist/agent/agentcrm.mjs +159 -74
- package/dist/agent/chunks/api-BEl_EKwf.mjs +79 -0
- package/dist/agent/chunks/{automations-DwWruxmu.mjs → automations-BqY8EwfJ.mjs} +3 -3
- package/dist/agent/chunks/{backup-W3CjgkAJ.mjs → backup-CO0EzdF_.mjs} +3 -3
- package/dist/agent/chunks/{compact-BGHI92tm.mjs → compact-qF5t42ko.mjs} +3 -3
- package/dist/agent/chunks/{exportCsv-C6e6XEmB.mjs → exportCsv-C-fIHLm1.mjs} +2 -2
- package/dist/agent/chunks/{extCheck-GgHG1dYB.mjs → extCheck-BcbUc7eB.mjs} +4 -4
- package/dist/agent/chunks/{files-D1daDgY0.mjs → files-C4YONVqQ.mjs} +2 -2
- package/dist/agent/chunks/{firebase-DWR0K1OC.mjs → firebase-Bknru2pE.mjs} +230 -53
- package/dist/agent/chunks/firebaseCache-sK4jA0ru.mjs +39 -0
- package/dist/agent/chunks/{firebaseRules-iori-s7U.mjs → firebaseRules-Jq85MBBN.mjs} +14 -3
- package/dist/agent/chunks/{forms-BfuWaQjA.mjs → forms-DJDop3XY.mjs} +2 -2
- package/dist/agent/chunks/{hosting-Dnn1k3cV.mjs → hosting-C_UDyMkm.mjs} +1 -1
- package/dist/agent/chunks/{importCsv-LVmMTaWU.mjs → importCsv-XjQJ9QIR.mjs} +2 -2
- package/dist/agent/chunks/{index-DtCezG4R.mjs → index-DWecFbTi.mjs} +1 -1
- package/dist/agent/chunks/{install-Bp-CwDVC.mjs → install-D1lgtRIH.mjs} +4 -4
- package/dist/agent/chunks/{loader-MPD9iAvh.mjs → loader-Cv0UJf6b.mjs} +11 -11
- package/dist/agent/chunks/{migrate-CsClnnN8.mjs → migrate-Cc_8MHZv.mjs} +1 -1
- package/dist/agent/chunks/{pipelines-BAKpPk1w.mjs → pipelines-DiJ2dLqQ.mjs} +3 -3
- package/dist/agent/chunks/{records-DcHFelq5.mjs → records-CCTxxMWF.mjs} +1 -1
- package/dist/agent/chunks/{registry-COfJrSmg.mjs → registry-ClzNZv9Z.mjs} +10 -10
- package/dist/agent/chunks/{reports-BACHGccj.mjs → reports-WlI_A5Nr.mjs} +3 -3
- package/dist/agent/chunks/{runtime-C0Xf0Axi.mjs → runtime-BAF5vU5D.mjs} +17 -17
- package/dist/agent/chunks/{schema-C4lSslXv.mjs → schema-DOnnH2zL.mjs} +3 -3
- package/dist/agent/chunks/schemaApply-DKHipylx.mjs +164 -0
- package/dist/agent/chunks/{sdk-CYgNyuoK.mjs → sdk-NYUHv7WG.mjs} +493 -214
- package/dist/agent/chunks/{seed-6MRmQQBR.mjs → seed-Bb26Zsni.mjs} +2 -2
- package/dist/agent/chunks/{server-DC3t8r30.mjs → server-Bch4TQon.mjs} +3 -3
- package/dist/agent/chunks/{standardObjects-CheqfDqa.mjs → standardObjects-vrmqIVLH.mjs} +1 -1
- package/dist/agent/chunks/{stats-BfuLWhVV.mjs → stats-DfmDP3tE.mjs} +3 -3
- package/dist/agent/chunks/{tools-iSH-yMpl.mjs → tools-CCk24tdh.mjs} +9 -9
- package/dist/agent/chunks/{views-DH1n9Lrt.mjs → views-BbhmQO3n.mjs} +4 -4
- package/dist/version.json +1 -1
- package/package.json +1 -1
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/skills/agentcrm-admin/SKILL.md +67 -67
- package/plugin/skills/agentcrm-admin/references/records.md +5 -5
- package/plugin/skills/agentcrm-admin/references/schema.md +19 -19
- package/plugin/skills/agentcrm-extensions/SKILL.md +8 -8
- package/plugin/skills/agentcrm-extensions/references/module-recipes.md +2 -2
- package/plugin/skills/agentcrm-extensions/references/testing.md +14 -14
- package/plugin/skills/agentcrm-extensions/references/theme-recipes.md +3 -3
- package/plugin/skills/agentcrm-install/SKILL.md +13 -13
- package/plugin/skills/agentcrm-install/references/firebase.md +1 -1
- package/plugin/skills/agentcrm-install/references/github.md +1 -1
- package/plugin/skills/agentcrm-install/references/mcp.md +2 -2
- package/plugin/skills/agentcrm-install/references/plans.md +1 -1
- package/plugin/skills/agentcrm-install/references/s3.md +1 -1
- package/plugin/skills/agentcrm-manage/SKILL.md +15 -15
- package/plugin/skills/agentcrm-sales/SKILL.md +18 -18
- package/dist/admin/assets/firebase-BG1GpyBB.js +0 -2
- package/dist/agent/chunks/api-Dwb4xz1k.mjs +0 -27
|
@@ -0,0 +1,79 @@
|
|
|
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 DELETE_FIELD = Symbol("deleteField");
|
|
8
|
+
const TS_FIELD = "_ts";
|
|
9
|
+
const DELETIONS_COLLECTION = "deletions";
|
|
10
|
+
const SERVER_TIMESTAMP = Symbol("serverTimestamp");
|
|
11
|
+
let trackedCollections = /* @__PURE__ */ new Set();
|
|
12
|
+
function setTrackedCollections(collections) {
|
|
13
|
+
trackedCollections = new Set(collections);
|
|
14
|
+
}
|
|
15
|
+
function tombstoneId(collection, id) {
|
|
16
|
+
return `${collection}__${id}`;
|
|
17
|
+
}
|
|
18
|
+
function withChangeTracking(ops) {
|
|
19
|
+
const out = [];
|
|
20
|
+
for (const op of ops) {
|
|
21
|
+
if (!trackedCollections.has(op.collection)) {
|
|
22
|
+
out.push(op);
|
|
23
|
+
} else if (op.type === "delete") {
|
|
24
|
+
out.push(op, { type: "set", collection: DELETIONS_COLLECTION, id: tombstoneId(op.collection, op.id), data: { collection: op.collection, id: op.id, [TS_FIELD]: SERVER_TIMESTAMP } });
|
|
25
|
+
} else {
|
|
26
|
+
out.push({ ...op, data: { ...op.data, [TS_FIELD]: SERVER_TIMESTAMP } });
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return out;
|
|
30
|
+
}
|
|
31
|
+
function splitTs(id, raw) {
|
|
32
|
+
const { [TS_FIELD]: ts, ...data } = raw;
|
|
33
|
+
return typeof ts === "number" ? { id, data, ts } : { id, data };
|
|
34
|
+
}
|
|
35
|
+
function errorCode(err) {
|
|
36
|
+
return err?.code ?? "";
|
|
37
|
+
}
|
|
38
|
+
function isPermissionDenied(err) {
|
|
39
|
+
const code = errorCode(err);
|
|
40
|
+
return code === "permission-denied" || code === "firestore/permission-denied";
|
|
41
|
+
}
|
|
42
|
+
const FIRESTORE_BATCH_LIMIT = 500;
|
|
43
|
+
function chunkForCommit(ops) {
|
|
44
|
+
const out = [];
|
|
45
|
+
let part = [];
|
|
46
|
+
let weight = 0;
|
|
47
|
+
for (const op of ops) {
|
|
48
|
+
const w = op.type === "delete" && trackedCollections.has(op.collection) ? 2 : 1;
|
|
49
|
+
if (weight + w > FIRESTORE_BATCH_LIMIT) {
|
|
50
|
+
out.push(part);
|
|
51
|
+
part = [];
|
|
52
|
+
weight = 0;
|
|
53
|
+
}
|
|
54
|
+
part.push(op);
|
|
55
|
+
weight += w;
|
|
56
|
+
}
|
|
57
|
+
if (part.length) out.push(part);
|
|
58
|
+
return out;
|
|
59
|
+
}
|
|
60
|
+
function chunk(items, size) {
|
|
61
|
+
const out = [];
|
|
62
|
+
for (let i = 0; i < items.length; i += size) out.push(items.slice(i, i + size));
|
|
63
|
+
return out;
|
|
64
|
+
}
|
|
65
|
+
export {
|
|
66
|
+
DELETE_FIELD as D,
|
|
67
|
+
FIRESTORE_BATCH_LIMIT as F,
|
|
68
|
+
SERVER_TIMESTAMP as S,
|
|
69
|
+
TS_FIELD as T,
|
|
70
|
+
DELETIONS_COLLECTION as a,
|
|
71
|
+
chunk as b,
|
|
72
|
+
chunkForCommit as c,
|
|
73
|
+
splitTs as d,
|
|
74
|
+
errorCode as e,
|
|
75
|
+
isPermissionDenied as i,
|
|
76
|
+
setTrackedCollections as s,
|
|
77
|
+
tombstoneId as t,
|
|
78
|
+
withChangeTracking as w
|
|
79
|
+
};
|
|
@@ -8,16 +8,16 @@ import { toIsoDate, recordLabel, formatFieldValue, normalizeEmail } from "./fiel
|
|
|
8
8
|
import { n as newId } from "./ids-CvnmrMJX.mjs";
|
|
9
9
|
import { plugins } from "./pluginRegistry-DyHtlkcr.mjs";
|
|
10
10
|
import { r as resolveField, m as matchesFilter } from "./query-C5nJB_3C.mjs";
|
|
11
|
-
import { queryContext, setRecordOwner, updateRecord } from "./records-
|
|
11
|
+
import { queryContext, setRecordOwner, updateRecord } from "./records-CCTxxMWF.mjs";
|
|
12
12
|
import { getSettings } from "./settings-Di8nzNEL.mjs";
|
|
13
13
|
import { userOf, automationRunsOf, asDoc, requireRecord, requireObject, automationsOf, objectOf, recordsOf, thaw, recordOf, usersOf } from "./store-5gqRSKXg.mjs";
|
|
14
14
|
import { F as FILTER_OPS } from "../agentcrm.mjs";
|
|
15
15
|
import { lastActivityAt, addNote, createTask } from "./activities-Cj_SIOay.mjs";
|
|
16
16
|
import { OpsError, assertCan } from "./context-CxFfqS4D.mjs";
|
|
17
|
-
import { pipelineOfRecord } from "./pipelines-
|
|
17
|
+
import { pipelineOfRecord } from "./pipelines-DiJ2dLqQ.mjs";
|
|
18
18
|
import { getAutomationAction, listAutomationActions } from "./extensionActions-D2HtpQz4.mjs";
|
|
19
19
|
import { registerAutomationAction, subscribeAutomationActions } from "./extensionActions-D2HtpQz4.mjs";
|
|
20
|
-
import "./standardObjects-
|
|
20
|
+
import "./standardObjects-vrmqIVLH.mjs";
|
|
21
21
|
import "./relations-B9TynBmA.mjs";
|
|
22
22
|
import "./runtimeConfig-CCOFg1RM.mjs";
|
|
23
23
|
import "node:util";
|
|
@@ -7,7 +7,7 @@ const __dirname = __acrmDirname(__filename);
|
|
|
7
7
|
import { J as JSZip } from "./index-w03ReRs4.mjs";
|
|
8
8
|
import { F as FILE_ID_RE } from "./ids-u-noYcy5.mjs";
|
|
9
9
|
import { assertCan, OpsError } from "./context-CxFfqS4D.mjs";
|
|
10
|
-
import { exportJson, importJson, planImportJson } from "./exportCsv-
|
|
10
|
+
import { exportJson, importJson, planImportJson } from "./exportCsv-C-fIHLm1.mjs";
|
|
11
11
|
import { filesOf } from "./store-5gqRSKXg.mjs";
|
|
12
12
|
import "./_commonjsHelpers-ClciMIcB.mjs";
|
|
13
13
|
import "stream";
|
|
@@ -19,9 +19,9 @@ import "./papaparse-Df7i1dHr.mjs";
|
|
|
19
19
|
import "./fields-B0bitgmr.mjs";
|
|
20
20
|
import "./query-C5nJB_3C.mjs";
|
|
21
21
|
import "./types-BOy9PUl7.mjs";
|
|
22
|
-
import "./records-
|
|
22
|
+
import "./records-CCTxxMWF.mjs";
|
|
23
23
|
import "./ids-CvnmrMJX.mjs";
|
|
24
|
-
import "./standardObjects-
|
|
24
|
+
import "./standardObjects-vrmqIVLH.mjs";
|
|
25
25
|
import "./relations-B9TynBmA.mjs";
|
|
26
26
|
const BACKUP_DATA_FILE = "data.json";
|
|
27
27
|
const BACKUP_FILES_DIR = "files/";
|
|
@@ -6,8 +6,8 @@ const __filename = __acrmPath(import.meta.url);
|
|
|
6
6
|
const __dirname = __acrmDirname(__filename);
|
|
7
7
|
import { assertCan } from "./context-CxFfqS4D.mjs";
|
|
8
8
|
import { b as fileCleanupMutations } from "./relations-B9TynBmA.mjs";
|
|
9
|
-
import { purgeFiles } from "./files-
|
|
10
|
-
import { hardDeleteMutations } from "./records-
|
|
9
|
+
import { purgeFiles } from "./files-C4YONVqQ.mjs";
|
|
10
|
+
import { hardDeleteMutations } from "./records-CCTxxMWF.mjs";
|
|
11
11
|
import { recordsOf, activitiesOf, automationRunsOf } from "./store-5gqRSKXg.mjs";
|
|
12
12
|
import "./pluginRegistry-DyHtlkcr.mjs";
|
|
13
13
|
import "./fields-B0bitgmr.mjs";
|
|
@@ -27,7 +27,7 @@ import "node:crypto";
|
|
|
27
27
|
import "node:os";
|
|
28
28
|
import "node:child_process";
|
|
29
29
|
import "node:http";
|
|
30
|
-
import "./standardObjects-
|
|
30
|
+
import "./standardObjects-vrmqIVLH.mjs";
|
|
31
31
|
async function compactData(ctx, opts = {}) {
|
|
32
32
|
assertCan(ctx, "compact");
|
|
33
33
|
const cutoff = ctx.now() - (opts.olderThanDays ?? 0) * 864e5;
|
|
@@ -9,13 +9,13 @@ import { formatAddress, formatFieldValue } from "./fields-B0bitgmr.mjs";
|
|
|
9
9
|
import { a as applySorts, b as applyFilters, r as resolveField, g as getRecordValue } from "./query-C5nJB_3C.mjs";
|
|
10
10
|
import { COLLECTIONS } from "./types-BOy9PUl7.mjs";
|
|
11
11
|
import { assertCan, OpsError } from "./context-CxFfqS4D.mjs";
|
|
12
|
-
import { queryContext } from "./records-
|
|
12
|
+
import { queryContext } from "./records-CCTxxMWF.mjs";
|
|
13
13
|
import { requireObject, recordsOf, userOf } from "./store-5gqRSKXg.mjs";
|
|
14
14
|
import "./_commonjsHelpers-ClciMIcB.mjs";
|
|
15
15
|
import "stream";
|
|
16
16
|
import "./pluginRegistry-DyHtlkcr.mjs";
|
|
17
17
|
import "./ids-CvnmrMJX.mjs";
|
|
18
|
-
import "./standardObjects-
|
|
18
|
+
import "./standardObjects-vrmqIVLH.mjs";
|
|
19
19
|
import "./relations-B9TynBmA.mjs";
|
|
20
20
|
function exportCsv(ctx, objectId, opts = {}) {
|
|
21
21
|
assertCan(ctx, "export");
|
|
@@ -93,11 +93,11 @@ async function loadInSandbox(report, manifest, files, opts) {
|
|
|
93
93
|
import("./memory-WNR3VHUJ.mjs"),
|
|
94
94
|
import("./context-CxFfqS4D.mjs"),
|
|
95
95
|
import("./pluginRegistry-DyHtlkcr.mjs"),
|
|
96
|
-
import("./seed-
|
|
96
|
+
import("./seed-Bb26Zsni.mjs"),
|
|
97
97
|
import("./store-5gqRSKXg.mjs"),
|
|
98
|
-
import("./loader-
|
|
99
|
-
import("./registry-
|
|
100
|
-
import("./tools-
|
|
98
|
+
import("./loader-Cv0UJf6b.mjs"),
|
|
99
|
+
import("./registry-ClzNZv9Z.mjs"),
|
|
100
|
+
import("./tools-CCk24tdh.mjs"),
|
|
101
101
|
import("./extensionActions-D2HtpQz4.mjs"),
|
|
102
102
|
import("./featureRegistry-DSsZbhrh.mjs"),
|
|
103
103
|
import("./themes-DPZFv3yc.mjs"),
|
|
@@ -11,7 +11,7 @@ import { m as mapLimit } from "./http-Bk9ICb7I.mjs";
|
|
|
11
11
|
import { assertCan, OpsError, actorId } from "./context-CxFfqS4D.mjs";
|
|
12
12
|
import { s as sameTarget, e as fileFieldIds, g as fileIdsIn, f as fileTargetPatch, h as referencedFileIds } from "./relations-B9TynBmA.mjs";
|
|
13
13
|
import { timelineScope } from "./activities-Cj_SIOay.mjs";
|
|
14
|
-
import { updateRecord } from "./records-
|
|
14
|
+
import { updateRecord } from "./records-CCTxxMWF.mjs";
|
|
15
15
|
import { fileOf, requireFile, filesOf, recordOf, activitiesOf, settingsDoc, asDoc, requireRecord, objectOf, requireObject, recordsOf } from "./store-5gqRSKXg.mjs";
|
|
16
16
|
import "./index-BM8qeGOb.mjs";
|
|
17
17
|
import "./pluginRegistry-DyHtlkcr.mjs";
|
|
@@ -26,7 +26,7 @@ import "node:crypto";
|
|
|
26
26
|
import "node:os";
|
|
27
27
|
import "node:child_process";
|
|
28
28
|
import "node:http";
|
|
29
|
-
import "./standardObjects-
|
|
29
|
+
import "./standardObjects-vrmqIVLH.mjs";
|
|
30
30
|
const ORPHAN_GRACE_MS = 864e5;
|
|
31
31
|
function maxUploadBytes(ctx) {
|
|
32
32
|
const mb = settingsDoc(ctx.store)?.maxUploadMb;
|
|
@@ -7,7 +7,8 @@ const __dirname = __acrmDirname(__filename);
|
|
|
7
7
|
import "./jsx-runtime-Ca2uwGDr.mjs";
|
|
8
8
|
import { r as reactExports } from "./index-Bnmav-Oh.mjs";
|
|
9
9
|
import { c as createStoreCore, p as plainCopy, e as emptySnapshot, n as normalizeDoc } from "./core-Dy7vY9H6.mjs";
|
|
10
|
-
import { i as isPermissionDenied, D as DELETE_FIELD, e as errorCode, c as
|
|
10
|
+
import { i as isPermissionDenied, D as DELETE_FIELD, a as DELETIONS_COLLECTION, e as errorCode, c as chunkForCommit, s as setTrackedCollections } from "./api-BEl_EKwf.mjs";
|
|
11
|
+
import { F, S, T, b, d, t, w } from "./api-BEl_EKwf.mjs";
|
|
11
12
|
import { w as withFileStore, F as FileStoreUnavailableError } from "./index-RCmo-uld.mjs";
|
|
12
13
|
import { COLLECTIONS } from "./types-BOy9PUl7.mjs";
|
|
13
14
|
import { a as assertFileId } from "./ids-u-noYcy5.mjs";
|
|
@@ -17,10 +18,70 @@ function registerAuthAdapter(backend, adapter) {
|
|
|
17
18
|
adapters.set(backend, adapter);
|
|
18
19
|
}
|
|
19
20
|
reactExports.createContext(null);
|
|
21
|
+
const FIREBASE_CACHE_VERSION = 1;
|
|
22
|
+
const FULL_RELOAD_MS = 7 * 24 * 3600 * 1e3;
|
|
23
|
+
function firebaseCacheKey(projectId, uid) {
|
|
24
|
+
return `${projectId}:${uid}`;
|
|
25
|
+
}
|
|
26
|
+
function isUsableCache(state, now) {
|
|
27
|
+
return !!state && state.v === FIREBASE_CACHE_VERSION && typeof state.cursor === "number" && now - state.fullAt < FULL_RELOAD_MS;
|
|
28
|
+
}
|
|
29
|
+
const DB_NAME = "agentcrm-firebase-cache";
|
|
30
|
+
const STORE = "datasets";
|
|
31
|
+
function openDb() {
|
|
32
|
+
return new Promise((resolve, reject) => {
|
|
33
|
+
const req = indexedDB.open(DB_NAME, 1);
|
|
34
|
+
req.onupgradeneeded = () => req.result.createObjectStore(STORE);
|
|
35
|
+
req.onsuccess = () => resolve(req.result);
|
|
36
|
+
req.onerror = () => reject(req.error);
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
function run(mode, fn) {
|
|
40
|
+
return openDb().then(
|
|
41
|
+
(db) => new Promise((resolve, reject) => {
|
|
42
|
+
const tx = db.transaction(STORE, mode);
|
|
43
|
+
const req = fn(tx.objectStore(STORE));
|
|
44
|
+
tx.oncomplete = () => {
|
|
45
|
+
db.close();
|
|
46
|
+
resolve(req.result);
|
|
47
|
+
};
|
|
48
|
+
tx.onerror = () => {
|
|
49
|
+
db.close();
|
|
50
|
+
reject(tx.error);
|
|
51
|
+
};
|
|
52
|
+
})
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
function indexedDbFirebaseCache() {
|
|
56
|
+
if (typeof indexedDB === "undefined") return null;
|
|
57
|
+
return {
|
|
58
|
+
async read(key) {
|
|
59
|
+
try {
|
|
60
|
+
return await run("readonly", (s) => s.get(key)) ?? null;
|
|
61
|
+
} catch {
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
},
|
|
65
|
+
async write(key, state) {
|
|
66
|
+
try {
|
|
67
|
+
await run("readwrite", (s) => s.put(state, key));
|
|
68
|
+
} catch {
|
|
69
|
+
}
|
|
70
|
+
},
|
|
71
|
+
async clear(key) {
|
|
72
|
+
try {
|
|
73
|
+
await run("readwrite", (s) => s.delete(key));
|
|
74
|
+
} catch {
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
}
|
|
20
79
|
const INVITES_COLLECTION = "invites";
|
|
21
80
|
const EXTENSION_CHUNKS_COLLECTION = "extensionChunks";
|
|
22
81
|
const EXTENSION_CHUNK_CHARS = 3e5;
|
|
23
82
|
const INBOX_COLLECTION = "inbox";
|
|
83
|
+
const TRACKED = [...COLLECTIONS, EXTENSION_CHUNKS_COLLECTION];
|
|
84
|
+
setTrackedCollections(TRACKED);
|
|
24
85
|
const ROLES = ["admin", "member", "viewer"];
|
|
25
86
|
class FirebaseAccessError extends Error {
|
|
26
87
|
constructor(reason, message, email) {
|
|
@@ -37,8 +98,8 @@ function toData(doc) {
|
|
|
37
98
|
const { id: _id, ...rest } = plainCopy(doc);
|
|
38
99
|
return rest;
|
|
39
100
|
}
|
|
40
|
-
function fromData(
|
|
41
|
-
return normalizeDoc({ ...
|
|
101
|
+
function fromData(d2) {
|
|
102
|
+
return normalizeDoc({ ...d2.data, id: d2.id });
|
|
42
103
|
}
|
|
43
104
|
function isRole(v) {
|
|
44
105
|
return typeof v === "string" && ROLES.includes(v);
|
|
@@ -103,18 +164,18 @@ function assembleExtensionDocs(docs, chunks) {
|
|
|
103
164
|
const ext = String(c.data.ext ?? "");
|
|
104
165
|
byExt.set(ext, [...byExt.get(ext) ?? [], c]);
|
|
105
166
|
}
|
|
106
|
-
return docs.map((
|
|
107
|
-
const counts =
|
|
108
|
-
if (!counts || typeof counts !== "object") return
|
|
167
|
+
return docs.map((d2) => {
|
|
168
|
+
const counts = d2.data.chunks;
|
|
169
|
+
if (!counts || typeof counts !== "object") return d2;
|
|
109
170
|
const files = {};
|
|
110
|
-
const parts = byExt.get(
|
|
171
|
+
const parts = byExt.get(d2.id) ?? [];
|
|
111
172
|
for (const [file, n] of Object.entries(counts)) {
|
|
112
|
-
const pieces = parts.filter((p) => p.data.file === file).sort((a,
|
|
173
|
+
const pieces = parts.filter((p) => p.data.file === file).sort((a, b2) => Number(a.data.index) - Number(b2.data.index));
|
|
113
174
|
if (pieces.length !== n) continue;
|
|
114
175
|
files[file] = pieces.map((p) => String(p.data.data ?? "")).join("");
|
|
115
176
|
}
|
|
116
|
-
const { chunks: _chunks, ...rest } =
|
|
117
|
-
return { id:
|
|
177
|
+
const { chunks: _chunks, ...rest } = d2.data;
|
|
178
|
+
return { id: d2.id, data: { ...rest, files } };
|
|
118
179
|
});
|
|
119
180
|
}
|
|
120
181
|
function mutationOps(mutations, before) {
|
|
@@ -187,8 +248,8 @@ function createFirebaseStoreWith(opts) {
|
|
|
187
248
|
function buildSnapshot(raw, chunks) {
|
|
188
249
|
const s = emptySnapshot();
|
|
189
250
|
for (const c of COLLECTIONS) {
|
|
190
|
-
const
|
|
191
|
-
for (const
|
|
251
|
+
const docs2 = c === "extensions" ? assembleExtensionDocs(raw.get(c) ?? [], chunks) : raw.get(c) ?? [];
|
|
252
|
+
for (const d2 of docs2) s[c].set(d2.id, fromData(d2));
|
|
192
253
|
}
|
|
193
254
|
rawUsers = new Map(s.users);
|
|
194
255
|
for (const u of [...s.users.values()]) {
|
|
@@ -273,7 +334,7 @@ function createFirebaseStoreWith(opts) {
|
|
|
273
334
|
const ops = mutationOps(mutations, before);
|
|
274
335
|
if (!currentIdentity && mutations.some((m) => m.collection === "users")) await getIdentity();
|
|
275
336
|
if (currentIdentity?.role === "admin") ops.push(...inviteOps(mutations, (id) => before.users.get(id) ?? rawUsers.get(id), after));
|
|
276
|
-
for (const part of
|
|
337
|
+
for (const part of chunkForCommit(ops)) await api.commit(part);
|
|
277
338
|
for (const m of mutations) {
|
|
278
339
|
if (m.collection !== "users") continue;
|
|
279
340
|
const id = m.op === "set" ? m.doc.id : m.id;
|
|
@@ -282,52 +343,143 @@ function createFirebaseStoreWith(opts) {
|
|
|
282
343
|
else rawUsers.delete(id);
|
|
283
344
|
}
|
|
284
345
|
}
|
|
346
|
+
const docs = new Map(TRACKED.map((c) => [c, /* @__PURE__ */ new Map()]));
|
|
347
|
+
let cursor = 0;
|
|
348
|
+
let fullAt = 0;
|
|
349
|
+
let cacheKey = null;
|
|
350
|
+
let loadInfo = null;
|
|
351
|
+
let markLoaded = () => void 0;
|
|
352
|
+
const loadedOnce = new Promise((resolve) => markLoaded = resolve);
|
|
353
|
+
let saveTimer = null;
|
|
354
|
+
const advance = (d2) => {
|
|
355
|
+
if (typeof d2.ts === "number" && d2.ts > cursor) cursor = d2.ts;
|
|
356
|
+
};
|
|
357
|
+
function upsert(c, list) {
|
|
358
|
+
const m = docs.get(c);
|
|
359
|
+
for (const d2 of list) {
|
|
360
|
+
m.set(d2.id, d2);
|
|
361
|
+
advance(d2);
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
function applyTombstones(list) {
|
|
365
|
+
for (const t2 of list) {
|
|
366
|
+
advance(t2);
|
|
367
|
+
const c = t2.data.collection;
|
|
368
|
+
const id = t2.data.id;
|
|
369
|
+
if (typeof c === "string" && typeof id === "string") docs.get(c)?.delete(id);
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
function snapshotFromDocsMap() {
|
|
373
|
+
const raw = new Map(COLLECTIONS.map((c) => [c, [...docs.get(c).values()]]));
|
|
374
|
+
return buildSnapshot(raw, [...docs.get(EXTENSION_CHUNKS_COLLECTION).values()]);
|
|
375
|
+
}
|
|
376
|
+
async function saveCache() {
|
|
377
|
+
if (!opts.cache || !cacheKey) return;
|
|
378
|
+
const collections = Object.fromEntries(TRACKED.map((c) => [c, [...docs.get(c).values()]]));
|
|
379
|
+
await opts.cache.write(cacheKey, { v: FIREBASE_CACHE_VERSION, cursor, fullAt, collections });
|
|
380
|
+
}
|
|
381
|
+
function scheduleSave() {
|
|
382
|
+
if (saveTimer) return;
|
|
383
|
+
saveTimer = setTimeout(() => {
|
|
384
|
+
saveTimer = null;
|
|
385
|
+
void saveCache();
|
|
386
|
+
}, 1e3);
|
|
387
|
+
saveTimer.unref?.();
|
|
388
|
+
}
|
|
389
|
+
async function readSince(c, since) {
|
|
390
|
+
try {
|
|
391
|
+
return await api.listSince(c, since);
|
|
392
|
+
} catch (err) {
|
|
393
|
+
if (isPermissionDenied(err)) return null;
|
|
394
|
+
throw err;
|
|
395
|
+
}
|
|
396
|
+
}
|
|
285
397
|
const core = createStoreCore({
|
|
286
398
|
kind: "firebase",
|
|
287
399
|
async load() {
|
|
288
|
-
const
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
const
|
|
400
|
+
const readsBefore = api.readCount();
|
|
401
|
+
const user = await api.auth.currentUser();
|
|
402
|
+
cacheKey = opts.cache && opts.projectId && user ? firebaseCacheKey(opts.projectId, user.uid) : null;
|
|
403
|
+
const cached = cacheKey ? await opts.cache.read(cacheKey) : null;
|
|
404
|
+
let mode = "full";
|
|
405
|
+
if (isUsableCache(cached, now())) {
|
|
406
|
+
mode = "incremental";
|
|
407
|
+
cursor = cached.cursor;
|
|
408
|
+
fullAt = cached.fullAt;
|
|
409
|
+
for (const c of TRACKED) docs.set(c, new Map((cached.collections[c] ?? []).map((d2) => [d2.id, d2])));
|
|
410
|
+
const since = cursor;
|
|
411
|
+
const changed = await Promise.all(TRACKED.map(async (c) => [c, await readSince(c, since)]));
|
|
412
|
+
for (const [c, list] of changed) {
|
|
413
|
+
if (list === null) docs.set(c, /* @__PURE__ */ new Map());
|
|
414
|
+
else upsert(c, list);
|
|
415
|
+
}
|
|
416
|
+
applyTombstones(await readSince(DELETIONS_COLLECTION, since) ?? []);
|
|
417
|
+
} else {
|
|
418
|
+
cursor = 0;
|
|
419
|
+
const all = await Promise.all(TRACKED.map(async (c) => [c, await readCollection(c)]));
|
|
420
|
+
for (const [c, list] of all) {
|
|
421
|
+
docs.set(c, /* @__PURE__ */ new Map());
|
|
422
|
+
upsert(c, list);
|
|
423
|
+
}
|
|
424
|
+
fullAt = now();
|
|
425
|
+
}
|
|
426
|
+
loadInfo = { mode, reads: api.readCount() - readsBefore };
|
|
427
|
+
globalThis.__AGENTCRM_FIRESTORE__ = { ...loadInfo, cursor, totalReads: api.readCount };
|
|
428
|
+
await saveCache();
|
|
429
|
+
markLoaded();
|
|
430
|
+
const snap = snapshotFromDocsMap();
|
|
292
431
|
scheduleReconcile();
|
|
293
432
|
return snap;
|
|
294
433
|
},
|
|
295
434
|
persist: (mutations, before, after) => persist(mutations, before, after),
|
|
435
|
+
// Live updates: one listener per collection on documents written after
|
|
436
|
+
// the load's cursor — only changes are read (and billed).
|
|
296
437
|
watch(onRemote) {
|
|
297
|
-
|
|
298
|
-
let
|
|
438
|
+
let offs = [];
|
|
439
|
+
let stopped = false;
|
|
299
440
|
const emit = () => {
|
|
300
|
-
|
|
301
|
-
onRemote(buildSnapshot(raw, chunks));
|
|
441
|
+
onRemote(snapshotFromDocsMap());
|
|
302
442
|
scheduleReconcile();
|
|
443
|
+
scheduleSave();
|
|
444
|
+
};
|
|
445
|
+
void loadedOnce.then(() => {
|
|
446
|
+
if (stopped) return;
|
|
447
|
+
const since = cursor;
|
|
448
|
+
offs = TRACKED.map(
|
|
449
|
+
(c) => api.watchSince(
|
|
450
|
+
c,
|
|
451
|
+
since,
|
|
452
|
+
(ch) => {
|
|
453
|
+
upsert(c, ch.upserts);
|
|
454
|
+
for (const id of ch.removed) docs.get(c).delete(id);
|
|
455
|
+
emit();
|
|
456
|
+
},
|
|
457
|
+
(err) => {
|
|
458
|
+
if (isPermissionDenied(err)) {
|
|
459
|
+
docs.set(c, /* @__PURE__ */ new Map());
|
|
460
|
+
emit();
|
|
461
|
+
} else console.warn(`[firebase] live updates of ${c} stopped:`, err);
|
|
462
|
+
}
|
|
463
|
+
)
|
|
464
|
+
);
|
|
465
|
+
offs.push(
|
|
466
|
+
api.watchSince(
|
|
467
|
+
DELETIONS_COLLECTION,
|
|
468
|
+
since,
|
|
469
|
+
(ch) => {
|
|
470
|
+
applyTombstones(ch.upserts);
|
|
471
|
+
emit();
|
|
472
|
+
},
|
|
473
|
+
(err) => {
|
|
474
|
+
if (!isPermissionDenied(err)) console.warn("[firebase] live deletions stopped:", err);
|
|
475
|
+
}
|
|
476
|
+
)
|
|
477
|
+
);
|
|
478
|
+
});
|
|
479
|
+
return () => {
|
|
480
|
+
stopped = true;
|
|
481
|
+
offs.forEach((off) => off());
|
|
303
482
|
};
|
|
304
|
-
const offs = COLLECTIONS.map(
|
|
305
|
-
(c) => api.watchCollection(
|
|
306
|
-
c,
|
|
307
|
-
(docs) => {
|
|
308
|
-
raw.set(c, docs);
|
|
309
|
-
emit();
|
|
310
|
-
},
|
|
311
|
-
(err) => {
|
|
312
|
-
if (isPermissionDenied(err)) raw.set(c, []);
|
|
313
|
-
else console.warn(`[firebase] live updates of ${c} stopped:`, err);
|
|
314
|
-
}
|
|
315
|
-
)
|
|
316
|
-
);
|
|
317
|
-
offs.push(
|
|
318
|
-
api.watchCollection(
|
|
319
|
-
EXTENSION_CHUNKS_COLLECTION,
|
|
320
|
-
(docs) => {
|
|
321
|
-
chunks = docs;
|
|
322
|
-
emit();
|
|
323
|
-
},
|
|
324
|
-
(err) => {
|
|
325
|
-
if (isPermissionDenied(err)) chunks = [];
|
|
326
|
-
else console.warn("[firebase] live updates of extension chunks stopped:", err);
|
|
327
|
-
}
|
|
328
|
-
)
|
|
329
|
-
);
|
|
330
|
-
return () => offs.forEach((off) => off());
|
|
331
483
|
},
|
|
332
484
|
identity: getIdentity,
|
|
333
485
|
close() {
|
|
@@ -383,9 +535,23 @@ function createFirebaseStoreWith(opts) {
|
|
|
383
535
|
console.warn("[firebase] could not link invited users:", err);
|
|
384
536
|
}
|
|
385
537
|
}
|
|
538
|
+
const closeCore = core.close?.bind(core);
|
|
539
|
+
core.close = () => {
|
|
540
|
+
if (saveTimer) {
|
|
541
|
+
clearTimeout(saveTimer);
|
|
542
|
+
saveTimer = null;
|
|
543
|
+
void saveCache();
|
|
544
|
+
}
|
|
545
|
+
closeCore?.();
|
|
546
|
+
};
|
|
386
547
|
withFileStore(core, firebaseFileStore(api));
|
|
548
|
+
activeCacheClear = async () => {
|
|
549
|
+
if (opts.cache && cacheKey) await opts.cache.clear(cacheKey);
|
|
550
|
+
};
|
|
387
551
|
return Object.assign(core, {
|
|
388
552
|
api,
|
|
553
|
+
readCount: () => api.readCount(),
|
|
554
|
+
lastLoad: () => loadInfo,
|
|
389
555
|
accessError: () => lastAccessError,
|
|
390
556
|
resetSession() {
|
|
391
557
|
identityPromise = null;
|
|
@@ -399,15 +565,17 @@ function firebaseApiFor(config) {
|
|
|
399
565
|
const key = config.firebase.projectId ?? "default";
|
|
400
566
|
let api = apis.get(key);
|
|
401
567
|
if (!api) {
|
|
402
|
-
api = import("./sdk-
|
|
568
|
+
api = import("./sdk-NYUHv7WG.mjs").then((m) => m.createSdkFirebaseApi(config.firebase));
|
|
403
569
|
apis.set(key, api);
|
|
404
570
|
api.catch(() => apis.delete(key));
|
|
405
571
|
}
|
|
406
572
|
return api;
|
|
407
573
|
}
|
|
408
574
|
let activeApi = null;
|
|
575
|
+
let activeCacheClear = null;
|
|
409
576
|
registerAuthAdapter("firebase", {
|
|
410
577
|
async signOut() {
|
|
578
|
+
await activeCacheClear?.();
|
|
411
579
|
await activeApi?.auth.signOut();
|
|
412
580
|
}
|
|
413
581
|
});
|
|
@@ -451,21 +619,26 @@ async function createFirebaseStore(config, opts = {}) {
|
|
|
451
619
|
const api = await firebaseApiFor(cfg);
|
|
452
620
|
activeApi = api;
|
|
453
621
|
await signInWithSecrets(api, opts.secrets);
|
|
454
|
-
|
|
622
|
+
const cache = opts.firebaseCache === void 0 ? indexedDbFirebaseCache() : opts.firebaseCache;
|
|
623
|
+
return createFirebaseStoreWith({ api, cache, projectId: cfg.firebase.projectId });
|
|
455
624
|
}
|
|
456
625
|
export {
|
|
457
626
|
DELETE_FIELD,
|
|
627
|
+
DELETIONS_COLLECTION,
|
|
458
628
|
EXTENSION_CHUNKS_COLLECTION,
|
|
459
629
|
EXTENSION_CHUNK_CHARS,
|
|
460
|
-
FIRESTORE_BATCH_LIMIT,
|
|
630
|
+
F as FIRESTORE_BATCH_LIMIT,
|
|
461
631
|
FirebaseAccessError,
|
|
462
632
|
INBOX_COLLECTION,
|
|
463
633
|
INVITES_COLLECTION,
|
|
634
|
+
S as SERVER_TIMESTAMP,
|
|
464
635
|
STORAGE_FILES_DIR,
|
|
465
636
|
STORAGE_NOT_ENABLED,
|
|
637
|
+
T as TS_FIELD,
|
|
466
638
|
assembleExtensionDocs,
|
|
467
639
|
checkFirebaseAccess,
|
|
468
|
-
chunk,
|
|
640
|
+
b as chunk,
|
|
641
|
+
chunkForCommit,
|
|
469
642
|
createFirebaseStore,
|
|
470
643
|
createFirebaseStoreWith,
|
|
471
644
|
emailKey,
|
|
@@ -476,5 +649,9 @@ export {
|
|
|
476
649
|
isFirebaseStore,
|
|
477
650
|
isPermissionDenied,
|
|
478
651
|
mutationOps,
|
|
479
|
-
|
|
652
|
+
setTrackedCollections,
|
|
653
|
+
signInWithSecrets,
|
|
654
|
+
d as splitTs,
|
|
655
|
+
t as tombstoneId,
|
|
656
|
+
w as withChangeTracking
|
|
480
657
|
};
|
|
@@ -0,0 +1,39 @@
|
|
|
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 { existsSync, rmSync } from "node:fs";
|
|
8
|
+
import { w as writeJsonFile, h as readJsonFile, i as homePath } from "../agentcrm.mjs";
|
|
9
|
+
import "node:util";
|
|
10
|
+
import "node:path";
|
|
11
|
+
import "node:url";
|
|
12
|
+
import "node:crypto";
|
|
13
|
+
import "node:os";
|
|
14
|
+
import "node:child_process";
|
|
15
|
+
import "node:http";
|
|
16
|
+
function cacheFile(key) {
|
|
17
|
+
return homePath("cache", `firebase-${key.replace(/[^a-z0-9._-]+/gi, "_")}.json`);
|
|
18
|
+
}
|
|
19
|
+
function fileFirebaseCache() {
|
|
20
|
+
return {
|
|
21
|
+
async read(key) {
|
|
22
|
+
try {
|
|
23
|
+
return readJsonFile(cacheFile(key), null);
|
|
24
|
+
} catch {
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
async write(key, state) {
|
|
29
|
+
writeJsonFile(cacheFile(key), state, 384);
|
|
30
|
+
},
|
|
31
|
+
async clear(key) {
|
|
32
|
+
const f = cacheFile(key);
|
|
33
|
+
if (existsSync(f)) rmSync(f, { force: true });
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
export {
|
|
38
|
+
fileFirebaseCache
|
|
39
|
+
};
|
|
@@ -124,7 +124,7 @@ function buildFirestoreRules(input) {
|
|
|
124
124
|
" // Nobody changes their own role / email / disabled flag.",
|
|
125
125
|
" allow update: if (isAdmin() && validRole(request.resource.data.role))",
|
|
126
126
|
" || (isActive() && request.auth.uid == uid",
|
|
127
|
-
" && request.resource.data.diff(resource.data).affectedKeys().hasOnly(['name', 'avatarUrl', 'updatedAt']));",
|
|
127
|
+
" && request.resource.data.diff(resource.data).affectedKeys().hasOnly(['name', 'avatarUrl', 'updatedAt', '_ts']));",
|
|
128
128
|
" allow delete: if isAdmin();",
|
|
129
129
|
" }",
|
|
130
130
|
"",
|
|
@@ -152,7 +152,7 @@ function buildFirestoreRules(input) {
|
|
|
152
152
|
" allow read: if isActive();",
|
|
153
153
|
" allow create, delete: if isAdmin();",
|
|
154
154
|
" allow update: if isAdmin()",
|
|
155
|
-
" || (isMember() && request.resource.data.diff(resource.data).affectedKeys().hasOnly(['lastRunAt', 'updatedAt']));",
|
|
155
|
+
" || (isMember() && request.resource.data.diff(resource.data).affectedKeys().hasOnly(['lastRunAt', 'updatedAt', '_ts']));",
|
|
156
156
|
" }",
|
|
157
157
|
"",
|
|
158
158
|
" match /automationRuns/{id} {",
|
|
@@ -180,6 +180,17 @@ function buildFirestoreRules(input) {
|
|
|
180
180
|
" allow write: if isAdmin();",
|
|
181
181
|
" }",
|
|
182
182
|
"",
|
|
183
|
+
" // Tombstones of deleted documents, so cached copies drop them on the",
|
|
184
|
+
" // next incremental read (src/data/firebase/api.ts, withChangeTracking).",
|
|
185
|
+
" // Written in the same batch as the delete, which the rules above judge.",
|
|
186
|
+
" match /deletions/{id} {",
|
|
187
|
+
" allow read: if isActive();",
|
|
188
|
+
" allow create, update: if isMember()",
|
|
189
|
+
" && request.resource.data.keys().hasOnly(['collection', 'id', '_ts'])",
|
|
190
|
+
" && request.resource.data._ts == request.time;",
|
|
191
|
+
" allow delete: if isAdmin();",
|
|
192
|
+
" }",
|
|
193
|
+
"",
|
|
183
194
|
" // ─── inbox/{id}: public lead-form submissions ──────────────────────",
|
|
184
195
|
" function validSubmission(d) {",
|
|
185
196
|
" return d.keys().hasAll(['formId', 'data', 'createdAt'])",
|
|
@@ -213,7 +224,7 @@ function buildFirestoreRules(input) {
|
|
|
213
224
|
);
|
|
214
225
|
return lines.join("\n");
|
|
215
226
|
}
|
|
216
|
-
[...MEMBER_WRITE, "views", "automations", "automationRuns", "files", ...ADMIN_WRITE, "users"];
|
|
227
|
+
[...MEMBER_WRITE, "views", "automations", "automationRuns", "files", ...ADMIN_WRITE, "users", "deletions"];
|
|
217
228
|
export {
|
|
218
229
|
INBOX_LIMITS,
|
|
219
230
|
buildFirestoreRules,
|