@vandenberghinc/volt 1.2.3 → 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 +1 -1
@@ -159,6 +159,23 @@ export declare class Collection<Data extends mongodb.Document = mongodb.Document
159
159
  * - Plain object without '$' keys → NOT valid for updateOne/findOneAndUpdate.
160
160
  */
161
161
  private _is_operator_update_or_pipeline;
162
+ private _index_key_signature;
163
+ private _keys_equal;
164
+ private _normalize_index_opts;
165
+ /**
166
+ * Drop all indexes that are NOT part of this._init_indexes, excluding _id_ (and TTL index if enabled).
167
+ *
168
+ * @note We match by key pattern rather than name because names can differ.
169
+ */
170
+ private _drop_non_init_indexes;
171
+ /**
172
+ * Creates indexes on collections.
173
+ *
174
+ * @note When transaction mode is enabled, the session option will not be used.
175
+ *
176
+ * @param opts The index create options.
177
+ */
178
+ private _create_index;
162
179
  /**
163
180
  * Initialize the collection, creating indexes and setting up TTL if needed.
164
181
  * @returns The initialized collection instance.
@@ -215,16 +232,6 @@ export declare class Collection<Data extends mongodb.Document = mongodb.Document
215
232
  * @docs
216
233
  */
217
234
  has_index(index: string): Promise<boolean>;
218
- /**
219
- * Creates indexes on collections.
220
- *
221
- * @note When transaction mode is enabled, the session option will not be used.
222
- *
223
- * @param opts The index create options.
224
- *
225
- * @docs
226
- */
227
- create_index(opts: string | Collection.IndexOpts): Promise<string>;
228
235
  /**
229
236
  * Standalone helper: merge `source` into `target` for missing keys only.
230
237
  * Clones assigned nested objects/arrays/dates once (when `clone` is true).
@@ -462,6 +462,293 @@ class Collection {
462
462
  _is_operator_update_or_pipeline(operation) {
463
463
  return Array.isArray(operation) || operation && typeof operation === "object" && Object.keys(operation).some((k) => k[0] === "$");
464
464
  }
465
+ _index_key_signature(keys) {
466
+ return Object.entries(keys).map(([k, v]) => `${k}:${v}`).join("|");
467
+ }
468
+ _keys_equal(a, b) {
469
+ const aEnt = Object.entries(a);
470
+ const bEnt = Object.entries(b);
471
+ if (aEnt.length !== bEnt.length)
472
+ return false;
473
+ for (let i = 0; i < aEnt.length; i++) {
474
+ const [ak, av] = aEnt[i];
475
+ const [bk, bv] = bEnt[i];
476
+ if (ak !== bk || av !== bv)
477
+ return false;
478
+ }
479
+ return true;
480
+ }
481
+ _normalize_index_opts(opts) {
482
+ let key;
483
+ let keys;
484
+ let options;
485
+ let unique;
486
+ let sparse;
487
+ let forced = false;
488
+ if (typeof opts === "string") {
489
+ key = opts;
490
+ unique = void 0;
491
+ sparse = void 0;
492
+ } else {
493
+ ({ key, keys, forced = false } = opts);
494
+ const user_options = opts.options;
495
+ options = user_options ? { ...user_options } : void 0;
496
+ if (opts.unique != null && options?.unique != null && opts.unique !== options.unique) {
497
+ throw new import_errors.InvalidUsageError({
498
+ message: `Encountered different values for attribute 'unique': ${opts.unique} and 'options.unique': ${options.unique}.`,
499
+ reason: "invalid_unique_option"
500
+ });
501
+ }
502
+ unique = opts.unique ?? options?.unique;
503
+ if (opts.sparse != null && options?.sparse != null && opts.sparse !== options.sparse) {
504
+ throw new import_errors.InvalidUsageError({
505
+ message: `Encountered different values for attribute 'sparse': ${opts.sparse} and 'options.sparse': ${options.sparse}.`,
506
+ reason: "invalid_sparse_option"
507
+ });
508
+ }
509
+ sparse = opts.sparse ?? options?.sparse;
510
+ }
511
+ if (unique != null) {
512
+ options = options || {};
513
+ options.unique = unique;
514
+ }
515
+ if (sparse != null) {
516
+ options = options || {};
517
+ options.sparse = sparse;
518
+ }
519
+ let keys_obj;
520
+ if (key) {
521
+ keys_obj = { [key]: 1 };
522
+ } else if (Array.isArray(keys) && keys.length > 0) {
523
+ keys_obj = {};
524
+ for (const k of keys)
525
+ keys_obj[k] = 1;
526
+ } else if (keys != null && typeof keys === "object") {
527
+ keys_obj = keys;
528
+ } else {
529
+ throw new import_errors.InvalidUsageError({
530
+ message: "Define one of the following parameters: [key, keys].",
531
+ reason: "invalid_index_definition"
532
+ });
533
+ }
534
+ return { keys_obj, options, forced };
535
+ }
536
+ /**
537
+ * Drop all indexes that are NOT part of this._init_indexes, excluding _id_ (and TTL index if enabled).
538
+ *
539
+ * @note We match by key pattern rather than name because names can differ.
540
+ */
541
+ async _drop_non_init_indexes() {
542
+ this.assert_not_transaction_based();
543
+ this.assert_init();
544
+ const existing = await this._col.listIndexes().toArray();
545
+ const desired = /* @__PURE__ */ new Set();
546
+ for (const item of this._init_indexes ?? []) {
547
+ const { keys_obj } = this._normalize_index_opts(item);
548
+ desired.add(this._index_key_signature(keys_obj));
549
+ }
550
+ const protected_names = /* @__PURE__ */ new Set(["_id_"]);
551
+ const keep_ttl = this.ttl_enabled;
552
+ const ttl_sig = this._index_key_signature({ __ttl_timestamp: 1 });
553
+ for (const ix of existing) {
554
+ const name = ix?.name;
555
+ if (!name)
556
+ continue;
557
+ if (protected_names.has(name))
558
+ continue;
559
+ const keyObj = ix.key;
560
+ if (!keyObj)
561
+ continue;
562
+ const sig = this._index_key_signature(keyObj);
563
+ if (desired.has(sig))
564
+ continue;
565
+ if (keep_ttl && sig === ttl_sig)
566
+ continue;
567
+ try {
568
+ this.db.server.log(3, `Dropping stale index "${name}" on collection: ${this.name}`);
569
+ await this._col.dropIndex(name);
570
+ } catch (err) {
571
+ if (err?.codeName !== "IndexNotFound")
572
+ throw err;
573
+ }
574
+ }
575
+ }
576
+ /**
577
+ * Creates indexes on collections.
578
+ *
579
+ * @note When transaction mode is enabled, the session option will not be used.
580
+ *
581
+ * @param opts The index create options.
582
+ */
583
+ async _create_index(opts) {
584
+ this.assert_not_transaction_based();
585
+ if (!this.initialized) {
586
+ await this.init();
587
+ }
588
+ this.assert_init();
589
+ const { keys_obj, options, forced } = this._normalize_index_opts(opts);
590
+ const drop_index = async () => {
591
+ const existing = await this._col.listIndexes().toArray();
592
+ const match = existing.find((ix) => {
593
+ const ix_key = ix?.key;
594
+ if (!ix_key)
595
+ return false;
596
+ return this._keys_equal(ix_key, keys_obj);
597
+ });
598
+ if (match?.name) {
599
+ try {
600
+ await this._col.dropIndex(match.name);
601
+ } catch (err) {
602
+ if (err?.codeName !== "IndexNotFound")
603
+ throw err;
604
+ }
605
+ return;
606
+ }
607
+ if (options?.name) {
608
+ try {
609
+ await this._col.dropIndex(options.name);
610
+ } catch (err) {
611
+ if (err?.codeName !== "IndexNotFound")
612
+ throw err;
613
+ }
614
+ return;
615
+ }
616
+ const synthesized = Object.entries(keys_obj).map(([k, v]) => `${k}_${v}`).join("_");
617
+ try {
618
+ await this._col.dropIndex(synthesized);
619
+ } catch (err) {
620
+ if (err?.codeName !== "IndexNotFound")
621
+ throw err;
622
+ }
623
+ };
624
+ try {
625
+ try {
626
+ return await this._col.createIndex(keys_obj, options);
627
+ } catch (err) {
628
+ if (forced && err && typeof err === "object" && err.codeName === "IndexKeySpecsConflict") {
629
+ await drop_index();
630
+ return await this._col.createIndex(keys_obj, options);
631
+ }
632
+ throw err;
633
+ }
634
+ } catch (err) {
635
+ throw new Error(`Failed to create index on collection "${this.name}": ${err}`, { cause: err });
636
+ }
637
+ }
638
+ // async create_index(opts: string | Collection.IndexOpts): Promise<string> {
639
+ // // Not supported on transaction-based collections.
640
+ // this.assert_not_transaction_based();
641
+ // // Ensure initialized
642
+ // if (!this.initialized) { await this.init(); } this.assert_init();
643
+ // // ---- Normalize inputs ----
644
+ // let key: string | undefined;
645
+ // let keys: string[] | Record<string, number> | undefined;
646
+ // let options: mongodb.CreateIndexesOptions | undefined;
647
+ // let unique: boolean | undefined;
648
+ // let sparse: boolean | undefined;
649
+ // let forced = false;
650
+ // if (typeof opts === "string") {
651
+ // key = opts;
652
+ // unique = undefined;
653
+ // sparse = undefined;
654
+ // } else {
655
+ // ({ key, keys, forced = false } = opts);
656
+ // const options = opts.options as unknown as undefined | mongodb.CreateIndexesOptions;
657
+ // // Conflict guard between `unique` and `options.unique`
658
+ // if (opts.unique != null && options?.unique != null && opts.unique !== options.unique) {
659
+ // throw new InvalidUsageError({
660
+ // message: `Encountered different values for attribute 'unique': ${opts.unique} and 'options.unique': ${options.unique}.`,
661
+ // reason: "invalid_unique_option",
662
+ // });
663
+ // }
664
+ // unique = opts.unique ?? options?.unique;
665
+ // // Conflict guard between `sparse` and `options.sparse`
666
+ // if (opts.sparse != null && options?.sparse != null && opts.sparse !== options.sparse) {
667
+ // throw new InvalidUsageError({
668
+ // message: `Encountered different values for attribute 'sparse': ${opts.sparse} and 'options.sparse': ${options.sparse}.`,
669
+ // reason: "invalid_sparse_option",
670
+ // });
671
+ // }
672
+ // sparse = opts.sparse ?? options?.sparse;
673
+ // }
674
+ // // Ensure `unique` in options when provided
675
+ // if (unique) {
676
+ // options = options || {};
677
+ // options.unique = unique;
678
+ // }
679
+ // // Ensure `sparse` in options when provided
680
+ // if (sparse) {
681
+ // options = options || {};
682
+ // options.sparse = sparse;
683
+ // }
684
+ // // Build keys object
685
+ // let keys_obj: Record<string, number>;
686
+ // if (key) {
687
+ // keys_obj = { [key]: 1 };
688
+ // } else if (Array.isArray(keys) && keys.length > 0) {
689
+ // keys_obj = {};
690
+ // for (const k of keys) keys_obj[k] = 1;
691
+ // } else if (keys != null && typeof keys === "object") {
692
+ // keys_obj = keys as Record<string, number>;
693
+ // } else {
694
+ // throw new InvalidUsageError({
695
+ // message: "Define one of the following parameters: [key, keys].",
696
+ // reason: "invalid_index_definition",
697
+ // });
698
+ // }
699
+ // const drop_index = async () => {
700
+ // try {
701
+ // const existing = await this._col.listIndexes().toArray();
702
+ // const match = existing.find(ix => {
703
+ // const ix_key = ix?.key as Record<string, number> | undefined;
704
+ // if (!ix_key) return false;
705
+ // const a = Object.entries(ix_key);
706
+ // const b = Object.entries(keys_obj);
707
+ // if (a.length !== b.length) return false;
708
+ // // exact key-value equality (order-insensitive)
709
+ // const as = new Map(a);
710
+ // for (const [kk, vv] of b) {
711
+ // if (as.get(kk) !== vv) return false;
712
+ // }
713
+ // return true;
714
+ // });
715
+ // // Prefer matched key's real name
716
+ // if (match?.name) {
717
+ // try { await this._col.dropIndex(match.name); }
718
+ // catch (err: any) { if (err?.codeName !== "IndexNotFound") throw err; }
719
+ // } else if (options?.name) {
720
+ // try { await this._col.dropIndex(options.name); }
721
+ // catch (err: any) { if (err?.codeName !== "IndexNotFound") throw err; }
722
+ // } else {
723
+ // // last-resort synthesized name (simple cases)
724
+ // const synthesized = Object.entries(keys_obj).map(([k, v]) => `${k}_${v}`).join("_");
725
+ // try { await this._col.dropIndex(synthesized); }
726
+ // catch (err: any) { if (err?.codeName !== "IndexNotFound") throw err; }
727
+ // }
728
+ // } catch (err) {
729
+ // // If listIndexes itself fails for some reason, do not hide the error
730
+ // throw new Error(`Failed to create index on collection "${this.name}": ${err}`, { cause: err });
731
+ // }
732
+ // }
733
+ // try {
734
+ // // Create (or re-create)
735
+ // try {
736
+ // return await this._col.createIndex(keys_obj, options);
737
+ // }
738
+ // // Retry once on IndexKeySpecsConflict when forced=true
739
+ // catch (err) {
740
+ // if (forced && err && typeof err === "object" && (
741
+ // (err as any).codeName === "IndexKeySpecsConflict"
742
+ // )) {
743
+ // await drop_index();
744
+ // return await this._col.createIndex(keys_obj, options);
745
+ // }
746
+ // throw err;
747
+ // }
748
+ // } catch (err) {
749
+ // throw new Error(`Failed to create index on collection "${this.name}": ${err}`, { cause: err });
750
+ // }
751
+ // }
465
752
  // -------------------------------------------------------------------
466
753
  // Public methods.
467
754
  // -------------------------------------------------------------------
@@ -524,10 +811,11 @@ class Collection {
524
811
  this.db.server.log(3, "Setting up TTL index for collection: " + this.name);
525
812
  await this._setup_ttl();
526
813
  }
814
+ await this._drop_non_init_indexes();
527
815
  if (this._init_indexes?.length) {
528
816
  for (const item of this._init_indexes) {
529
817
  this.db.server.log(3, "Creating index " + JSON.stringify(item) + " on collection: " + this.name);
530
- await this.create_index(item);
818
+ await this._create_index(item);
531
819
  }
532
820
  }
533
821
  } else {
@@ -631,131 +919,6 @@ class Collection {
631
919
  this.assert_not_transaction_based();
632
920
  return (await this._col.listIndexes().toArray()).some((x) => x.name === index);
633
921
  }
634
- /**
635
- * Creates indexes on collections.
636
- *
637
- * @note When transaction mode is enabled, the session option will not be used.
638
- *
639
- * @param opts The index create options.
640
- *
641
- * @docs
642
- */
643
- async create_index(opts) {
644
- this.assert_not_transaction_based();
645
- if (!this.initialized) {
646
- await this.init();
647
- }
648
- this.assert_init();
649
- let key;
650
- let keys;
651
- let options;
652
- let unique;
653
- let sparse;
654
- let forced = false;
655
- if (typeof opts === "string") {
656
- key = opts;
657
- unique = void 0;
658
- sparse = void 0;
659
- } else {
660
- ({ key, keys, forced = false } = opts);
661
- const options2 = opts.options;
662
- if (opts.unique != null && options2?.unique != null && opts.unique !== options2.unique) {
663
- throw new import_errors.InvalidUsageError({
664
- message: `Encountered different values for attribute 'unique': ${opts.unique} and 'options.unique': ${options2.unique}.`,
665
- reason: "invalid_unique_option"
666
- });
667
- }
668
- unique = opts.unique ?? options2?.unique;
669
- if (opts.sparse != null && options2?.sparse != null && opts.sparse !== options2.sparse) {
670
- throw new import_errors.InvalidUsageError({
671
- message: `Encountered different values for attribute 'sparse': ${opts.sparse} and 'options.sparse': ${options2.sparse}.`,
672
- reason: "invalid_sparse_option"
673
- });
674
- }
675
- sparse = opts.sparse ?? options2?.sparse;
676
- }
677
- if (unique) {
678
- options = options || {};
679
- options.unique = unique;
680
- }
681
- if (sparse) {
682
- options = options || {};
683
- options.sparse = sparse;
684
- }
685
- let keys_obj;
686
- if (key) {
687
- keys_obj = { [key]: 1 };
688
- } else if (Array.isArray(keys) && keys.length > 0) {
689
- keys_obj = {};
690
- for (const k of keys)
691
- keys_obj[k] = 1;
692
- } else if (keys != null && typeof keys === "object") {
693
- keys_obj = keys;
694
- } else {
695
- throw new import_errors.InvalidUsageError({
696
- message: "Define one of the following parameters: [key, keys].",
697
- reason: "invalid_index_definition"
698
- });
699
- }
700
- const drop_index = async () => {
701
- try {
702
- const existing = await this._col.listIndexes().toArray();
703
- const match = existing.find((ix) => {
704
- const ix_key = ix?.key;
705
- if (!ix_key)
706
- return false;
707
- const a = Object.entries(ix_key);
708
- const b = Object.entries(keys_obj);
709
- if (a.length !== b.length)
710
- return false;
711
- const as = new Map(a);
712
- for (const [kk, vv] of b) {
713
- if (as.get(kk) !== vv)
714
- return false;
715
- }
716
- return true;
717
- });
718
- if (match?.name) {
719
- try {
720
- await this._col.dropIndex(match.name);
721
- } catch (err) {
722
- if (err?.codeName !== "IndexNotFound")
723
- throw err;
724
- }
725
- } else if (options?.name) {
726
- try {
727
- await this._col.dropIndex(options.name);
728
- } catch (err) {
729
- if (err?.codeName !== "IndexNotFound")
730
- throw err;
731
- }
732
- } else {
733
- const synthesized = Object.entries(keys_obj).map(([k, v]) => `${k}_${v}`).join("_");
734
- try {
735
- await this._col.dropIndex(synthesized);
736
- } catch (err) {
737
- if (err?.codeName !== "IndexNotFound")
738
- throw err;
739
- }
740
- }
741
- } catch (err) {
742
- throw new Error(`Failed to create index on collection "${this.name}": ${err}`, { cause: err });
743
- }
744
- };
745
- try {
746
- try {
747
- return await this._col.createIndex(keys_obj, options);
748
- } catch (err) {
749
- if (forced && err && typeof err === "object" && err.codeName === "IndexKeySpecsConflict") {
750
- await drop_index();
751
- return await this._col.createIndex(keys_obj, options);
752
- }
753
- throw err;
754
- }
755
- } catch (err) {
756
- throw new Error(`Failed to create index on collection "${this.name}": ${err}`, { cause: err });
757
- }
758
- }
759
922
  /**
760
923
  * Standalone helper: merge `source` into `target` for missing keys only.
761
924
  * Clones assigned nested objects/arrays/dates once (when `clone` is true).
@@ -123,7 +123,8 @@ async function list_all_stripe_products(client) {
123
123
  for (; ; ) {
124
124
  const page = await (0, import_utils.stripe_api_call)(() => client.products.list({
125
125
  limit: stripe_list_page_size,
126
- starting_after
126
+ starting_after,
127
+ expand: ["data.default_price"]
127
128
  }), { operation: "products.list_all", starting_after });
128
129
  all_products.push(...page.data);
129
130
  if (!page.has_more || page.data.length === 0) {
@@ -239,7 +240,8 @@ async function create_stripe_product(client, server, product) {
239
240
  images: product.images,
240
241
  metadata: {
241
242
  [app_product_id_metadata_key]: product.id
242
- }
243
+ },
244
+ expand: ["default_price"]
243
245
  }, { idempotencyKey: (0, import_utils.generate_random_idempotency_key)(`create_product_${product.id}`) }), { operation: "products.create", app_product_id: product.id });
244
246
  }
245
247
  async function update_stripe_product_if_needed(client, server, stripe_product, product) {
@@ -255,11 +257,24 @@ async function update_stripe_product_if_needed(client, server, stripe_product, p
255
257
  name: product.name,
256
258
  description: product.description,
257
259
  tax_code: product.tax_code,
258
- images: product.images
260
+ images: product.images,
261
+ expand: ["default_price"]
259
262
  }, { idempotencyKey: (0, import_utils.generate_random_idempotency_key)(`update_product_${product.id}_${stripe_product.id}`) }), { operation: "products.update", app_product_id: product.id, stripe_product_id: stripe_product.id });
260
263
  }
261
- async function update_stripe_product_default_price_if_needed(client, server, stripe_product, default_price) {
262
- if (stripe_product.default_price === default_price.id) {
264
+ async function update_stripe_product_default_price_if_needed(client, server, stripe_product, default_price, other_plans_from_parent_subscription) {
265
+ let default_price_id = null;
266
+ if (typeof stripe_product.default_price === "string") {
267
+ default_price_id = stripe_product.default_price;
268
+ } else if (stripe_product.default_price && typeof stripe_product.default_price === "object") {
269
+ default_price_id = stripe_product.default_price.id;
270
+ }
271
+ if (!default_price_id) {
272
+ const fetched = await (0, import_utils.stripe_api_call)(() => client.products.retrieve(stripe_product.id, {
273
+ expand: ["default_price"]
274
+ }), { operation: "products.retrieve_for_default_price", stripe_product_id: stripe_product.id });
275
+ default_price_id = typeof fetched.default_price === "string" ? fetched.default_price : fetched.default_price?.id ?? null;
276
+ }
277
+ if (default_price_id === default_price.id || other_plans_from_parent_subscription?.some((plan) => plan.stripe_price_id === default_price_id)) {
263
278
  return;
264
279
  }
265
280
  server.log(0, `Updating default price for Stripe product '${stripe_product.id}' to price '${default_price.id}'`);
@@ -526,7 +541,7 @@ async function initialize_product(client, server, product, stripe_products_by_ap
526
541
  const updated_prices = [...active_prices, stripe_price];
527
542
  active_prices_by_stripe_product_id.set(stripe_product.id, updated_prices);
528
543
  }
529
- await update_stripe_product_default_price_if_needed(client, server, stripe_product, stripe_price);
544
+ await update_stripe_product_default_price_if_needed(client, server, stripe_product, stripe_price, initialized_plans);
530
545
  initialized_plans.push({
531
546
  ...plan,
532
547
  type: "subscription_plan",
@@ -362,17 +362,69 @@ export declare class Stream {
362
362
  */
363
363
  remove_headers(...names: string[]): this;
364
364
  /**
365
- * Set a cookie that will be sent with the response.
365
+ * Set a cookie to be sent with the response.
366
+ *
367
+ * Accepts either:
368
+ * 1) a pre-built cookie header string (used as-is, no validation), or
369
+ * 2) a structured object describing the cookie, from which a standards-compliant
370
+ * cookie string will be generated.
371
+ *
372
+ * If a cookie with the same name already exists in the pending response list,
373
+ * it will be replaced.
374
+ *
375
+ * @warning Cookies are only included in the response when using `send()`,
376
+ * `success()` or `error()`.
366
377
  *
367
- * @warning Will only be added to the response when the user uses `send()`, `success()` or `error()`.
368
- * @param cookie The cookie string.
369
378
  * @example
370
379
  * ```ts
371
- * stream.set_cookie("MyCookie=Hello World;");
380
+ * stream.set_cookie("sid=abc123; Path=/; SameSite=Lax; Secure; HttpOnly");
381
+ *
382
+ * stream.set_cookie({
383
+ * name: "sid",
384
+ * value: session_id,
385
+ * http_only: true,
386
+ * secure: true,
387
+ * same_site: "Lax",
388
+ * path: "/",
389
+ * max_age: 60 * 60 * 24 * 14,
390
+ * });
372
391
  * ```
373
- * @docs
374
392
  */
375
- set_cookie(cookie: string): this;
393
+ set_cookie(cookie: string | {
394
+ /** Cookie name (required). */
395
+ name: string;
396
+ /** Cookie value. Will be URI-encoded. Defaults to empty string. */
397
+ value?: string | number | boolean | null;
398
+ /** Cookie path attribute. Defaults to "/". */
399
+ path?: string;
400
+ /** Cookie domain attribute. */
401
+ domain?: string;
402
+ /** Max-Age in seconds. Must be a finite number. */
403
+ max_age?: number;
404
+ /** Expiration date (Date or preformatted HTTP date string). */
405
+ expires?: Date | string;
406
+ /** Adds the Secure attribute (HTTPS only). */
407
+ secure?: boolean;
408
+ /** Adds the HttpOnly attribute (not accessible to JS). */
409
+ http_only?: boolean;
410
+ /**
411
+ * SameSite attribute.
412
+ * Use "Lax" for most session cookies.
413
+ * Use "None" only together with `secure: true`.
414
+ */
415
+ same_site?: "Strict" | "Lax" | "None";
416
+ /**
417
+ * Cookie name prefix.
418
+ * "__Host-" requires: secure=true, path="/", and no domain.
419
+ * "__Secure-" requires: secure=true.
420
+ */
421
+ prefix?: "__Host-" | "__Secure-";
422
+ /**
423
+ * Additional raw attributes appended verbatim.
424
+ * Example: ["Priority=High"]
425
+ */
426
+ extra?: string[];
427
+ }): this;
376
428
  /**
377
429
  * Set cookies that will be sent with the response.
378
430
  *