@hasna/events 0.1.15 → 0.1.17
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 -5
- package/dist/cli/index.js +554 -53
- package/dist/commander.js +481 -20
- package/dist/durable.js +524 -34
- package/dist/index.js +486 -20
- package/dist/ssrf.js +222 -0
- package/dist/storage.js +120 -15
- package/dist/transports.js +355 -1
- package/fixtures/hasna.app_event.v1.json +2 -2
- package/package.json +7 -2
- package/types/app-home.d.ts +33 -0
- package/types/durable.d.ts +2 -0
- package/types/index.d.ts +1 -0
- package/types/ssrf.d.ts +59 -0
- package/types/storage.d.ts +2 -2
- package/types/transports.d.ts +23 -0
package/dist/ssrf.js
ADDED
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
// src/ssrf.ts
|
|
3
|
+
import { lookup as dnsLookup } from "dns/promises";
|
|
4
|
+
import { isIP } from "net";
|
|
5
|
+
var DEFAULT_MAX_REDIRECTS = 5;
|
|
6
|
+
var IPV4_PRIVATE_RANGES = [
|
|
7
|
+
[0, 16777215],
|
|
8
|
+
[167772160, 184549375],
|
|
9
|
+
[1681915904, 1686110207],
|
|
10
|
+
[2130706432, 2147483647],
|
|
11
|
+
[2851995648, 2852061183],
|
|
12
|
+
[2886729728, 2887778303],
|
|
13
|
+
[3221225472, 3221225727],
|
|
14
|
+
[3221225984, 3221226239],
|
|
15
|
+
[3227017984, 3227018239],
|
|
16
|
+
[3232235520, 3232301055],
|
|
17
|
+
[3323068416, 3323199487],
|
|
18
|
+
[3325256704, 3325256959],
|
|
19
|
+
[3405803776, 3405804031],
|
|
20
|
+
[3758096384, 4294967295]
|
|
21
|
+
];
|
|
22
|
+
var IPV6_SPECIAL_PREFIXES = [
|
|
23
|
+
{ groups: [0, 0, 0, 0, 0, 0, 0, 0], bits: 128 },
|
|
24
|
+
{ groups: [0, 0, 0, 0, 0, 0, 0, 1], bits: 128 },
|
|
25
|
+
{ groups: [0, 0, 0, 0, 0, 65535, 0, 0], bits: 96 },
|
|
26
|
+
{ groups: [100, 65435, 0, 0, 0, 0, 0, 0], bits: 96 },
|
|
27
|
+
{ groups: [256, 0, 0, 0, 0, 0, 0, 0], bits: 64 },
|
|
28
|
+
{ groups: [8193, 0, 0, 0, 0, 0, 0, 0], bits: 32 },
|
|
29
|
+
{ groups: [8193, 2, 0, 0, 0, 0, 0, 0], bits: 48 },
|
|
30
|
+
{ groups: [8193, 16, 0, 0, 0, 0, 0, 0], bits: 28 },
|
|
31
|
+
{ groups: [8193, 3512, 0, 0, 0, 0, 0, 0], bits: 32 },
|
|
32
|
+
{ groups: [8194, 0, 0, 0, 0, 0, 0, 0], bits: 16 },
|
|
33
|
+
{ groups: [16383, 0, 0, 0, 0, 0, 0, 0], bits: 20 },
|
|
34
|
+
{ groups: [64512, 0, 0, 0, 0, 0, 0, 0], bits: 7 },
|
|
35
|
+
{ groups: [65152, 0, 0, 0, 0, 0, 0, 0], bits: 10 },
|
|
36
|
+
{ groups: [65216, 0, 0, 0, 0, 0, 0, 0], bits: 10 },
|
|
37
|
+
{ groups: [65280, 0, 0, 0, 0, 0, 0, 0], bits: 8 }
|
|
38
|
+
];
|
|
39
|
+
function isPrivateAddress(address) {
|
|
40
|
+
const normalized = stripZoneId(address);
|
|
41
|
+
const version = isIP(normalized);
|
|
42
|
+
if (version === 4) {
|
|
43
|
+
const integer = ipv4ToInt(normalized);
|
|
44
|
+
if (integer === undefined)
|
|
45
|
+
return true;
|
|
46
|
+
return IPV4_PRIVATE_RANGES.some(([low, high]) => integer >= low && integer <= high);
|
|
47
|
+
}
|
|
48
|
+
if (version === 6) {
|
|
49
|
+
const groups = ipv6Groups(normalized);
|
|
50
|
+
if (!groups)
|
|
51
|
+
return true;
|
|
52
|
+
for (const prefix of IPV6_SPECIAL_PREFIXES) {
|
|
53
|
+
if (!ipv6MatchesPrefix(groups, prefix.groups, prefix.bits))
|
|
54
|
+
continue;
|
|
55
|
+
if (prefix.bits === 96 && groups[5] === 65535) {
|
|
56
|
+
return isPrivateAddress(ipv4IntToString(groups[6] << 16 | groups[7]));
|
|
57
|
+
}
|
|
58
|
+
if (prefix.bits === 16 && groups[0] === 8194) {
|
|
59
|
+
return isPrivateAddress(ipv4IntToString(groups[1] << 16 | groups[2]));
|
|
60
|
+
}
|
|
61
|
+
return true;
|
|
62
|
+
}
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
65
|
+
return true;
|
|
66
|
+
}
|
|
67
|
+
async function resolveWebhookTarget(url, policy = {}) {
|
|
68
|
+
const hostname = normalizeHostname(url.hostname);
|
|
69
|
+
const allowlist = (policy.allowPrivateHosts ?? []).map((entry) => normalizeHostname(entry.toLowerCase()));
|
|
70
|
+
if (allowlist.includes(hostname)) {
|
|
71
|
+
const version2 = isIP(hostname);
|
|
72
|
+
if (version2 === 4 || version2 === 6) {
|
|
73
|
+
return { hostname, addresses: [hostname] };
|
|
74
|
+
}
|
|
75
|
+
const lookup2 = policy.lookup ?? defaultTargetLookup;
|
|
76
|
+
let resolved2;
|
|
77
|
+
try {
|
|
78
|
+
resolved2 = await lookup2(hostname);
|
|
79
|
+
} catch {
|
|
80
|
+
throw new Error(`Webhook target ${hostname} could not be resolved`);
|
|
81
|
+
}
|
|
82
|
+
if (!Array.isArray(resolved2) || resolved2.length === 0) {
|
|
83
|
+
throw new Error(`Webhook target ${hostname} resolved to no addresses`);
|
|
84
|
+
}
|
|
85
|
+
const addresses = resolved2.map((entry) => normalizeHostname(entry.address));
|
|
86
|
+
return { hostname, addresses };
|
|
87
|
+
}
|
|
88
|
+
const version = isIP(hostname);
|
|
89
|
+
if (version === 4 || version === 6) {
|
|
90
|
+
if (isPrivateAddress(hostname)) {
|
|
91
|
+
throw new Error(`Webhook target ${hostname} is a private or special-use address`);
|
|
92
|
+
}
|
|
93
|
+
return { hostname, addresses: [hostname] };
|
|
94
|
+
}
|
|
95
|
+
const lookup = policy.lookup ?? defaultTargetLookup;
|
|
96
|
+
let resolved;
|
|
97
|
+
try {
|
|
98
|
+
resolved = await lookup(hostname);
|
|
99
|
+
} catch {
|
|
100
|
+
throw new Error(`Webhook target ${hostname} could not be resolved`);
|
|
101
|
+
}
|
|
102
|
+
if (!Array.isArray(resolved) || resolved.length === 0) {
|
|
103
|
+
throw new Error(`Webhook target ${hostname} resolved to no addresses`);
|
|
104
|
+
}
|
|
105
|
+
const allowed = [];
|
|
106
|
+
for (const entry of resolved) {
|
|
107
|
+
const address = normalizeHostname(entry.address);
|
|
108
|
+
if (isPrivateAddress(address)) {
|
|
109
|
+
if (allowlist.includes(address)) {
|
|
110
|
+
allowed.push(address);
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
throw new Error(`Webhook target ${hostname} resolves to private or special-use address ${address}`);
|
|
114
|
+
}
|
|
115
|
+
allowed.push(address);
|
|
116
|
+
}
|
|
117
|
+
if (allowed.length === 0) {
|
|
118
|
+
throw new Error(`Webhook target ${hostname} resolved to no public addresses`);
|
|
119
|
+
}
|
|
120
|
+
return { hostname, addresses: allowed };
|
|
121
|
+
}
|
|
122
|
+
async function assertWebhookTargetAllowed(url, policy = {}) {
|
|
123
|
+
await resolveWebhookTarget(url, policy);
|
|
124
|
+
}
|
|
125
|
+
function normalizeMaxRedirects(value) {
|
|
126
|
+
if (value === undefined)
|
|
127
|
+
return DEFAULT_MAX_REDIRECTS;
|
|
128
|
+
if (!Number.isInteger(value) || value < 0)
|
|
129
|
+
throw new Error("webhookTargetPolicy.maxRedirects must be a non-negative integer");
|
|
130
|
+
return value;
|
|
131
|
+
}
|
|
132
|
+
var defaultTargetLookup = async (hostname) => {
|
|
133
|
+
return dnsLookup(hostname, { all: true, verbatim: false });
|
|
134
|
+
};
|
|
135
|
+
function normalizeHostname(hostname) {
|
|
136
|
+
const lower = hostname.toLowerCase();
|
|
137
|
+
if (lower.startsWith("[") && lower.endsWith("]"))
|
|
138
|
+
return lower.slice(1, -1);
|
|
139
|
+
return lower;
|
|
140
|
+
}
|
|
141
|
+
function stripZoneId(address) {
|
|
142
|
+
const percent = address.indexOf("%");
|
|
143
|
+
return percent === -1 ? address : address.slice(0, percent);
|
|
144
|
+
}
|
|
145
|
+
function ipv4ToInt(address) {
|
|
146
|
+
const parts = address.split(".");
|
|
147
|
+
if (parts.length !== 4)
|
|
148
|
+
return;
|
|
149
|
+
let value = 0;
|
|
150
|
+
for (const part of parts) {
|
|
151
|
+
if (!/^\d{1,3}$/.test(part))
|
|
152
|
+
return;
|
|
153
|
+
const octet = Number(part);
|
|
154
|
+
if (octet > 255)
|
|
155
|
+
return;
|
|
156
|
+
value = value << 8 | octet;
|
|
157
|
+
}
|
|
158
|
+
return value >>> 0;
|
|
159
|
+
}
|
|
160
|
+
function ipv4IntToString(integer) {
|
|
161
|
+
return [
|
|
162
|
+
integer >>> 24 & 255,
|
|
163
|
+
integer >>> 16 & 255,
|
|
164
|
+
integer >>> 8 & 255,
|
|
165
|
+
integer & 255
|
|
166
|
+
].join(".");
|
|
167
|
+
}
|
|
168
|
+
function ipv6Groups(address) {
|
|
169
|
+
const raw = stripZoneId(address);
|
|
170
|
+
const doubleColon = raw.indexOf("::");
|
|
171
|
+
const headText = doubleColon === -1 ? raw : raw.slice(0, doubleColon);
|
|
172
|
+
const tailText = doubleColon === -1 ? "" : raw.slice(doubleColon + 2);
|
|
173
|
+
const parseGroups = (text) => {
|
|
174
|
+
if (text === "")
|
|
175
|
+
return [];
|
|
176
|
+
const out = [];
|
|
177
|
+
for (const part of text.split(":")) {
|
|
178
|
+
if (part.includes(".")) {
|
|
179
|
+
const v4 = ipv4ToInt(part);
|
|
180
|
+
if (v4 === undefined)
|
|
181
|
+
return;
|
|
182
|
+
out.push(v4 >>> 16 & 65535, v4 & 65535);
|
|
183
|
+
} else {
|
|
184
|
+
if (!/^[0-9a-fA-F]{1,4}$/.test(part))
|
|
185
|
+
return;
|
|
186
|
+
out.push(parseInt(part, 16));
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
return out;
|
|
190
|
+
};
|
|
191
|
+
const head = parseGroups(headText);
|
|
192
|
+
if (!head)
|
|
193
|
+
return;
|
|
194
|
+
const tail = parseGroups(tailText);
|
|
195
|
+
if (!tail)
|
|
196
|
+
return;
|
|
197
|
+
const total = head.length + tail.length;
|
|
198
|
+
if (doubleColon === -1) {
|
|
199
|
+
return total === 8 ? head : undefined;
|
|
200
|
+
}
|
|
201
|
+
if (total >= 8)
|
|
202
|
+
return;
|
|
203
|
+
return [...head, ...new Array(8 - total).fill(0), ...tail];
|
|
204
|
+
}
|
|
205
|
+
function ipv6MatchesPrefix(groups, prefixGroups, prefixBits) {
|
|
206
|
+
let remaining = prefixBits;
|
|
207
|
+
for (let index = 0;index < prefixGroups.length && remaining > 0; index += 1) {
|
|
208
|
+
const take = Math.min(16, remaining);
|
|
209
|
+
const mask = 65535 << 16 - take & 65535;
|
|
210
|
+
if ((groups[index] & mask) !== (prefixGroups[index] & mask))
|
|
211
|
+
return false;
|
|
212
|
+
remaining -= take;
|
|
213
|
+
}
|
|
214
|
+
return true;
|
|
215
|
+
}
|
|
216
|
+
export {
|
|
217
|
+
resolveWebhookTarget,
|
|
218
|
+
normalizeMaxRedirects,
|
|
219
|
+
isPrivateAddress,
|
|
220
|
+
assertWebhookTargetAllowed,
|
|
221
|
+
DEFAULT_MAX_REDIRECTS
|
|
222
|
+
};
|
package/dist/storage.js
CHANGED
|
@@ -2,16 +2,121 @@
|
|
|
2
2
|
// src/storage.ts
|
|
3
3
|
import { chmod, mkdir, readFile, rename, writeFile } from "fs/promises";
|
|
4
4
|
import { Buffer } from "buffer";
|
|
5
|
+
import { existsSync as existsSync2 } from "fs";
|
|
6
|
+
import { join as join3 } from "path";
|
|
7
|
+
|
|
8
|
+
// src/app-home.ts
|
|
5
9
|
import { existsSync } from "fs";
|
|
10
|
+
import { homedir as homedir2 } from "os";
|
|
11
|
+
import { join as join2, resolve } from "path";
|
|
12
|
+
|
|
13
|
+
// ../../node_modules/.bun/@hasna+paths@0.1.0/node_modules/@hasna/paths/dist/index.js
|
|
6
14
|
import { homedir } from "os";
|
|
7
15
|
import { join } from "path";
|
|
16
|
+
var KIND_ENV = {
|
|
17
|
+
config: "HASNA_CONFIG_HOME",
|
|
18
|
+
data: "HASNA_DATA_HOME",
|
|
19
|
+
state: "HASNA_STATE_HOME",
|
|
20
|
+
cache: "HASNA_CACHE_HOME"
|
|
21
|
+
};
|
|
22
|
+
var APP_SLUG_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
23
|
+
function assertApp(app) {
|
|
24
|
+
if (typeof app !== "string" || app.length === 0) {
|
|
25
|
+
throw new TypeError("paths: app must be a non-empty string");
|
|
26
|
+
}
|
|
27
|
+
if (!APP_SLUG_RE.test(app)) {
|
|
28
|
+
throw new TypeError(`paths: invalid app slug "${app}" \u2014 expected lowercase kebab-case ([a-z0-9]+(-[a-z0-9]+)*)`);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
function envOf(options) {
|
|
32
|
+
return options.env ?? process.env;
|
|
33
|
+
}
|
|
34
|
+
function envValue(options, kind) {
|
|
35
|
+
const value = envOf(options)[KIND_ENV[kind]];
|
|
36
|
+
return typeof value === "string" && value.length > 0 ? value : undefined;
|
|
37
|
+
}
|
|
38
|
+
function isMacOS(platform) {
|
|
39
|
+
return platform === "darwin";
|
|
40
|
+
}
|
|
41
|
+
function baseDir(kind, options) {
|
|
42
|
+
const override = envValue(options, kind);
|
|
43
|
+
if (override)
|
|
44
|
+
return override;
|
|
45
|
+
const home = options.home ?? homedir();
|
|
46
|
+
const platform = options.platform ?? process.platform;
|
|
47
|
+
if (isMacOS(platform)) {
|
|
48
|
+
switch (kind) {
|
|
49
|
+
case "config":
|
|
50
|
+
case "data":
|
|
51
|
+
return join(home, "Library", "Application Support", "Hasna");
|
|
52
|
+
case "cache":
|
|
53
|
+
return join(home, "Library", "Caches", "Hasna");
|
|
54
|
+
case "state":
|
|
55
|
+
return join(home, "Library", "Logs", "Hasna");
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
switch (kind) {
|
|
59
|
+
case "config":
|
|
60
|
+
return join(home, ".config", "hasna");
|
|
61
|
+
case "data":
|
|
62
|
+
return join(home, ".local", "share", "hasna");
|
|
63
|
+
case "state":
|
|
64
|
+
return join(home, ".local", "state", "hasna");
|
|
65
|
+
case "cache":
|
|
66
|
+
return join(home, ".cache", "hasna");
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
function resolvePath(kind, options) {
|
|
70
|
+
assertApp(options.app);
|
|
71
|
+
const appSegment = options.internal === true ? join("internal", options.app) : options.app;
|
|
72
|
+
return join(baseDir(kind, options), appSegment);
|
|
73
|
+
}
|
|
74
|
+
function dataDir(options) {
|
|
75
|
+
return resolvePath("data", options);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// src/app-home.ts
|
|
8
79
|
var HASNA_EVENTS_DIR_ENV = "HASNA_EVENTS_DIR";
|
|
9
80
|
var HASNA_EVENTS_HOME_ENV = "HASNA_EVENTS_HOME";
|
|
81
|
+
var EVENTS_STORE_SENTINEL_FILE = "events.json";
|
|
82
|
+
function effectiveHome() {
|
|
83
|
+
return process.env["HOME"] || process.env["USERPROFILE"] || homedir2();
|
|
84
|
+
}
|
|
85
|
+
function legacyHomeDir() {
|
|
86
|
+
return join2(effectiveHome(), ".hasna", "events");
|
|
87
|
+
}
|
|
88
|
+
function resolverHome() {
|
|
89
|
+
return dataDir({ app: "events", home: effectiveHome() || undefined });
|
|
90
|
+
}
|
|
91
|
+
function adoptResolverHome(resolved, env = process.env) {
|
|
92
|
+
const dataOverride = env.HASNA_DATA_HOME;
|
|
93
|
+
if (typeof dataOverride === "string" && dataOverride.trim().length > 0)
|
|
94
|
+
return true;
|
|
95
|
+
return existsSync(join2(resolved, EVENTS_STORE_SENTINEL_FILE));
|
|
96
|
+
}
|
|
97
|
+
function exactEventsHome() {
|
|
98
|
+
const dir = process.env[HASNA_EVENTS_DIR_ENV];
|
|
99
|
+
if (dir && dir.trim())
|
|
100
|
+
return dir.trim();
|
|
101
|
+
const home = process.env[HASNA_EVENTS_HOME_ENV];
|
|
102
|
+
if (home && home.trim())
|
|
103
|
+
return home.trim();
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
function getEventsHome() {
|
|
107
|
+
const exact = exactEventsHome();
|
|
108
|
+
if (exact)
|
|
109
|
+
return resolve(exact);
|
|
110
|
+
const resolved = resolverHome();
|
|
111
|
+
return adoptResolverHome(resolved) ? resolve(resolved) : resolve(legacyHomeDir());
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// src/storage.ts
|
|
10
115
|
var LOCAL_JSON_EVENT_CURSOR_PREFIX = "local-json-v1:";
|
|
11
116
|
var DEFAULT_EVENT_PAGE_LIMIT = 100;
|
|
12
117
|
var MAX_EVENT_PAGE_LIMIT = 1000;
|
|
13
118
|
function getEventsDataDir(override) {
|
|
14
|
-
return override ||
|
|
119
|
+
return override || getEventsHome();
|
|
15
120
|
}
|
|
16
121
|
function getActiveEventsDirEnv() {
|
|
17
122
|
if (process.env[HASNA_EVENTS_DIR_ENV])
|
|
@@ -27,12 +132,12 @@ class JsonEventsStore {
|
|
|
27
132
|
channelsPath;
|
|
28
133
|
eventsPath;
|
|
29
134
|
deliveriesPath;
|
|
30
|
-
constructor(
|
|
31
|
-
this.dataDir =
|
|
32
|
-
this.runtime = localJsonRuntime(
|
|
33
|
-
this.channelsPath =
|
|
34
|
-
this.eventsPath =
|
|
35
|
-
this.deliveriesPath =
|
|
135
|
+
constructor(dataDir2 = getEventsDataDir()) {
|
|
136
|
+
this.dataDir = dataDir2;
|
|
137
|
+
this.runtime = localJsonRuntime(dataDir2);
|
|
138
|
+
this.channelsPath = join3(dataDir2, "channels.json");
|
|
139
|
+
this.eventsPath = join3(dataDir2, "events.json");
|
|
140
|
+
this.deliveriesPath = join3(dataDir2, "deliveries.json");
|
|
36
141
|
}
|
|
37
142
|
async init() {
|
|
38
143
|
await mkdir(this.dataDir, { recursive: true, mode: 448 });
|
|
@@ -149,7 +254,7 @@ class JsonEventsStore {
|
|
|
149
254
|
};
|
|
150
255
|
}
|
|
151
256
|
async ensureArrayFile(path) {
|
|
152
|
-
if (!
|
|
257
|
+
if (!existsSync2(path)) {
|
|
153
258
|
await writeFile(path, `[]
|
|
154
259
|
`, { encoding: "utf-8", mode: 384 });
|
|
155
260
|
}
|
|
@@ -179,7 +284,7 @@ class JsonEventsStore {
|
|
|
179
284
|
});
|
|
180
285
|
}
|
|
181
286
|
}
|
|
182
|
-
function localJsonRuntime(
|
|
287
|
+
function localJsonRuntime(dataDir2 = getEventsDataDir()) {
|
|
183
288
|
return {
|
|
184
289
|
mode: "local-files",
|
|
185
290
|
name: "json-events-store",
|
|
@@ -192,7 +297,7 @@ function localJsonRuntime(dataDir = getEventsDataDir()) {
|
|
|
192
297
|
durable: true,
|
|
193
298
|
idempotency: "best-effort-local",
|
|
194
299
|
replayCursors: true,
|
|
195
|
-
description: `Local JSON files in ${
|
|
300
|
+
description: `Local JSON files in ${dataDir2}; no SQLite, Postgres, S3, or AWS runtime is configured by this store.`
|
|
196
301
|
};
|
|
197
302
|
}
|
|
198
303
|
function encodeLocalJsonEventCursor(offset, options = {}) {
|
|
@@ -256,8 +361,8 @@ function assertCursorFilter(name, cursorValue, optionValue) {
|
|
|
256
361
|
function findEventByIdentity(events, identity) {
|
|
257
362
|
return events.find((event) => identity.id !== undefined && event.id === identity.id || identity.dedupeKey !== undefined && event.dedupeKey === identity.dedupeKey);
|
|
258
363
|
}
|
|
259
|
-
async function getEventsStatus(
|
|
260
|
-
const store = new JsonEventsStore(
|
|
364
|
+
async function getEventsStatus(dataDir2) {
|
|
365
|
+
const store = new JsonEventsStore(dataDir2);
|
|
261
366
|
await store.init();
|
|
262
367
|
const [channels, events, deliveries] = await Promise.all([
|
|
263
368
|
store.listChannels(),
|
|
@@ -299,9 +404,9 @@ async function getEventsStatus(dataDir) {
|
|
|
299
404
|
}
|
|
300
405
|
};
|
|
301
406
|
}
|
|
302
|
-
function statusFile(
|
|
303
|
-
const path =
|
|
304
|
-
return { path, exists:
|
|
407
|
+
function statusFile(dataDir2, fileName, records) {
|
|
408
|
+
const path = join3(dataDir2, fileName);
|
|
409
|
+
return { path, exists: existsSync2(path), records };
|
|
305
410
|
}
|
|
306
411
|
export {
|
|
307
412
|
normalizeEventPageLimit,
|