@vandenberghinc/volt 1.2.2 → 1.2.4

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.
Files changed (26) hide show
  1. package/backend/dist/cjs/backend/src/database/collection.d.ts +17 -10
  2. package/backend/dist/cjs/backend/src/database/collection.js +289 -126
  3. package/backend/dist/cjs/backend/src/payments/stripe/products.js +21 -6
  4. package/backend/dist/cjs/backend/src/stream.d.ts +58 -6
  5. package/backend/dist/cjs/backend/src/stream.js +91 -12
  6. package/backend/dist/cjs/backend/src/users.d.ts +11 -4
  7. package/backend/dist/cjs/backend/src/users.js +93 -26
  8. package/backend/dist/esm/backend/src/database/collection.d.ts +17 -10
  9. package/backend/dist/esm/backend/src/database/collection.js +311 -152
  10. package/backend/dist/esm/backend/src/database/filters/strict_update_filter_test.js +10 -0
  11. package/backend/dist/esm/backend/src/payments/stripe/products.js +24 -3
  12. package/backend/dist/esm/backend/src/stream.d.ts +58 -6
  13. package/backend/dist/esm/backend/src/stream.js +96 -12
  14. package/backend/dist/esm/backend/src/users.d.ts +11 -4
  15. package/backend/dist/esm/backend/src/users.js +95 -27
  16. package/frontend/dist/backend/src/database/collection.d.ts +17 -10
  17. package/frontend/dist/backend/src/database/collection.js +311 -152
  18. package/frontend/dist/backend/src/payments/stripe/products.js +24 -3
  19. package/frontend/dist/backend/src/stream.d.ts +58 -6
  20. package/frontend/dist/backend/src/stream.js +96 -12
  21. package/frontend/dist/backend/src/users.d.ts +11 -4
  22. package/frontend/dist/backend/src/users.js +95 -27
  23. package/frontend/dist/frontend/src/modules/user.js +3 -2
  24. package/frontend/dist/frontend/src/ui/table.d.ts +2 -2
  25. package/frontend/dist/frontend/src/ui/table.js +3 -6
  26. package/package.json +2 -2
@@ -1477,31 +1477,115 @@ export class Stream {
1477
1477
  remove_headers(...names) {
1478
1478
  return this.remove_header(...names);
1479
1479
  }
1480
- // Set a cookie.
1481
1480
  /**
1482
- * Set a cookie that will be sent with the response.
1481
+ * Set a cookie to be sent with the response.
1482
+ *
1483
+ * Accepts either:
1484
+ * 1) a pre-built cookie header string (used as-is, no validation), or
1485
+ * 2) a structured object describing the cookie, from which a standards-compliant
1486
+ * cookie string will be generated.
1487
+ *
1488
+ * If a cookie with the same name already exists in the pending response list,
1489
+ * it will be replaced.
1490
+ *
1491
+ * @warning Cookies are only included in the response when using `send()`,
1492
+ * `success()` or `error()`.
1483
1493
  *
1484
- * @warning Will only be added to the response when the user uses `send()`, `success()` or `error()`.
1485
- * @param cookie The cookie string.
1486
1494
  * @example
1487
1495
  * ```ts
1488
- * stream.set_cookie("MyCookie=Hello World;");
1496
+ * stream.set_cookie("sid=abc123; Path=/; SameSite=Lax; Secure; HttpOnly");
1497
+ *
1498
+ * stream.set_cookie({
1499
+ * name: "sid",
1500
+ * value: session_id,
1501
+ * http_only: true,
1502
+ * secure: true,
1503
+ * same_site: "Lax",
1504
+ * path: "/",
1505
+ * max_age: 60 * 60 * 24 * 14,
1506
+ * });
1489
1507
  * ```
1490
- * @docs
1491
1508
  */
1492
1509
  set_cookie(cookie) {
1493
- cookie = cookie.trim();
1494
- const name_end = cookie.indexOf("=");
1510
+ // If the user provided a raw cookie string, trust it and use it as-is.
1511
+ if (typeof cookie === "string") {
1512
+ const cookie_str = cookie.trim();
1513
+ const name_end = cookie_str.indexOf("=");
1514
+ if (name_end !== -1) {
1515
+ const name = cookie_str.substring(0, name_end);
1516
+ for (let i = 0; i < this.res_cookies.length; i++) {
1517
+ if (this.res_cookies[i].startsWith(name)) {
1518
+ this.res_cookies[i] = cookie_str;
1519
+ return this;
1520
+ }
1521
+ }
1522
+ }
1523
+ this.res_cookies.push(cookie_str);
1524
+ return this;
1525
+ }
1526
+ // Structured cookie path (commercial-grade, predictable, minimal validation)
1527
+ const { name, value, path = "/", domain, max_age, expires, secure, http_only, same_site, prefix, extra, } = cookie;
1528
+ if (!name || typeof name !== "string") {
1529
+ throw new Error("set_cookie: cookie.name must be a non-empty string");
1530
+ }
1531
+ const full_name = `${prefix ?? ""}${name}`;
1532
+ // Enforce prefix rules (light but correct)
1533
+ if (prefix === "__Host-") {
1534
+ if (domain) {
1535
+ throw new Error("__Host- cookies must not include a domain attribute");
1536
+ }
1537
+ if (path !== "/") {
1538
+ throw new Error("__Host- cookies must have path='/'");
1539
+ }
1540
+ if (!secure) {
1541
+ throw new Error("__Host- cookies require secure=true");
1542
+ }
1543
+ }
1544
+ if (prefix === "__Secure-" && !secure) {
1545
+ throw new Error("__Secure- cookies require secure=true");
1546
+ }
1547
+ const encoded_value = value === null || typeof value === "undefined"
1548
+ ? ""
1549
+ : encodeURIComponent(String(value));
1550
+ const parts = [];
1551
+ parts.push(`${full_name}=${encoded_value}`);
1552
+ if (path)
1553
+ parts.push(`Path=${path}`);
1554
+ if (domain)
1555
+ parts.push(`Domain=${domain}`);
1556
+ if (typeof max_age === "number" && Number.isFinite(max_age)) {
1557
+ parts.push(`Max-Age=${Math.trunc(max_age)}`);
1558
+ }
1559
+ if (expires) {
1560
+ const exp = expires instanceof Date ? expires.toUTCString() : String(expires).trim();
1561
+ if (exp)
1562
+ parts.push(`Expires=${exp}`);
1563
+ }
1564
+ if (secure)
1565
+ parts.push("Secure");
1566
+ if (http_only)
1567
+ parts.push("HttpOnly");
1568
+ if (same_site)
1569
+ parts.push(`SameSite=${same_site}`);
1570
+ if (extra && Array.isArray(extra)) {
1571
+ for (const attr of extra) {
1572
+ const trimmed = String(attr).trim();
1573
+ if (trimmed)
1574
+ parts.push(trimmed);
1575
+ }
1576
+ }
1577
+ const cookie_str = parts.join("; ");
1578
+ const name_end = cookie_str.indexOf("=");
1495
1579
  if (name_end !== -1) {
1496
- const name = cookie.substr(0, name_end);
1580
+ const existing_name = cookie_str.substring(0, name_end);
1497
1581
  for (let i = 0; i < this.res_cookies.length; i++) {
1498
- if (this.res_cookies[i].startsWith(name)) {
1499
- this.res_cookies[i] = cookie;
1582
+ if (this.res_cookies[i].startsWith(existing_name)) {
1583
+ this.res_cookies[i] = cookie_str;
1500
1584
  return this;
1501
1585
  }
1502
1586
  }
1503
1587
  }
1504
- this.res_cookies.push(cookie);
1588
+ this.res_cookies.push(cookie_str);
1505
1589
  return this;
1506
1590
  }
1507
1591
  // Set cookies.
@@ -259,24 +259,31 @@ export declare class Users {
259
259
  }): Promise<void>;
260
260
  /**
261
261
  * Create the auth token cookie on the response.
262
+ * `T` is treated as a real authentication credential.
263
+ *
262
264
  * @param stream The request stream.
263
265
  * @param token The token string or Token object.
264
266
  */
265
267
  _create_token_cookie(stream: Stream, token: string | User.Token): void;
266
268
  /**
267
- * Create user cookies (id and activation flag).
269
+ * Create user cookies (ID and activation flag).
270
+ * These are user-state cookies, NOT auth credentials.
271
+ *
268
272
  * @param stream The request stream.
269
273
  * @param uid The user ID, or invalid to clear.
270
274
  */
271
- _create_user_cookie(stream: Stream, uid: string): Promise<void>;
275
+ _create_user_cookie(stream: Stream, uid: string | null): Promise<void>;
272
276
  /**
273
- * Create non-HTTP-only cookies with detailed user info for the frontend.
277
+ * Create non-HttpOnly cookies with detailed user info for frontend usage.
278
+ * These are UI convenience cookies only.
279
+ *
274
280
  * @param stream The request stream.
275
281
  * @param uid The user ID.
276
282
  */
277
283
  _create_detailed_user_cookie(stream: Stream, uid: string): Promise<void>;
278
284
  /**
279
- * Clear all default auth/user cookies.
285
+ * Clear all default auth and user-related cookies.
286
+ *
280
287
  * @param stream The request stream.
281
288
  */
282
289
  _reset_cookies(stream: Stream): void;
@@ -431,61 +431,129 @@ export class Users {
431
431
  // ---------------------------------------------------------
432
432
  /**
433
433
  * Create the auth token cookie on the response.
434
+ * `T` is treated as a real authentication credential.
435
+ *
434
436
  * @param stream The request stream.
435
437
  * @param token The token string or Token object.
436
438
  */
437
439
  _create_token_cookie(stream, token) {
438
440
  stream.set_header("Cache-Control", "max-age=0, no-cache, no-store, must-revalidate, proxy-revalidate");
439
441
  stream.set_header("Access-Control-Allow-Credentials", "true");
440
- if (typeof token === "object") {
441
- token = token.token;
442
- }
442
+ const token_value = typeof token === "object" ? token.token : token;
443
443
  const max_age = this.token_expiration; // seconds
444
- const expires = new Date(Date.now() + max_age * 1000).toUTCString();
445
- stream.set_cookie(`T=${encodeURIComponent(token ?? "")}; Max-Age=${max_age}; Path=/; Expires=${expires}; SameSite=Strict; Secure; HttpOnly;`);
444
+ stream.set_cookie({
445
+ name: "T",
446
+ value: token_value,
447
+ path: "/",
448
+ max_age,
449
+ secure: true,
450
+ http_only: true,
451
+ same_site: "Lax", // REQUIRED for Stripe success/cancel redirects
452
+ });
446
453
  }
447
454
  /**
448
- * Create user cookies (id and activation flag).
455
+ * Create user cookies (ID and activation flag).
456
+ * These are user-state cookies, NOT auth credentials.
457
+ *
449
458
  * @param stream The request stream.
450
459
  * @param uid The user ID, or invalid to clear.
451
460
  */
452
461
  async _create_user_cookie(stream, uid) {
453
- if (typeof uid === "string") {
454
- stream.set_cookie(`UserID=${encodeURIComponent(uid ?? "")}; Path=/; SameSite=Strict; Secure;`); // http only since we use this value for account activation without signin.
455
- const is_activated = this.enable_account_activation ? await this.is_activated(uid) : true;
456
- stream.set_cookie(`UserActivated=${is_activated}; Path=/; SameSite=Strict; Secure;`);
462
+ if (typeof uid === "string" && uid.length > 0) {
463
+ stream.set_cookie({
464
+ name: "UserID",
465
+ value: uid,
466
+ path: "/",
467
+ secure: true,
468
+ same_site: "Lax",
469
+ });
470
+ const is_activated = this.enable_account_activation
471
+ ? await this.is_activated(uid)
472
+ : true;
473
+ stream.set_cookie({
474
+ name: "UserActivated",
475
+ value: is_activated ? "1" : "0",
476
+ path: "/",
477
+ secure: true,
478
+ same_site: "Lax",
479
+ });
457
480
  }
458
481
  else {
459
- stream.set_cookie(`UserID=-1; Path=/; SameSite=Strict; Secure;`); // http only since we use this value for account activation without signin.
460
- const is_activated = this.enable_account_activation ? false : true;
461
- stream.set_cookie(`UserActivated=${is_activated}; Path=/; SameSite=Strict; Secure;`);
482
+ stream.set_cookie({
483
+ name: "UserID",
484
+ value: "",
485
+ path: "/",
486
+ max_age: 0,
487
+ secure: true,
488
+ same_site: "Lax",
489
+ });
490
+ stream.set_cookie({
491
+ name: "UserActivated",
492
+ value: "0",
493
+ path: "/",
494
+ max_age: 0,
495
+ secure: true,
496
+ same_site: "Lax",
497
+ });
462
498
  }
463
499
  }
464
500
  /**
465
- * Create non-HTTP-only cookies with detailed user info for the frontend.
501
+ * Create non-HttpOnly cookies with detailed user info for frontend usage.
502
+ * These are UI convenience cookies only.
503
+ *
466
504
  * @param stream The request stream.
467
505
  * @param uid The user ID.
468
506
  */
469
507
  async _create_detailed_user_cookie(stream, uid) {
470
508
  const user = await this.get(uid);
471
- stream.set_cookie(`UserName=${encodeURIComponent(user.username ?? "")}; Path=/; SameSite=Strict; Secure;`);
472
- stream.set_cookie(`UserFirstName=${encodeURIComponent(user.first_name ?? "")}; Path=/; SameSite=Strict; Secure;`);
473
- stream.set_cookie(`UserLastName=${encodeURIComponent(user.last_name ?? "")}; Path=/; SameSite=Strict; Secure;`);
474
- stream.set_cookie(`UserEmail=${encodeURIComponent(user.email ?? "")}; Path=/; SameSite=Strict; Secure;`);
509
+ stream.set_cookie({
510
+ name: "UserName",
511
+ value: user.username ?? "",
512
+ path: "/",
513
+ secure: true,
514
+ same_site: "Lax",
515
+ });
516
+ stream.set_cookie({
517
+ name: "UserFirstName",
518
+ value: user.first_name ?? "",
519
+ path: "/",
520
+ secure: true,
521
+ same_site: "Lax",
522
+ });
523
+ stream.set_cookie({
524
+ name: "UserLastName",
525
+ value: user.last_name ?? "",
526
+ path: "/",
527
+ secure: true,
528
+ same_site: "Lax",
529
+ });
530
+ stream.set_cookie({
531
+ name: "UserEmail",
532
+ value: user.email ?? "",
533
+ path: "/",
534
+ secure: true,
535
+ same_site: "Lax",
536
+ });
475
537
  }
476
538
  /**
477
- * Clear all default auth/user cookies.
539
+ * Clear all default auth and user-related cookies.
540
+ *
478
541
  * @param stream The request stream.
479
542
  */
480
543
  _reset_cookies(stream) {
481
- const past = "Thu, 01 Jan 1970 00:00:00 GMT";
482
- stream.set_cookie(`T=; Max-Age=0; Path=/; Expires=${past}; SameSite=Strict; Secure; HttpOnly;`);
483
- stream.set_cookie(`UserID=; Max-Age=0; Path=/; Expires=${past}; SameSite=Strict; Secure;`); // http only since we use this value for account activation without signin.
484
- stream.set_cookie(`UserActivated=; Max-Age=0; Path=/; Expires=${past}; SameSite=Strict; Secure;`);
485
- stream.set_cookie(`UserName=; Max-Age=0; Path=/; Expires=${past}; SameSite=Strict; Secure;`);
486
- stream.set_cookie(`UserFirstName=; Max-Age=0; Path=/; Expires=${past}; SameSite=Strict; Secure;`);
487
- stream.set_cookie(`UserLastName=; Max-Age=0; Path=/; Expires=${past}; SameSite=Strict; Secure;`);
488
- stream.set_cookie(`UserEmail=; Max-Age=0; Path=/; Expires=${past}; SameSite=Strict; Secure;`);
544
+ const clear = {
545
+ path: "/",
546
+ max_age: 0,
547
+ secure: true,
548
+ same_site: "Lax",
549
+ };
550
+ stream.set_cookie({ name: "T", value: "", http_only: true, ...clear });
551
+ stream.set_cookie({ name: "UserID", value: "", ...clear });
552
+ stream.set_cookie({ name: "UserActivated", value: "", ...clear });
553
+ stream.set_cookie({ name: "UserName", value: "", ...clear });
554
+ stream.set_cookie({ name: "UserFirstName", value: "", ...clear });
555
+ stream.set_cookie({ name: "UserLastName", value: "", ...clear });
556
+ stream.set_cookie({ name: "UserEmail", value: "", ...clear });
489
557
  }
490
558
  // ---------------------------------------------------------
491
559
  // 2FA mail.
@@ -17,7 +17,7 @@ export var User;
17
17
  */
18
18
  function uid() {
19
19
  const uid = Cookies.get("UserID");
20
- return typeof uid !== "string" || uid == "-1" ? undefined : uid;
20
+ return typeof uid !== "string" || uid == "-1" || uid === "" ? undefined : uid;
21
21
  }
22
22
  User.uid = uid;
23
23
  /**
@@ -87,7 +87,8 @@ export var User;
87
87
  * @docs
88
88
  */
89
89
  function is_activated() {
90
- return Cookies.get("UserActivated") === "true";
90
+ const activated = Cookies.get("UserActivated");
91
+ return activated === "true" || activated === "1";
91
92
  }
92
93
  User.is_activated = is_activated;
93
94
  /**
@@ -71,7 +71,7 @@ export declare class TableElement extends VElementTagMap.div {
71
71
  table_body: TableBodyElement;
72
72
  constructor({ rows, columns, show_columns, }: {
73
73
  rows?: AppendType[][];
74
- columns: AppendType[] | boolean;
74
+ columns?: AppendType[];
75
75
  show_columns?: boolean;
76
76
  });
77
77
  iterate(callback: any): this;
@@ -89,7 +89,7 @@ export declare class TableElement extends VElementTagMap.div {
89
89
  }
90
90
  export declare const Table: <Extensions extends object = {}>(args_0: {
91
91
  rows?: AppendType[][];
92
- columns: AppendType[] | boolean;
92
+ columns?: AppendType[];
93
93
  show_columns?: boolean;
94
94
  }) => TableElement & Extensions;
95
95
  export declare const NullTable: <Extensions extends object = {}>() => TableElement & Extensions;
@@ -285,19 +285,16 @@ let TableElement = (() => {
285
285
  table_head = NullTableHead();
286
286
  table_body = NullTableBody();
287
287
  // Constructor.
288
- constructor({ rows = [
289
- ["a", "b"],
290
- ["c", "d"],
291
- ], columns = ["Column 1", "Column 2"], show_columns = true, }) {
288
+ constructor({ rows = [], columns = [], show_columns = true, }) {
292
289
  // Initialize base class.
293
290
  super({
294
291
  derived: TableElement,
295
292
  });
296
293
  // Attributes.
297
294
  this.table_rows = [];
298
- this.show_columns = show_columns === true && columns !== false;
295
+ this.show_columns = show_columns === true && columns.length > 0;
299
296
  // Append content.
300
- this.create({ rows, columns: columns === false ? [] : columns });
297
+ this.create({ rows, columns });
301
298
  this.overflow("hidden"); // without overflow hidden when border radius is set on the element the background of rows can overflow.
302
299
  }
303
300
  // Iterate the table data items.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "author": "Daan van den Bergh",
3
3
  "name": "@vandenberghinc/volt",
4
- "version": "1.2.2",
4
+ "version": "1.2.4",
5
5
  "description": "",
6
6
  "type": "module",
7
7
  "types": "./backend/dist/esm/index.d.ts",
@@ -35,7 +35,7 @@
35
35
  },
36
36
  "dependencies": {
37
37
  "@vandenberghinc/vhighlight": "^1.3.17",
38
- "@vandenberghinc/vlib": "^1.6.42",
38
+ "@vandenberghinc/vlib": "^1.6.46",
39
39
  "blob-stream": "^0.1.3",
40
40
  "clean-css": "^5.3.3",
41
41
  "esbuild": "^0.25.12",