@progressive-development/pd-provider-firebase-functions 1.2.0 → 1.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.
package/dist/index.d.ts CHANGED
@@ -5,6 +5,8 @@ export { requireAuth, requireClaims, requireOwnership } from './auth.js';
5
5
  export { success, created, handleError } from './response.js';
6
6
  export { getDb } from './firestore.js';
7
7
  export { getNextCounter } from './counter.js';
8
+ export type { VisitInput, VisitDevice, VisitResult, CountVisitOptions, } from './visits.js';
9
+ export { countVisit } from './visits.js';
8
10
  export type { TrashItem } from './trash.js';
9
11
  export { moveToTrash, restoreFromTrash, permanentDelete, listTrash, getTrashItem, } from './trash.js';
10
12
  export type { AuthTrashOptions } from './auth-trash.js';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,YAAY,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAC3D,OAAO,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAGxC,OAAO,EACL,QAAQ,EACR,eAAe,EACf,iBAAiB,EACjB,cAAc,EACd,aAAa,EACb,aAAa,EACb,qBAAqB,EACrB,qBAAqB,GACtB,MAAM,aAAa,CAAC;AAGrB,OAAO,EAAE,WAAW,EAAE,aAAa,EAAE,gBAAgB,EAAE,MAAM,WAAW,CAAC;AAGzE,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAG9D,OAAO,EAAE,KAAK,EAAE,MAAM,gBAAgB,CAAC;AAGvC,OAAO,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAG9C,YAAY,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAC5C,OAAO,EACL,WAAW,EACX,gBAAgB,EAChB,eAAe,EACf,SAAS,EACT,YAAY,GACb,MAAM,YAAY,CAAC;AAGpB,YAAY,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;AACxD,OAAO,EACL,mBAAmB,EACnB,wBAAwB,EACxB,uBAAuB,GACxB,MAAM,iBAAiB,CAAC;AAGzB,YAAY,EACV,WAAW,EACX,YAAY,EACZ,qBAAqB,GACtB,MAAM,cAAc,CAAC;AACtB,OAAO,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAG9C,YAAY,EAAE,YAAY,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AAClE,OAAO,EACL,WAAW,EACX,eAAe,EACf,aAAa,EACb,WAAW,GACZ,MAAM,cAAc,CAAC;AAGtB,YAAY,EACV,UAAU,EACV,YAAY,EACZ,WAAW,EACX,cAAc,EACd,UAAU,GACX,MAAM,WAAW,CAAC;AACnB,OAAO,EAAE,cAAc,EAAE,WAAW,EAAE,eAAe,EAAE,MAAM,WAAW,CAAC;AAGzE,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAGrC,YAAY,EACV,OAAO,EACP,mBAAmB,EACnB,aAAa,GACd,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EACL,SAAS,EACT,iBAAiB,EACjB,mBAAmB,GACpB,MAAM,qBAAqB,CAAC;AAG7B,YAAY,EAAE,qBAAqB,EAAE,MAAM,uBAAuB,CAAC;AACnE,OAAO,EAAE,kBAAkB,EAAE,MAAM,uBAAuB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,YAAY,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAC3D,OAAO,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAGxC,OAAO,EACL,QAAQ,EACR,eAAe,EACf,iBAAiB,EACjB,cAAc,EACd,aAAa,EACb,aAAa,EACb,qBAAqB,EACrB,qBAAqB,GACtB,MAAM,aAAa,CAAC;AAGrB,OAAO,EAAE,WAAW,EAAE,aAAa,EAAE,gBAAgB,EAAE,MAAM,WAAW,CAAC;AAGzE,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAG9D,OAAO,EAAE,KAAK,EAAE,MAAM,gBAAgB,CAAC;AAGvC,OAAO,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAG9C,YAAY,EACV,UAAU,EACV,WAAW,EACX,WAAW,EACX,iBAAiB,GAClB,MAAM,aAAa,CAAC;AACrB,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAGzC,YAAY,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAC5C,OAAO,EACL,WAAW,EACX,gBAAgB,EAChB,eAAe,EACf,SAAS,EACT,YAAY,GACb,MAAM,YAAY,CAAC;AAGpB,YAAY,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;AACxD,OAAO,EACL,mBAAmB,EACnB,wBAAwB,EACxB,uBAAuB,GACxB,MAAM,iBAAiB,CAAC;AAGzB,YAAY,EACV,WAAW,EACX,YAAY,EACZ,qBAAqB,GACtB,MAAM,cAAc,CAAC;AACtB,OAAO,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAG9C,YAAY,EAAE,YAAY,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AAClE,OAAO,EACL,WAAW,EACX,eAAe,EACf,aAAa,EACb,WAAW,GACZ,MAAM,cAAc,CAAC;AAGtB,YAAY,EACV,UAAU,EACV,YAAY,EACZ,WAAW,EACX,cAAc,EACd,UAAU,GACX,MAAM,WAAW,CAAC;AACnB,OAAO,EAAE,cAAc,EAAE,WAAW,EAAE,eAAe,EAAE,MAAM,WAAW,CAAC;AAGzE,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAGrC,YAAY,EACV,OAAO,EACP,mBAAmB,EACnB,aAAa,GACd,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EACL,SAAS,EACT,iBAAiB,EACjB,mBAAmB,GACpB,MAAM,qBAAqB,CAAC;AAG7B,YAAY,EAAE,qBAAqB,EAAE,MAAM,uBAAuB,CAAC;AACnE,OAAO,EAAE,kBAAkB,EAAE,MAAM,uBAAuB,CAAC"}
package/dist/index.js CHANGED
@@ -4,6 +4,7 @@ export { requireAuth, requireClaims, requireOwnership } from './auth.js';
4
4
  export { created, handleError, success } from './response.js';
5
5
  export { getDb } from './firestore.js';
6
6
  export { getNextCounter } from './counter.js';
7
+ export { countVisit } from './visits.js';
7
8
  export { getTrashItem, listTrash, moveToTrash, permanentDelete, restoreFromTrash } from './trash.js';
8
9
  export { moveToTrashWithAuth, permanentDeleteWithAuth, restoreFromTrashWithAuth } from './auth-trash.js';
9
10
  export { computeChanges } from './changes.js';
@@ -0,0 +1,135 @@
1
+ import { CallableRequest } from 'firebase-functions/v2/https';
2
+ /**
3
+ * Reach measurement — one row per visitor and day.
4
+ *
5
+ * WHY ROWS AND NOT COUNTERS: a counter answers only the question you asked
6
+ * when you built it. Rows can still be grouped, filtered and exported later,
7
+ * and every value the client sends stays a field VALUE — never a field name,
8
+ * where a dot would silently create a nested field.
9
+ *
10
+ * HOW DEDUPLICATION WORKS: the document id is `<day>_<visitorKey>` and the row
11
+ * is written with `create()`. A second call on the same day hits ALREADY_EXISTS
12
+ * and is dropped. No read, no counter, no transaction — the storage IS the
13
+ * deduplication.
14
+ *
15
+ * THE DAY IS IN THE ID, NOT IN THE KEY. That is what makes returning visitors
16
+ * visible: same suffix on two days is the same visitor. How long that link
17
+ * survives is one option (`rotate`): "day" makes rows unlinkable across days,
18
+ * "month" lets you recognise a returning visitor within a month. Default is
19
+ * the private one; an app that wants recognition asks for it.
20
+ *
21
+ * WHAT THE KEY IS WORTH: it is derived from IP and user agent. A browser
22
+ * update changes the user agent, mobile networks rotate IPs, a household
23
+ * shares one — so a recognised visitor is real, but an unrecognised one may
24
+ * still be the same person. It is a lower bound, not a head count. With
25
+ * `rotate: "month"` the key is a pseudonymous identifier: two rows carrying it
26
+ * are the same person, which is the point and the price.
27
+ *
28
+ * `secret` is optional. Without it the key is a plain hash of values an
29
+ * attacker could guess, so anyone WHO ALREADY HAS THE ROWS could test whether
30
+ * a given IP and browser visited. Rows are readable through the Admin SDK
31
+ * only; adding a constant closes even that, without changing this signature.
32
+ */
33
+ export type VisitDevice = "phone" | "tablet" | "desktop";
34
+ /**
35
+ * What the client sends — coarse on purpose; every value is a field, not a key.
36
+ *
37
+ * Passed in SEPARATELY from the request, because an app usually wraps its calls
38
+ * in an envelope of its own (`{ object, type, details }`) and the payload then
39
+ * sits somewhere inside `request.data`. The helper reads headers from the
40
+ * request and takes the payload from the caller, so both shapes fit.
41
+ */
42
+ export interface VisitInput {
43
+ /** Coarse device class, e.g. from a pointer/width media query. */
44
+ device?: VisitDevice;
45
+ /** Opened as an installed app (display-mode: standalone). */
46
+ standalone?: boolean;
47
+ /** Host of the referrer, reduced by the client. Never a full URL. */
48
+ referrer?: string;
49
+ }
50
+ export interface CountVisitOptions {
51
+ /**
52
+ * Count only calls from these origins — keeps dev servers out of the numbers.
53
+ * A site is usually reachable under more than one (hosting plus own domain).
54
+ */
55
+ origin?: string | string[];
56
+ /** IANA zone the day boundary follows. Functions run in UTC; default "UTC". */
57
+ timeZone?: string;
58
+ /** How long a visitor key stays the same. Default "day". */
59
+ rotate?: "day" | "month";
60
+ /** Optional salt; see the note above. */
61
+ secret?: string;
62
+ /**
63
+ * Days after which a row may be removed — written as `expires`.
64
+ *
65
+ * The FIELD is all this does. Deleting is a Firestore TTL policy on that
66
+ * field, switched on once per collection outside the code; without it the
67
+ * timestamp is documentation and nothing else. Say a retention period in a
68
+ * privacy notice only once the policy is on.
69
+ */
70
+ retentionDays?: number;
71
+ /** Default "visits". */
72
+ collection?: string;
73
+ /** Named Firestore database. */
74
+ databaseId?: string;
75
+ }
76
+ export interface VisitResult {
77
+ counted: boolean;
78
+ day: string;
79
+ }
80
+ /**
81
+ * The day as YYYY-MM-DD in the given zone.
82
+ *
83
+ * "sv-SE" is the shortest way to an ISO-shaped date: `toISOString()` would be
84
+ * UTC, and building it by hand from parts is where off-by-one bugs live.
85
+ */
86
+ export declare function visitDay(now: Date, timeZone: string): string;
87
+ /** The stretch of time a visitor key stays the same. */
88
+ export declare function visitPeriod(day: string, rotate: "day" | "month"): string;
89
+ /** The technical key — short enough to read, long enough not to collide. */
90
+ export declare function visitorKey(period: string, ip: string, userAgent: string, secret?: string): string;
91
+ /** Unknown values are dropped, never guessed and never stored raw. */
92
+ export declare function cleanDevice(value: unknown): VisitDevice | undefined;
93
+ /** A host, nothing else — no path, no query, no room for someone else's data. */
94
+ export declare function cleanReferrer(value: unknown): string | undefined;
95
+ export declare function isBot(userAgent: string): boolean;
96
+ export interface BrowserInfo {
97
+ /** Family as the user would name it, or "other". */
98
+ browser: string;
99
+ /** MAJOR version only. The minor and patch parts identify, they do not inform. */
100
+ version?: string;
101
+ }
102
+ /**
103
+ * Browser family and major version.
104
+ *
105
+ * ORDER IS THE WHOLE TRICK, because these strings quote each other: Edge says
106
+ * Edg AND Chrome AND Safari, Chrome says Chrome AND Safari. So the most
107
+ * specific claim wins, and Safari is asked last. On iOS every engine is
108
+ * Safari's, but the browsers name themselves (CriOS, FxiOS) — reported as what
109
+ * the user sees, since that is what the question is about.
110
+ *
111
+ * Unclassified traffic becomes "other" rather than nothing: an unreadable
112
+ * agent is a fact worth seeing, missing data is a different fact.
113
+ */
114
+ export declare function browserOf(userAgent: string): BrowserInfo;
115
+ /**
116
+ * Operating system family, never a version.
117
+ *
118
+ * Android carries "Linux" in its agent and an iPad in desktop mode calls
119
+ * itself a Macintosh — so the order matters here too, and the iPad stays
120
+ * wrong. That is a limit of the source, not something to guess around.
121
+ */
122
+ export declare function osOf(userAgent: string): string;
123
+ /** No list means no restriction; an empty match means this call does not count. */
124
+ export declare function originAllowed(origin: unknown, allowed?: string | string[]): boolean;
125
+ /** First entry of x-forwarded-for; the rest are proxies. */
126
+ export declare function clientIp(headers: Record<string, unknown>): string | undefined;
127
+ /**
128
+ * Count one visit — or recognise that this visitor was already counted today.
129
+ *
130
+ * Never throws for the ordinary "does not count" cases (wrong origin, bot, no
131
+ * address, already seen): the caller is a fire-and-forget call from a page
132
+ * that must not care.
133
+ */
134
+ export declare function countVisit(request: CallableRequest, visit?: VisitInput, options?: CountVisitOptions): Promise<VisitResult>;
135
+ //# sourceMappingURL=visits.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"visits.d.ts","sourceRoot":"","sources":["../src/visits.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,eAAe,EAAE,MAAM,6BAA6B,CAAC;AAI9D;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AAEH,MAAM,MAAM,WAAW,GAAG,OAAO,GAAG,QAAQ,GAAG,SAAS,CAAC;AAEzD;;;;;;;GAOG;AACH,MAAM,WAAW,UAAU;IACzB,kEAAkE;IAClE,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB,6DAA6D;IAC7D,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,qEAAqE;IACrE,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,iBAAiB;IAChC;;;OAGG;IACH,MAAM,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC;IAC3B,+EAA+E;IAC/E,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,4DAA4D;IAC5D,MAAM,CAAC,EAAE,KAAK,GAAG,OAAO,CAAC;IACzB,yCAAyC;IACzC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB;;;;;;;OAOG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,wBAAwB;IACxB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,gCAAgC;IAChC,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,WAAW;IAC1B,OAAO,EAAE,OAAO,CAAC;IACjB,GAAG,EAAE,MAAM,CAAC;CACb;AAWD;;;;;GAKG;AACH,wBAAgB,QAAQ,CAAC,GAAG,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAE5D;AAED,wDAAwD;AACxD,wBAAgB,WAAW,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,KAAK,GAAG,OAAO,GAAG,MAAM,CAExE;AAED,4EAA4E;AAC5E,wBAAgB,UAAU,CACxB,MAAM,EAAE,MAAM,EACd,EAAE,EAAE,MAAM,EACV,SAAS,EAAE,MAAM,EACjB,MAAM,SAAK,GACV,MAAM,CAKR;AAED,sEAAsE;AACtE,wBAAgB,WAAW,CAAC,KAAK,EAAE,OAAO,GAAG,WAAW,GAAG,SAAS,CAInE;AAED,iFAAiF;AACjF,wBAAgB,aAAa,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,GAAG,SAAS,CAIhE;AAED,wBAAgB,KAAK,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAEhD;AAED,MAAM,WAAW,WAAW;IAC1B,oDAAoD;IACpD,OAAO,EAAE,MAAM,CAAC;IAChB,kFAAkF;IAClF,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,SAAS,CAAC,SAAS,EAAE,MAAM,GAAG,WAAW,CAgBxD;AAED;;;;;;GAMG;AACH,wBAAgB,IAAI,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAQ9C;AAED,mFAAmF;AACnF,wBAAgB,aAAa,CAC3B,MAAM,EAAE,OAAO,EACf,OAAO,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,GAC1B,OAAO,CAIT;AAED,4DAA4D;AAC5D,wBAAgB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,GAAG,SAAS,CAM7E;AAED;;;;;;GAMG;AACH,wBAAsB,UAAU,CAC9B,OAAO,EAAE,eAAe,EACxB,KAAK,GAAE,UAAe,EACtB,OAAO,GAAE,iBAAsB,GAC9B,OAAO,CAAC,WAAW,CAAC,CAqDtB"}
package/dist/visits.js ADDED
@@ -0,0 +1,105 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { Timestamp, FieldValue } from 'firebase-admin/firestore';
3
+ import { getDb } from './firestore.js';
4
+
5
+ const DEVICES = ["phone", "tablet", "desktop"];
6
+ const BOTS = /bot|crawl|spider|slurp|preview|monitor|headless|lighthouse|curl|wget/i;
7
+ const ALREADY_EXISTS = 6;
8
+ function visitDay(now, timeZone) {
9
+ return new Intl.DateTimeFormat("sv-SE", { timeZone }).format(now);
10
+ }
11
+ function visitPeriod(day, rotate) {
12
+ return rotate === "month" ? day.slice(0, 7) : day;
13
+ }
14
+ function visitorKey(period, ip, userAgent, secret = "") {
15
+ return createHash("sha256").update(`${secret}|${period}|${ip}|${userAgent}`).digest("hex").slice(0, 16);
16
+ }
17
+ function cleanDevice(value) {
18
+ return typeof value === "string" && DEVICES.includes(value) ? value : void 0;
19
+ }
20
+ function cleanReferrer(value) {
21
+ if (typeof value !== "string") return void 0;
22
+ const host = value.trim().toLowerCase();
23
+ return /^[a-z0-9.\-:]{1,60}$/.test(host) ? host : void 0;
24
+ }
25
+ function isBot(userAgent) {
26
+ return BOTS.test(userAgent);
27
+ }
28
+ function browserOf(userAgent) {
29
+ const rules = [
30
+ ["Edge", /Edg(?:iOS|A)?\/(\d+)/],
31
+ ["Opera", /OPR\/(\d+)/],
32
+ ["Samsung Internet", /SamsungBrowser\/(\d+)/],
33
+ ["Chrome", /CriOS\/(\d+)/],
34
+ ["Firefox", /FxiOS\/(\d+)/],
35
+ ["Firefox", /Firefox\/(\d+)/],
36
+ ["Chrome", /Chrome\/(\d+)/],
37
+ ["Safari", /Version\/(\d+)[\d.]*\s+(?:Mobile\/\S+\s+)?Safari/]
38
+ ];
39
+ for (const [browser, pattern] of rules) {
40
+ const hit = pattern.exec(userAgent);
41
+ if (hit) return { browser, version: hit[1] };
42
+ }
43
+ return { browser: "other" };
44
+ }
45
+ function osOf(userAgent) {
46
+ if (/iPhone|iPad|iPod/.test(userAgent)) return "iOS";
47
+ if (/Android/.test(userAgent)) return "Android";
48
+ if (/CrOS/.test(userAgent)) return "ChromeOS";
49
+ if (/Windows/.test(userAgent)) return "Windows";
50
+ if (/Macintosh|Mac OS X/.test(userAgent)) return "macOS";
51
+ if (/Linux/.test(userAgent)) return "Linux";
52
+ return "other";
53
+ }
54
+ function originAllowed(origin, allowed) {
55
+ if (!allowed) return true;
56
+ const list = Array.isArray(allowed) ? allowed : [allowed];
57
+ return typeof origin === "string" && list.includes(origin);
58
+ }
59
+ function clientIp(headers) {
60
+ const raw = headers["x-forwarded-for"];
61
+ const value = Array.isArray(raw) ? raw[0] : raw;
62
+ if (typeof value !== "string") return void 0;
63
+ const first = value.split(",")[0]?.trim();
64
+ return first || void 0;
65
+ }
66
+ async function countVisit(request, visit = {}, options = {}) {
67
+ const headers = request.rawRequest?.headers ?? {};
68
+ const day = visitDay(/* @__PURE__ */ new Date(), options.timeZone ?? "UTC");
69
+ const skip = { counted: false, day };
70
+ if (!originAllowed(headers.origin, options.origin)) return skip;
71
+ const userAgent = typeof headers["user-agent"] === "string" ? headers["user-agent"] : "";
72
+ if (!userAgent || isBot(userAgent)) return skip;
73
+ const ip = clientIp(headers);
74
+ if (!ip) return skip;
75
+ const { browser, version } = browserOf(userAgent);
76
+ const period = visitPeriod(day, options.rotate ?? "day");
77
+ const visitor = visitorKey(period, ip, userAgent, options.secret);
78
+ try {
79
+ await getDb(options.databaseId).collection(options.collection ?? "visits").doc(`${day}_${visitor}`).create({
80
+ day,
81
+ visitor,
82
+ at: FieldValue.serverTimestamp(),
83
+ // Missing is missing: getDb ignores undefined, so an absent value
84
+ // leaves no empty field behind.
85
+ device: cleanDevice(visit.device),
86
+ standalone: visit.standalone === true,
87
+ referrer: cleanReferrer(visit.referrer),
88
+ // Read from the agent the server already has — the client sends
89
+ // nothing for these. Family and MAJOR version only; the full string
90
+ // stays where it cannot be read back, in the key.
91
+ browser,
92
+ browserVersion: version,
93
+ os: osOf(userAgent),
94
+ expires: options.retentionDays ? Timestamp.fromMillis(
95
+ Date.now() + options.retentionDays * 864e5
96
+ ) : void 0
97
+ });
98
+ return { counted: true, day };
99
+ } catch (e) {
100
+ if (e?.code === ALREADY_EXISTS) return skip;
101
+ throw e;
102
+ }
103
+ }
104
+
105
+ export { browserOf, cleanDevice, cleanReferrer, clientIp, countVisit, isBot, originAllowed, osOf, visitDay, visitPeriod, visitorKey };
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=visits.spec.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"visits.spec.d.ts","sourceRoot":"","sources":["../src/visits.spec.ts"],"names":[],"mappings":""}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@progressive-development/pd-provider-firebase-functions",
3
- "version": "1.2.0",
3
+ "version": "1.3.0",
4
4
  "description": "Firebase Functions v2 utilities for pd-spa-helper backend",
5
5
  "author": "PD Progressive Development",
6
6
  "license": "SEE LICENSE IN LICENSE",