s3db.js 11.1.0 → 11.2.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/SECURITY.md +76 -0
- package/dist/s3db.cjs.js +144 -3
- package/dist/s3db.cjs.js.map +1 -1
- package/dist/s3db.es.js +144 -3
- package/dist/s3db.es.js.map +1 -1
- package/package.json +2 -1
- package/src/plugins/eventual-consistency/analytics.js +164 -2
- package/src/plugins/eventual-consistency/install.js +1 -1
- package/src/plugins/eventual-consistency/utils.js +64 -0
package/SECURITY.md
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# Security Policy
|
|
2
|
+
|
|
3
|
+
## Supported Versions
|
|
4
|
+
|
|
5
|
+
| Version | Supported |
|
|
6
|
+
| ------- | ------------------ |
|
|
7
|
+
| 11.x.x | :white_check_mark: |
|
|
8
|
+
| < 11.0 | :x: |
|
|
9
|
+
|
|
10
|
+
## Known Security Advisories
|
|
11
|
+
|
|
12
|
+
### Development Dependencies
|
|
13
|
+
|
|
14
|
+
The following vulnerabilities exist in **development-only** dependencies and **do not affect** the published npm package or runtime security:
|
|
15
|
+
|
|
16
|
+
#### pkg (GHSA-22r3-9w55-cj54) - MODERATE
|
|
17
|
+
- **Status**: Acknowledged, monitored
|
|
18
|
+
- **Impact**: Local privilege escalation
|
|
19
|
+
- **Scope**: Only affects developers running `pnpm run build:binaries`
|
|
20
|
+
- **Mitigation**: pkg is deprecated and archived. No patched version available (`<0.0.0`).
|
|
21
|
+
- **Risk Assessment**: LOW - Only used for creating standalone binaries during release process
|
|
22
|
+
- **Future Plans**: Migrate to Node.js Single Executable Applications (SEA) when stable
|
|
23
|
+
|
|
24
|
+
#### tar-fs - HIGH
|
|
25
|
+
- **Status**: RESOLVED in v11.1.1+
|
|
26
|
+
- **Fix**: Updated to patched version 2.1.4+
|
|
27
|
+
|
|
28
|
+
## Reporting a Vulnerability
|
|
29
|
+
|
|
30
|
+
If you discover a security vulnerability in the **runtime code** (not dev dependencies), please report it by:
|
|
31
|
+
|
|
32
|
+
1. **DO NOT** open a public issue
|
|
33
|
+
2. Email: [security contact - update this]
|
|
34
|
+
3. Include:
|
|
35
|
+
- Description of the vulnerability
|
|
36
|
+
- Steps to reproduce
|
|
37
|
+
- Potential impact
|
|
38
|
+
- Suggested fix (if any)
|
|
39
|
+
|
|
40
|
+
### Response Timeline
|
|
41
|
+
|
|
42
|
+
- **Initial Response**: Within 48 hours
|
|
43
|
+
- **Status Update**: Within 7 days
|
|
44
|
+
- **Fix Timeline**: Depends on severity
|
|
45
|
+
- Critical: 7 days
|
|
46
|
+
- High: 14 days
|
|
47
|
+
- Medium: 30 days
|
|
48
|
+
- Low: 60 days
|
|
49
|
+
|
|
50
|
+
## Security Best Practices
|
|
51
|
+
|
|
52
|
+
### For Users
|
|
53
|
+
|
|
54
|
+
1. **Always encrypt sensitive data**: Use `secret` field type for passwords, tokens, etc.
|
|
55
|
+
2. **Validate credentials**: Never commit AWS credentials to version control
|
|
56
|
+
3. **Use IAM policies**: Implement least-privilege access for S3 buckets
|
|
57
|
+
4. **Enable paranoid mode**: For production, use `paranoid: true` for soft deletes
|
|
58
|
+
5. **Audit hooks**: Review serialized functions before deploying to production
|
|
59
|
+
|
|
60
|
+
### For Contributors
|
|
61
|
+
|
|
62
|
+
1. **No secrets in tests**: Use environment variables or LocalStack
|
|
63
|
+
2. **Validate input**: All user input should be validated before S3 operations
|
|
64
|
+
3. **Handle errors safely**: Never expose AWS error details to end users
|
|
65
|
+
4. **Review dependencies**: Run `pnpm audit` before submitting PRs
|
|
66
|
+
5. **Test encryption**: Verify `secret` fields are actually encrypted in S3
|
|
67
|
+
|
|
68
|
+
## Audit Configuration
|
|
69
|
+
|
|
70
|
+
This project uses `audit-level=high` in `.npmrc` to focus on critical vulnerabilities affecting production. Moderate/low severity issues in dev-only dependencies are monitored but may not block releases if:
|
|
71
|
+
|
|
72
|
+
- They only affect development tools
|
|
73
|
+
- No patch is available
|
|
74
|
+
- The risk is assessed as acceptable
|
|
75
|
+
|
|
76
|
+
Current audit threshold: **HIGH** (ignores moderate/low in dev dependencies)
|
package/dist/s3db.cjs.js
CHANGED
|
@@ -5403,6 +5403,38 @@ function groupByCohort(transactions, cohortField) {
|
|
|
5403
5403
|
}
|
|
5404
5404
|
return groups;
|
|
5405
5405
|
}
|
|
5406
|
+
function ensureCohortHour(transaction, timezone = "UTC", verbose = false) {
|
|
5407
|
+
if (transaction.cohortHour) {
|
|
5408
|
+
return transaction;
|
|
5409
|
+
}
|
|
5410
|
+
if (transaction.timestamp) {
|
|
5411
|
+
const date = new Date(transaction.timestamp);
|
|
5412
|
+
const cohortInfo = getCohortInfo(date, timezone, verbose);
|
|
5413
|
+
if (verbose) {
|
|
5414
|
+
console.log(
|
|
5415
|
+
`[EventualConsistency] Transaction ${transaction.id} missing cohortHour, calculated from timestamp: ${cohortInfo.hour}`
|
|
5416
|
+
);
|
|
5417
|
+
}
|
|
5418
|
+
transaction.cohortHour = cohortInfo.hour;
|
|
5419
|
+
if (!transaction.cohortWeek) {
|
|
5420
|
+
transaction.cohortWeek = cohortInfo.week;
|
|
5421
|
+
}
|
|
5422
|
+
if (!transaction.cohortMonth) {
|
|
5423
|
+
transaction.cohortMonth = cohortInfo.month;
|
|
5424
|
+
}
|
|
5425
|
+
} else if (verbose) {
|
|
5426
|
+
console.warn(
|
|
5427
|
+
`[EventualConsistency] Transaction ${transaction.id} missing both cohortHour and timestamp, cannot calculate cohort`
|
|
5428
|
+
);
|
|
5429
|
+
}
|
|
5430
|
+
return transaction;
|
|
5431
|
+
}
|
|
5432
|
+
function ensureCohortHours(transactions, timezone = "UTC", verbose = false) {
|
|
5433
|
+
if (!transactions || !Array.isArray(transactions)) {
|
|
5434
|
+
return transactions;
|
|
5435
|
+
}
|
|
5436
|
+
return transactions.map((txn) => ensureCohortHour(txn, timezone, verbose));
|
|
5437
|
+
}
|
|
5406
5438
|
|
|
5407
5439
|
function createPartitionConfig() {
|
|
5408
5440
|
const partitions = {
|
|
@@ -6527,7 +6559,10 @@ async function getAnalytics(resourceName, field, options, fieldHandlers) {
|
|
|
6527
6559
|
if (!handler.analyticsResource) {
|
|
6528
6560
|
throw new Error("Analytics not enabled for this plugin");
|
|
6529
6561
|
}
|
|
6530
|
-
const { period = "day", date, startDate, endDate, month, year, breakdown = false } = options;
|
|
6562
|
+
const { period = "day", date, startDate, endDate, month, year, breakdown = false, recordId } = options;
|
|
6563
|
+
if (recordId) {
|
|
6564
|
+
return await getAnalyticsForRecord(resourceName, field, recordId, options, handler);
|
|
6565
|
+
}
|
|
6531
6566
|
const [ok, err, allAnalytics] = await tryFn(
|
|
6532
6567
|
() => handler.analyticsResource.list()
|
|
6533
6568
|
);
|
|
@@ -6566,6 +6601,105 @@ async function getAnalytics(resourceName, field, options, fieldHandlers) {
|
|
|
6566
6601
|
recordCount: a.recordCount
|
|
6567
6602
|
}));
|
|
6568
6603
|
}
|
|
6604
|
+
async function getAnalyticsForRecord(resourceName, field, recordId, options, handler) {
|
|
6605
|
+
const { period = "day", date, startDate, endDate, month, year } = options;
|
|
6606
|
+
const [okTrue, errTrue, appliedTransactions] = await tryFn(
|
|
6607
|
+
() => handler.transactionResource.query({
|
|
6608
|
+
originalId: recordId,
|
|
6609
|
+
applied: true
|
|
6610
|
+
})
|
|
6611
|
+
);
|
|
6612
|
+
const [okFalse, errFalse, pendingTransactions] = await tryFn(
|
|
6613
|
+
() => handler.transactionResource.query({
|
|
6614
|
+
originalId: recordId,
|
|
6615
|
+
applied: false
|
|
6616
|
+
})
|
|
6617
|
+
);
|
|
6618
|
+
let allTransactions = [
|
|
6619
|
+
...okTrue && appliedTransactions ? appliedTransactions : [],
|
|
6620
|
+
...okFalse && pendingTransactions ? pendingTransactions : []
|
|
6621
|
+
];
|
|
6622
|
+
if (allTransactions.length === 0) {
|
|
6623
|
+
return [];
|
|
6624
|
+
}
|
|
6625
|
+
allTransactions = ensureCohortHours(allTransactions, handler.config?.cohort?.timezone || "UTC", false);
|
|
6626
|
+
let filtered = allTransactions;
|
|
6627
|
+
if (date) {
|
|
6628
|
+
if (period === "hour") {
|
|
6629
|
+
filtered = filtered.filter((t) => t.cohortHour && t.cohortHour.startsWith(date));
|
|
6630
|
+
} else if (period === "day") {
|
|
6631
|
+
filtered = filtered.filter((t) => t.cohortDate === date);
|
|
6632
|
+
} else if (period === "month") {
|
|
6633
|
+
filtered = filtered.filter((t) => t.cohortMonth && t.cohortMonth.startsWith(date));
|
|
6634
|
+
}
|
|
6635
|
+
} else if (startDate && endDate) {
|
|
6636
|
+
if (period === "hour") {
|
|
6637
|
+
filtered = filtered.filter((t) => t.cohortHour && t.cohortHour >= startDate && t.cohortHour <= endDate);
|
|
6638
|
+
} else if (period === "day") {
|
|
6639
|
+
filtered = filtered.filter((t) => t.cohortDate && t.cohortDate >= startDate && t.cohortDate <= endDate);
|
|
6640
|
+
} else if (period === "month") {
|
|
6641
|
+
filtered = filtered.filter((t) => t.cohortMonth && t.cohortMonth >= startDate && t.cohortMonth <= endDate);
|
|
6642
|
+
}
|
|
6643
|
+
} else if (month) {
|
|
6644
|
+
if (period === "hour") {
|
|
6645
|
+
filtered = filtered.filter((t) => t.cohortHour && t.cohortHour.startsWith(month));
|
|
6646
|
+
} else if (period === "day") {
|
|
6647
|
+
filtered = filtered.filter((t) => t.cohortDate && t.cohortDate.startsWith(month));
|
|
6648
|
+
}
|
|
6649
|
+
} else if (year) {
|
|
6650
|
+
if (period === "hour") {
|
|
6651
|
+
filtered = filtered.filter((t) => t.cohortHour && t.cohortHour.startsWith(String(year)));
|
|
6652
|
+
} else if (period === "day") {
|
|
6653
|
+
filtered = filtered.filter((t) => t.cohortDate && t.cohortDate.startsWith(String(year)));
|
|
6654
|
+
} else if (period === "month") {
|
|
6655
|
+
filtered = filtered.filter((t) => t.cohortMonth && t.cohortMonth.startsWith(String(year)));
|
|
6656
|
+
}
|
|
6657
|
+
}
|
|
6658
|
+
const cohortField = period === "hour" ? "cohortHour" : period === "day" ? "cohortDate" : "cohortMonth";
|
|
6659
|
+
const aggregated = aggregateTransactionsByCohort(filtered, cohortField);
|
|
6660
|
+
return aggregated;
|
|
6661
|
+
}
|
|
6662
|
+
function aggregateTransactionsByCohort(transactions, cohortField) {
|
|
6663
|
+
const groups = {};
|
|
6664
|
+
for (const txn of transactions) {
|
|
6665
|
+
const cohort = txn[cohortField];
|
|
6666
|
+
if (!cohort) continue;
|
|
6667
|
+
if (!groups[cohort]) {
|
|
6668
|
+
groups[cohort] = {
|
|
6669
|
+
cohort,
|
|
6670
|
+
count: 0,
|
|
6671
|
+
sum: 0,
|
|
6672
|
+
min: Infinity,
|
|
6673
|
+
max: -Infinity,
|
|
6674
|
+
recordCount: /* @__PURE__ */ new Set(),
|
|
6675
|
+
operations: {}
|
|
6676
|
+
};
|
|
6677
|
+
}
|
|
6678
|
+
const group = groups[cohort];
|
|
6679
|
+
const signedValue = txn.operation === "sub" ? -txn.value : txn.value;
|
|
6680
|
+
group.count++;
|
|
6681
|
+
group.sum += signedValue;
|
|
6682
|
+
group.min = Math.min(group.min, signedValue);
|
|
6683
|
+
group.max = Math.max(group.max, signedValue);
|
|
6684
|
+
group.recordCount.add(txn.originalId);
|
|
6685
|
+
const op = txn.operation;
|
|
6686
|
+
if (!group.operations[op]) {
|
|
6687
|
+
group.operations[op] = { count: 0, sum: 0 };
|
|
6688
|
+
}
|
|
6689
|
+
group.operations[op].count++;
|
|
6690
|
+
group.operations[op].sum += signedValue;
|
|
6691
|
+
}
|
|
6692
|
+
return Object.values(groups).map((g) => ({
|
|
6693
|
+
cohort: g.cohort,
|
|
6694
|
+
count: g.count,
|
|
6695
|
+
sum: g.sum,
|
|
6696
|
+
avg: g.sum / g.count,
|
|
6697
|
+
min: g.min === Infinity ? 0 : g.min,
|
|
6698
|
+
max: g.max === -Infinity ? 0 : g.max,
|
|
6699
|
+
recordCount: g.recordCount.size,
|
|
6700
|
+
operations: g.operations
|
|
6701
|
+
})).sort((a, b) => a.cohort.localeCompare(b.cohort));
|
|
6702
|
+
}
|
|
6569
6703
|
async function getMonthByDay(resourceName, field, month, options, fieldHandlers) {
|
|
6570
6704
|
const year = parseInt(month.substring(0, 4));
|
|
6571
6705
|
const monthNum = parseInt(month.substring(5, 7));
|
|
@@ -6600,6 +6734,8 @@ async function getLastNDays(resourceName, field, days, options, fieldHandlers) {
|
|
|
6600
6734
|
return date.toISOString().substring(0, 10);
|
|
6601
6735
|
}).reverse();
|
|
6602
6736
|
const data = await getAnalytics(resourceName, field, {
|
|
6737
|
+
...options,
|
|
6738
|
+
// ✅ Include all options (recordId, etc.)
|
|
6603
6739
|
period: "day",
|
|
6604
6740
|
startDate: dates[0],
|
|
6605
6741
|
endDate: dates[dates.length - 1]
|
|
@@ -6793,6 +6929,8 @@ async function getLastNHours(resourceName, field, hours = 24, options, fieldHand
|
|
|
6793
6929
|
const startHour = hoursAgo.toISOString().substring(0, 13);
|
|
6794
6930
|
const endHour = now.toISOString().substring(0, 13);
|
|
6795
6931
|
const data = await getAnalytics(resourceName, field, {
|
|
6932
|
+
...options,
|
|
6933
|
+
// ✅ Include all options (recordId, etc.)
|
|
6796
6934
|
period: "hour",
|
|
6797
6935
|
startDate: startHour,
|
|
6798
6936
|
endDate: endHour
|
|
@@ -6840,6 +6978,8 @@ async function getLastNMonths(resourceName, field, months = 12, options, fieldHa
|
|
|
6840
6978
|
const startDate = monthsAgo.toISOString().substring(0, 7);
|
|
6841
6979
|
const endDate = now.toISOString().substring(0, 7);
|
|
6842
6980
|
const data = await getAnalytics(resourceName, field, {
|
|
6981
|
+
...options,
|
|
6982
|
+
// ✅ Include all options (recordId, etc.)
|
|
6843
6983
|
period: "month",
|
|
6844
6984
|
startDate,
|
|
6845
6985
|
endDate
|
|
@@ -7032,7 +7172,8 @@ async function completeFieldSetup(handler, database, config, plugin) {
|
|
|
7032
7172
|
operation: "string|required",
|
|
7033
7173
|
timestamp: "string|required",
|
|
7034
7174
|
cohortDate: "string|required",
|
|
7035
|
-
cohortHour: "string|
|
|
7175
|
+
cohortHour: "string|optional",
|
|
7176
|
+
// ✅ FIX BUG #2: Changed from required to optional for migration compatibility
|
|
7036
7177
|
cohortWeek: "string|optional",
|
|
7037
7178
|
cohortMonth: "string|optional",
|
|
7038
7179
|
source: "string|optional",
|
|
@@ -13610,7 +13751,7 @@ class Database extends EventEmitter {
|
|
|
13610
13751
|
this.id = idGenerator(7);
|
|
13611
13752
|
this.version = "1";
|
|
13612
13753
|
this.s3dbVersion = (() => {
|
|
13613
|
-
const [ok, err, version] = tryFn(() => true ? "11.
|
|
13754
|
+
const [ok, err, version] = tryFn(() => true ? "11.2.0" : "latest");
|
|
13614
13755
|
return ok ? version : "latest";
|
|
13615
13756
|
})();
|
|
13616
13757
|
this.resources = {};
|