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 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|required",
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.1.0" : "latest");
13754
+ const [ok, err, version] = tryFn(() => true ? "11.2.0" : "latest");
13614
13755
  return ok ? version : "latest";
13615
13756
  })();
13616
13757
  this.resources = {};