@puzzle-ui/session 0.0.0-stage → 0.5.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.
Files changed (61) hide show
  1. package/README.md +143 -2
  2. package/dist/LICENSE +21 -0
  3. package/dist/errors/session.errors.cjs +47 -0
  4. package/dist/errors/session.errors.cjs.map +1 -0
  5. package/dist/errors/session.errors.d.cts +19 -0
  6. package/dist/errors/session.errors.d.ts +19 -0
  7. package/dist/errors/session.errors.js +42 -0
  8. package/dist/errors/session.errors.js.map +1 -0
  9. package/dist/index.cjs +32 -0
  10. package/dist/index.d.cts +12 -0
  11. package/dist/index.d.ts +12 -0
  12. package/dist/index.js +9 -0
  13. package/dist/session-data.manager.cjs +141 -0
  14. package/dist/session-data.manager.cjs.map +1 -0
  15. package/dist/session-data.manager.d.cts +26 -0
  16. package/dist/session-data.manager.d.ts +26 -0
  17. package/dist/session-data.manager.js +141 -0
  18. package/dist/session-data.manager.js.map +1 -0
  19. package/dist/session.cookies.cjs +64 -0
  20. package/dist/session.cookies.cjs.map +1 -0
  21. package/dist/session.cookies.d.cts +10 -0
  22. package/dist/session.cookies.d.ts +10 -0
  23. package/dist/session.cookies.js +61 -0
  24. package/dist/session.cookies.js.map +1 -0
  25. package/dist/session.csrf.cjs +42 -0
  26. package/dist/session.csrf.cjs.map +1 -0
  27. package/dist/session.csrf.d.cts +6 -0
  28. package/dist/session.csrf.d.ts +6 -0
  29. package/dist/session.csrf.js +41 -0
  30. package/dist/session.csrf.js.map +1 -0
  31. package/dist/session.manager.cjs +289 -0
  32. package/dist/session.manager.cjs.map +1 -0
  33. package/dist/session.manager.d.cts +25 -0
  34. package/dist/session.manager.d.ts +25 -0
  35. package/dist/session.manager.js +289 -0
  36. package/dist/session.manager.js.map +1 -0
  37. package/dist/session.migration.cjs +163 -0
  38. package/dist/session.migration.cjs.map +1 -0
  39. package/dist/session.migration.d.cts +50 -0
  40. package/dist/session.migration.d.ts +50 -0
  41. package/dist/session.migration.js +163 -0
  42. package/dist/session.migration.js.map +1 -0
  43. package/dist/session.storage.cjs +64 -0
  44. package/dist/session.storage.cjs.map +1 -0
  45. package/dist/session.storage.d.cts +8 -0
  46. package/dist/session.storage.d.ts +8 -0
  47. package/dist/session.storage.js +58 -0
  48. package/dist/session.storage.js.map +1 -0
  49. package/dist/types/session-lock.types.d.cts +4 -0
  50. package/dist/types/session-lock.types.d.ts +4 -0
  51. package/dist/types/session-manager.types.d.cts +37 -0
  52. package/dist/types/session-manager.types.d.ts +37 -0
  53. package/dist/types/session.types.d.cts +51 -0
  54. package/dist/types/session.types.d.ts +51 -0
  55. package/dist/utils/session-crypto.cjs +29 -0
  56. package/dist/utils/session-crypto.cjs.map +1 -0
  57. package/dist/utils/session-crypto.d.cts +3 -0
  58. package/dist/utils/session-crypto.d.ts +3 -0
  59. package/dist/utils/session-crypto.js +27 -0
  60. package/dist/utils/session-crypto.js.map +1 -0
  61. package/package.json +43 -4
@@ -0,0 +1,141 @@
1
+ import { SESSION_DATA_COLLECTION, sessionDataCollection } from "./session.storage.js";
2
+ import { generateSessionId } from "./utils/session-crypto.js";
3
+ import { StorageConflictError, StorageDuplicateError } from "@puzzle-ui/storage";
4
+ // src/session-data.manager.ts
5
+ var SessionDataManager = class {
6
+ storage;
7
+ options;
8
+ clock;
9
+ maximumValueBytes;
10
+ maximumRetries;
11
+ constructor(storage, options) {
12
+ this.storage = storage;
13
+ this.options = options;
14
+ this.clock = options.clock ?? (() => /* @__PURE__ */ new Date());
15
+ this.maximumValueBytes = options.maximumValueBytes ?? 16384;
16
+ this.maximumRetries = options.maximumRetries ?? 5;
17
+ if (!options.allowedNamespaces.length) throw new Error("At least one session data namespace is required.");
18
+ if (!Number.isInteger(this.maximumValueBytes) || this.maximumValueBytes < 1) throw new Error("maximumValueBytes must be a positive integer.");
19
+ if (!Number.isInteger(this.maximumRetries) || this.maximumRetries < 1) throw new Error("maximumRetries must be a positive integer.");
20
+ if (options.defaultTtlSeconds !== void 0 && (!Number.isFinite(options.defaultTtlSeconds) || options.defaultTtlSeconds <= 0)) throw new Error("defaultTtlSeconds must be a positive number.");
21
+ }
22
+ async initialize() {
23
+ if (!this.storage.capabilities.uniqueConstraints) throw new Error("Session data requires unique storage constraints.");
24
+ await this.storage.registerCollection(sessionDataCollection);
25
+ }
26
+ assertOwner(ownerId) {
27
+ if (!ownerId.trim()) throw new Error("A valid session owner ID is required.");
28
+ }
29
+ assertNamespace(namespace) {
30
+ if (!this.options.allowedNamespaces.includes(namespace)) throw new Error(`Session data namespace is not allowed: ${namespace}`);
31
+ }
32
+ assertKey(key) {
33
+ if (!key.trim()) throw new Error("A valid session data key is required.");
34
+ }
35
+ assertValue(value) {
36
+ let serialized;
37
+ try {
38
+ serialized = JSON.stringify(value);
39
+ } catch {
40
+ throw new Error("Session data must be JSON serializable.");
41
+ }
42
+ if (serialized === void 0) throw new Error("Session data must be JSON serializable.");
43
+ if (new TextEncoder().encode(serialized).byteLength > this.maximumValueBytes) throw new Error("Session data exceeds the maximum allowed size.");
44
+ }
45
+ query(ownerId, namespace, key) {
46
+ return { where: {
47
+ ownerId: {
48
+ operator: "eq",
49
+ value: ownerId
50
+ },
51
+ namespace: {
52
+ operator: "eq",
53
+ value: namespace
54
+ },
55
+ key: {
56
+ operator: "eq",
57
+ value: key
58
+ }
59
+ } };
60
+ }
61
+ async set(ownerId, namespace, key, value, ttlSeconds = this.options.defaultTtlSeconds) {
62
+ this.assertOwner(ownerId);
63
+ this.assertNamespace(namespace);
64
+ this.assertKey(key);
65
+ this.assertValue(value);
66
+ if (ttlSeconds !== void 0 && (!Number.isFinite(ttlSeconds) || ttlSeconds <= 0)) throw new Error("TTL must be a positive number.");
67
+ for (let attempt = 0; attempt < this.maximumRetries; attempt++) {
68
+ const now = this.clock();
69
+ const expiresAt = ttlSeconds === void 0 ? null : new Date(now.getTime() + ttlSeconds * 1e3);
70
+ const existing = await this.storage.findOne(SESSION_DATA_COLLECTION, this.query(ownerId, namespace, key));
71
+ if (existing) {
72
+ try {
73
+ if (await this.storage.update("session_data", existing.id, {
74
+ value,
75
+ expiresAt
76
+ }, { expectedRevision: existing.revision })) return;
77
+ } catch (error) {
78
+ if (!(error instanceof StorageConflictError)) throw error;
79
+ }
80
+ continue;
81
+ }
82
+ try {
83
+ await this.storage.insert(SESSION_DATA_COLLECTION, {
84
+ id: generateSessionId(),
85
+ ownerId,
86
+ namespace,
87
+ key,
88
+ value,
89
+ expiresAt,
90
+ createdAt: now,
91
+ updatedAt: now,
92
+ revision: 1
93
+ });
94
+ return;
95
+ } catch (error) {
96
+ if (error instanceof StorageDuplicateError || error instanceof StorageConflictError) continue;
97
+ throw error;
98
+ }
99
+ }
100
+ throw new StorageConflictError("Could not write session data after repeated concurrent modifications.");
101
+ }
102
+ async get(ownerId, namespace, key) {
103
+ this.assertOwner(ownerId);
104
+ this.assertNamespace(namespace);
105
+ this.assertKey(key);
106
+ const record = await this.storage.findOne(SESSION_DATA_COLLECTION, this.query(ownerId, namespace, key));
107
+ if (!record) return null;
108
+ if (record.expiresAt && record.expiresAt.getTime() <= this.clock().getTime()) {
109
+ await this.storage.delete(SESSION_DATA_COLLECTION, record.id, { expectedRevision: record.revision }).catch((error) => {
110
+ if (!(error instanceof StorageConflictError)) throw error;
111
+ });
112
+ return null;
113
+ }
114
+ return record.value;
115
+ }
116
+ async remove(ownerId, namespace, key) {
117
+ this.assertOwner(ownerId);
118
+ this.assertNamespace(namespace);
119
+ this.assertKey(key);
120
+ for (let attempt = 0; attempt < this.maximumRetries; attempt++) {
121
+ const record = await this.storage.findOne(SESSION_DATA_COLLECTION, this.query(ownerId, namespace, key));
122
+ if (!record) return;
123
+ try {
124
+ if (await this.storage.delete("session_data", record.id, { expectedRevision: record.revision })) return;
125
+ } catch (error) {
126
+ if (!(error instanceof StorageConflictError)) throw error;
127
+ }
128
+ }
129
+ throw new StorageConflictError("Could not remove session data after repeated concurrent modifications.");
130
+ }
131
+ async removeExpired() {
132
+ const now = this.clock();
133
+ return this.storage.deleteMany(SESSION_DATA_COLLECTION, { where: { expiresAt: {
134
+ operator: "lte",
135
+ value: now
136
+ } } });
137
+ }
138
+ };
139
+ export { SessionDataManager };
140
+
141
+ //# sourceMappingURL=session-data.manager.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"session-data.manager.js","names":[],"sources":["../src/session-data.manager.ts"],"sourcesContent":["import {\n StorageConflictError,\n StorageDuplicateError,\n type StorageAdapter,\n} from \"@puzzle-ui/storage\";\n\nimport {\n SESSION_DATA_COLLECTION,\n sessionDataCollection,\n} from \"./session.storage\";\n\nimport type { SessionDataRecord } from \"./types/session.types\";\n\nimport { generateSessionId } from \"./utils/session-crypto\";\n\nexport type SessionDataOptions = {\n readonly allowedNamespaces: readonly string[];\n readonly defaultTtlSeconds?: number;\n readonly maximumValueBytes?: number;\n readonly maximumRetries?: number;\n readonly clock?: () => Date;\n};\n\nexport class SessionDataManager {\n private readonly clock: () => Date;\n private readonly maximumValueBytes: number;\n private readonly maximumRetries: number;\n\n constructor(\n private readonly storage: StorageAdapter,\n private readonly options: SessionDataOptions,\n ) {\n this.clock = options.clock ?? (() => new Date());\n this.maximumValueBytes = options.maximumValueBytes ?? 16_384;\n this.maximumRetries = options.maximumRetries ?? 5;\n\n if (!options.allowedNamespaces.length) {\n throw new Error(\"At least one session data namespace is required.\");\n }\n\n if (\n !Number.isInteger(this.maximumValueBytes) ||\n this.maximumValueBytes < 1\n ) {\n throw new Error(\"maximumValueBytes must be a positive integer.\");\n }\n\n if (!Number.isInteger(this.maximumRetries) || this.maximumRetries < 1) {\n throw new Error(\"maximumRetries must be a positive integer.\");\n }\n\n if (\n options.defaultTtlSeconds !== undefined &&\n (!Number.isFinite(options.defaultTtlSeconds) ||\n options.defaultTtlSeconds <= 0)\n ) {\n throw new Error(\"defaultTtlSeconds must be a positive number.\");\n }\n }\n\n async initialize(): Promise<void> {\n if (!this.storage.capabilities.uniqueConstraints) {\n throw new Error(\"Session data requires unique storage constraints.\");\n }\n\n await this.storage.registerCollection(sessionDataCollection);\n }\n\n private assertOwner(ownerId: string): void {\n if (!ownerId.trim()) {\n throw new Error(\"A valid session owner ID is required.\");\n }\n }\n\n private assertNamespace(namespace: string): void {\n if (!this.options.allowedNamespaces.includes(namespace)) {\n throw new Error(`Session data namespace is not allowed: ${namespace}`);\n }\n }\n\n private assertKey(key: string): void {\n if (!key.trim()) {\n throw new Error(\"A valid session data key is required.\");\n }\n }\n\n private assertValue(value: unknown): void {\n let serialized: string;\n\n try {\n serialized = JSON.stringify(value);\n } catch {\n throw new Error(\"Session data must be JSON serializable.\");\n }\n\n if (serialized === undefined) {\n throw new Error(\"Session data must be JSON serializable.\");\n }\n\n const size = new TextEncoder().encode(serialized).byteLength;\n\n if (size > this.maximumValueBytes) {\n throw new Error(\"Session data exceeds the maximum allowed size.\");\n }\n }\n\n private query(ownerId: string, namespace: string, key: string) {\n return {\n where: {\n ownerId: { operator: \"eq\" as const, value: ownerId },\n namespace: { operator: \"eq\" as const, value: namespace },\n key: { operator: \"eq\" as const, value: key },\n },\n };\n }\n\n async set(\n ownerId: string,\n namespace: string,\n key: string,\n value: unknown,\n ttlSeconds = this.options.defaultTtlSeconds,\n ): Promise<void> {\n this.assertOwner(ownerId);\n this.assertNamespace(namespace);\n this.assertKey(key);\n this.assertValue(value);\n\n if (\n ttlSeconds !== undefined &&\n (!Number.isFinite(ttlSeconds) || ttlSeconds <= 0)\n ) {\n throw new Error(\"TTL must be a positive number.\");\n }\n\n for (let attempt = 0; attempt < this.maximumRetries; attempt++) {\n const now = this.clock();\n\n const expiresAt =\n ttlSeconds === undefined\n ? null\n : new Date(now.getTime() + ttlSeconds * 1000);\n\n const existing = await this.storage.findOne<SessionDataRecord>(\n SESSION_DATA_COLLECTION,\n this.query(ownerId, namespace, key),\n );\n\n if (existing) {\n try {\n const updated = await this.storage.update<SessionDataRecord>(\n SESSION_DATA_COLLECTION,\n existing.id,\n {\n value,\n expiresAt,\n },\n {\n expectedRevision: existing.revision,\n },\n );\n\n if (updated) return;\n } catch (error) {\n if (!(error instanceof StorageConflictError)) {\n throw error;\n }\n }\n\n continue;\n }\n\n try {\n await this.storage.insert<SessionDataRecord>(SESSION_DATA_COLLECTION, {\n id: generateSessionId(),\n ownerId,\n namespace,\n key,\n value,\n expiresAt,\n createdAt: now,\n updatedAt: now,\n revision: 1,\n });\n\n return;\n } catch (error) {\n if (\n error instanceof StorageDuplicateError ||\n error instanceof StorageConflictError\n ) {\n continue;\n }\n\n throw error;\n }\n }\n\n throw new StorageConflictError(\n \"Could not write session data after repeated concurrent modifications.\",\n );\n }\n\n async get<T = unknown>(\n ownerId: string,\n namespace: string,\n key: string,\n ): Promise<T | null> {\n this.assertOwner(ownerId);\n this.assertNamespace(namespace);\n this.assertKey(key);\n\n const record = await this.storage.findOne<SessionDataRecord>(\n SESSION_DATA_COLLECTION,\n this.query(ownerId, namespace, key),\n );\n\n if (!record) return null;\n\n if (\n record.expiresAt &&\n record.expiresAt.getTime() <= this.clock().getTime()\n ) {\n await this.storage\n .delete(SESSION_DATA_COLLECTION, record.id, {\n expectedRevision: record.revision,\n })\n .catch((error) => {\n if (!(error instanceof StorageConflictError)) {\n throw error;\n }\n });\n\n return null;\n }\n\n return record.value as T;\n }\n\n async remove(ownerId: string, namespace: string, key: string): Promise<void> {\n this.assertOwner(ownerId);\n this.assertNamespace(namespace);\n this.assertKey(key);\n\n for (let attempt = 0; attempt < this.maximumRetries; attempt++) {\n const record = await this.storage.findOne<SessionDataRecord>(\n SESSION_DATA_COLLECTION,\n this.query(ownerId, namespace, key),\n );\n\n if (!record) return;\n\n try {\n const deleted = await this.storage.delete(\n SESSION_DATA_COLLECTION,\n record.id,\n {\n expectedRevision: record.revision,\n },\n );\n\n if (deleted) return;\n } catch (error) {\n if (!(error instanceof StorageConflictError)) {\n throw error;\n }\n }\n }\n\n throw new StorageConflictError(\n \"Could not remove session data after repeated concurrent modifications.\",\n );\n }\n\n async removeExpired(): Promise<number> {\n const now = this.clock();\n\n return this.storage.deleteMany<SessionDataRecord>(SESSION_DATA_COLLECTION, {\n where: {\n expiresAt: {\n operator: \"lte\",\n value: now,\n },\n },\n });\n }\n}\n"],"mappings":";;;;AAuBA,IAAa,qBAAb,MAAgC;CAMX;CACA;CANnB;CACA;CACA;CAEA,YACE,SACA,SACA;EAFiB,KAAA,UAAA;EACA,KAAA,UAAA;EAEjB,KAAK,QAAQ,QAAQ,gCAAgB,IAAI,KAAK;EAC9C,KAAK,oBAAoB,QAAQ,qBAAqB;EACtD,KAAK,iBAAiB,QAAQ,kBAAkB;EAEhD,IAAI,CAAC,QAAQ,kBAAkB,QAC7B,MAAM,IAAI,MAAM,kDAAkD;EAGpE,IACE,CAAC,OAAO,UAAU,KAAK,iBAAiB,KACxC,KAAK,oBAAoB,GAEzB,MAAM,IAAI,MAAM,+CAA+C;EAGjE,IAAI,CAAC,OAAO,UAAU,KAAK,cAAc,KAAK,KAAK,iBAAiB,GAClE,MAAM,IAAI,MAAM,4CAA4C;EAG9D,IACE,QAAQ,sBAAsB,KAAA,MAC7B,CAAC,OAAO,SAAS,QAAQ,iBAAiB,KACzC,QAAQ,qBAAqB,IAE/B,MAAM,IAAI,MAAM,8CAA8C;CAElE;CAEA,MAAM,aAA4B;EAChC,IAAI,CAAC,KAAK,QAAQ,aAAa,mBAC7B,MAAM,IAAI,MAAM,mDAAmD;EAGrE,MAAM,KAAK,QAAQ,mBAAmB,qBAAqB;CAC7D;CAEA,YAAoB,SAAuB;EACzC,IAAI,CAAC,QAAQ,KAAK,GAChB,MAAM,IAAI,MAAM,uCAAuC;CAE3D;CAEA,gBAAwB,WAAyB;EAC/C,IAAI,CAAC,KAAK,QAAQ,kBAAkB,SAAS,SAAS,GACpD,MAAM,IAAI,MAAM,0CAA0C,WAAW;CAEzE;CAEA,UAAkB,KAAmB;EACnC,IAAI,CAAC,IAAI,KAAK,GACZ,MAAM,IAAI,MAAM,uCAAuC;CAE3D;CAEA,YAAoB,OAAsB;EACxC,IAAI;EAEJ,IAAI;GACF,aAAa,KAAK,UAAU,KAAK;EACnC,QAAQ;GACN,MAAM,IAAI,MAAM,yCAAyC;EAC3D;EAEA,IAAI,eAAe,KAAA,GACjB,MAAM,IAAI,MAAM,yCAAyC;EAK3D,IAFa,IAAI,YAAY,CAAC,CAAC,OAAO,UAAU,CAAC,CAAC,aAEvC,KAAK,mBACd,MAAM,IAAI,MAAM,gDAAgD;CAEpE;CAEA,MAAc,SAAiB,WAAmB,KAAa;EAC7D,OAAO,EACL,OAAO;GACL,SAAS;IAAE,UAAU;IAAe,OAAO;GAAQ;GACnD,WAAW;IAAE,UAAU;IAAe,OAAO;GAAU;GACvD,KAAK;IAAE,UAAU;IAAe,OAAO;GAAI;EAC7C,EACF;CACF;CAEA,MAAM,IACJ,SACA,WACA,KACA,OACA,aAAa,KAAK,QAAQ,mBACX;EACf,KAAK,YAAY,OAAO;EACxB,KAAK,gBAAgB,SAAS;EAC9B,KAAK,UAAU,GAAG;EAClB,KAAK,YAAY,KAAK;EAEtB,IACE,eAAe,KAAA,MACd,CAAC,OAAO,SAAS,UAAU,KAAK,cAAc,IAE/C,MAAM,IAAI,MAAM,gCAAgC;EAGlD,KAAK,IAAI,UAAU,GAAG,UAAU,KAAK,gBAAgB,WAAW;GAC9D,MAAM,MAAM,KAAK,MAAM;GAEvB,MAAM,YACJ,eAAe,KAAA,IACX,OACA,IAAI,KAAK,IAAI,QAAQ,IAAI,aAAa,GAAI;GAEhD,MAAM,WAAW,MAAM,KAAK,QAAQ,QAClC,yBACA,KAAK,MAAM,SAAS,WAAW,GAAG,CACpC;GAEA,IAAI,UAAU;IACZ,IAAI;KAaF,IAAI,MAZkB,KAAK,QAAQ,OAAA,gBAEjC,SAAS,IACT;MACE;MACA;KACF,GACA,EACE,kBAAkB,SAAS,SAC7B,CACF,GAEa;IACf,SAAS,OAAO;KACd,IAAI,EAAE,iBAAiB,uBACrB,MAAM;IAEV;IAEA;GACF;GAEA,IAAI;IACF,MAAM,KAAK,QAAQ,OAA0B,yBAAyB;KACpE,IAAI,kBAAkB;KACtB;KACA;KACA;KACA;KACA;KACA,WAAW;KACX,WAAW;KACX,UAAU;IACZ,CAAC;IAED;GACF,SAAS,OAAO;IACd,IACE,iBAAiB,yBACjB,iBAAiB,sBAEjB;IAGF,MAAM;GACR;EACF;EAEA,MAAM,IAAI,qBACR,uEACF;CACF;CAEA,MAAM,IACJ,SACA,WACA,KACmB;EACnB,KAAK,YAAY,OAAO;EACxB,KAAK,gBAAgB,SAAS;EAC9B,KAAK,UAAU,GAAG;EAElB,MAAM,SAAS,MAAM,KAAK,QAAQ,QAChC,yBACA,KAAK,MAAM,SAAS,WAAW,GAAG,CACpC;EAEA,IAAI,CAAC,QAAQ,OAAO;EAEpB,IACE,OAAO,aACP,OAAO,UAAU,QAAQ,KAAK,KAAK,MAAM,CAAC,CAAC,QAAQ,GACnD;GACA,MAAM,KAAK,QACR,OAAO,yBAAyB,OAAO,IAAI,EAC1C,kBAAkB,OAAO,SAC3B,CAAC,CAAC,CACD,OAAO,UAAU;IAChB,IAAI,EAAE,iBAAiB,uBACrB,MAAM;GAEV,CAAC;GAEH,OAAO;EACT;EAEA,OAAO,OAAO;CAChB;CAEA,MAAM,OAAO,SAAiB,WAAmB,KAA4B;EAC3E,KAAK,YAAY,OAAO;EACxB,KAAK,gBAAgB,SAAS;EAC9B,KAAK,UAAU,GAAG;EAElB,KAAK,IAAI,UAAU,GAAG,UAAU,KAAK,gBAAgB,WAAW;GAC9D,MAAM,SAAS,MAAM,KAAK,QAAQ,QAChC,yBACA,KAAK,MAAM,SAAS,WAAW,GAAG,CACpC;GAEA,IAAI,CAAC,QAAQ;GAEb,IAAI;IASF,IAAI,MARkB,KAAK,QAAQ,OAAA,gBAEjC,OAAO,IACP,EACE,kBAAkB,OAAO,SAC3B,CACF,GAEa;GACf,SAAS,OAAO;IACd,IAAI,EAAE,iBAAiB,uBACrB,MAAM;GAEV;EACF;EAEA,MAAM,IAAI,qBACR,wEACF;CACF;CAEA,MAAM,gBAAiC;EACrC,MAAM,MAAM,KAAK,MAAM;EAEvB,OAAO,KAAK,QAAQ,WAA8B,yBAAyB,EACzE,OAAO,EACL,WAAW;GACT,UAAU;GACV,OAAO;EACT,EACF,EACF,CAAC;CACH;AACF"}
@@ -0,0 +1,64 @@
1
+ const require_session_errors = require("./errors/session.errors.cjs");
2
+ // src/session.cookies.ts
3
+ const createSessionCookieOptions = (environment) => {
4
+ const secure = environment.production;
5
+ const name = environment.name ?? (secure ? "__Host-puzzle.session" : "puzzle.session");
6
+ const sameSite = environment.sameSite ?? "lax";
7
+ if (name.startsWith("__Host-") && !secure) throw new require_session_errors.SessionConfigurationError("__Host- cookies require Secure.");
8
+ if (sameSite === "none" && !secure) throw new require_session_errors.SessionConfigurationError("SameSite=None cookies require Secure.");
9
+ return {
10
+ name,
11
+ path: "/",
12
+ httpOnly: true,
13
+ secure,
14
+ sameSite
15
+ };
16
+ };
17
+ const serializeCookieValue = (value) => encodeURIComponent(value);
18
+ const serializeSessionCookie = (token, options, maxAge) => {
19
+ if (!token) throw new require_session_errors.SessionConfigurationError("A session token is required.");
20
+ if (!Number.isInteger(maxAge) || maxAge <= 0) throw new require_session_errors.SessionConfigurationError("Cookie max-age must be a positive integer.");
21
+ const name = options.name ?? "__Host-puzzle.session";
22
+ if (name.startsWith("__Host-") && (!options.secure || options.path !== "/")) throw new require_session_errors.SessionConfigurationError("__Host- cookies require Secure and Path=/.");
23
+ const attributes = [
24
+ `${name}=${serializeCookieValue(token)}`,
25
+ "Path=/",
26
+ "HttpOnly",
27
+ `SameSite=${options.sameSite}`,
28
+ `Max-Age=${maxAge}`
29
+ ];
30
+ if (options.secure) attributes.push("Secure");
31
+ return attributes.join("; ");
32
+ };
33
+ const serializeClearedSessionCookie = (options) => {
34
+ const attributes = [
35
+ `${options.name ?? "__Host-puzzle.session"}=`,
36
+ "Path=/",
37
+ "HttpOnly",
38
+ `SameSite=${options.sameSite}`,
39
+ "Max-Age=0",
40
+ "Expires=Thu, 01 Jan 1970 00:00:00 GMT"
41
+ ];
42
+ if (options.secure) attributes.push("Secure");
43
+ return attributes.join("; ");
44
+ };
45
+ const readSessionCookie = (cookieHeader, cookieName = "__Host-puzzle.session") => {
46
+ if (!cookieHeader) return null;
47
+ for (const part of cookieHeader.split(";")) {
48
+ const separator = part.indexOf("=");
49
+ if (separator < 0) continue;
50
+ if (part.slice(0, separator).trim() !== cookieName) continue;
51
+ try {
52
+ return decodeURIComponent(part.slice(separator + 1).trim());
53
+ } catch {
54
+ return null;
55
+ }
56
+ }
57
+ return null;
58
+ };
59
+ exports.createSessionCookieOptions = createSessionCookieOptions;
60
+ exports.readSessionCookie = readSessionCookie;
61
+ exports.serializeClearedSessionCookie = serializeClearedSessionCookie;
62
+ exports.serializeSessionCookie = serializeSessionCookie;
63
+
64
+ //# sourceMappingURL=session.cookies.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"session.cookies.cjs","names":["SessionConfigurationError"],"sources":["../src/session.cookies.ts"],"sourcesContent":["import type { SessionCookieOptions } from \"./types/session.types\";\n\nimport { SessionConfigurationError } from \"./errors/session.errors\";\n\nexport type SessionCookieEnvironment = {\n readonly production: boolean;\n readonly name?: string;\n readonly sameSite?: \"lax\" | \"strict\" | \"none\";\n};\n\nexport const createSessionCookieOptions = (\n environment: SessionCookieEnvironment,\n): SessionCookieOptions => {\n const secure = environment.production;\n const name =\n environment.name ?? (secure ? \"__Host-puzzle.session\" : \"puzzle.session\");\n const sameSite = environment.sameSite ?? \"lax\";\n\n if (name.startsWith(\"__Host-\") && !secure) {\n throw new SessionConfigurationError(\"__Host- cookies require Secure.\");\n }\n\n if (sameSite === \"none\" && !secure) {\n throw new SessionConfigurationError(\n \"SameSite=None cookies require Secure.\",\n );\n }\n\n return {\n name,\n path: \"/\",\n httpOnly: true,\n secure,\n sameSite,\n };\n};\n\nconst serializeCookieValue = (value: string): string =>\n encodeURIComponent(value);\n\nexport const serializeSessionCookie = (\n token: string,\n options: SessionCookieOptions,\n maxAge: number,\n): string => {\n if (!token) {\n throw new SessionConfigurationError(\"A session token is required.\");\n }\n\n if (!Number.isInteger(maxAge) || maxAge <= 0) {\n throw new SessionConfigurationError(\n \"Cookie max-age must be a positive integer.\",\n );\n }\n\n const name = options.name ?? \"__Host-puzzle.session\";\n\n if (name.startsWith(\"__Host-\") && (!options.secure || options.path !== \"/\")) {\n throw new SessionConfigurationError(\n \"__Host- cookies require Secure and Path=/.\",\n );\n }\n\n const attributes = [\n `${name}=${serializeCookieValue(token)}`,\n \"Path=/\",\n \"HttpOnly\",\n `SameSite=${options.sameSite}`,\n `Max-Age=${maxAge}`,\n ];\n\n if (options.secure) attributes.push(\"Secure\");\n\n return attributes.join(\"; \");\n};\n\nexport const serializeClearedSessionCookie = (\n options: SessionCookieOptions,\n): string => {\n const name = options.name ?? \"__Host-puzzle.session\";\n\n const attributes = [\n `${name}=`,\n \"Path=/\",\n \"HttpOnly\",\n `SameSite=${options.sameSite}`,\n \"Max-Age=0\",\n \"Expires=Thu, 01 Jan 1970 00:00:00 GMT\",\n ];\n\n if (options.secure) attributes.push(\"Secure\");\n\n return attributes.join(\"; \");\n};\n\nexport const readSessionCookie = (\n cookieHeader: string | null,\n cookieName = \"__Host-puzzle.session\",\n): string | null => {\n if (!cookieHeader) return null;\n\n for (const part of cookieHeader.split(\";\")) {\n const separator = part.indexOf(\"=\");\n\n if (separator < 0) continue;\n\n const name = part.slice(0, separator).trim();\n\n if (name !== cookieName) continue;\n\n try {\n return decodeURIComponent(part.slice(separator + 1).trim());\n } catch {\n return null;\n }\n }\n\n return null;\n};\n"],"mappings":";;AAUA,MAAa,8BACX,gBACyB;CACzB,MAAM,SAAS,YAAY;CAC3B,MAAM,OACJ,YAAY,SAAS,SAAS,0BAA0B;CAC1D,MAAM,WAAW,YAAY,YAAY;CAEzC,IAAI,KAAK,WAAW,SAAS,KAAK,CAAC,QACjC,MAAM,IAAIA,uBAAAA,0BAA0B,iCAAiC;CAGvE,IAAI,aAAa,UAAU,CAAC,QAC1B,MAAM,IAAIA,uBAAAA,0BACR,uCACF;CAGF,OAAO;EACL;EACA,MAAM;EACN,UAAU;EACV;EACA;CACF;AACF;AAEA,MAAM,wBAAwB,UAC5B,mBAAmB,KAAK;AAE1B,MAAa,0BACX,OACA,SACA,WACW;CACX,IAAI,CAAC,OACH,MAAM,IAAIA,uBAAAA,0BAA0B,8BAA8B;CAGpE,IAAI,CAAC,OAAO,UAAU,MAAM,KAAK,UAAU,GACzC,MAAM,IAAIA,uBAAAA,0BACR,4CACF;CAGF,MAAM,OAAO,QAAQ,QAAQ;CAE7B,IAAI,KAAK,WAAW,SAAS,MAAM,CAAC,QAAQ,UAAU,QAAQ,SAAS,MACrE,MAAM,IAAIA,uBAAAA,0BACR,4CACF;CAGF,MAAM,aAAa;EACjB,GAAG,KAAK,GAAG,qBAAqB,KAAK;EACrC;EACA;EACA,YAAY,QAAQ;EACpB,WAAW;CACb;CAEA,IAAI,QAAQ,QAAQ,WAAW,KAAK,QAAQ;CAE5C,OAAO,WAAW,KAAK,IAAI;AAC7B;AAEA,MAAa,iCACX,YACW;CAGX,MAAM,aAAa;EACjB,GAHW,QAAQ,QAAQ,wBAGnB;EACR;EACA;EACA,YAAY,QAAQ;EACpB;EACA;CACF;CAEA,IAAI,QAAQ,QAAQ,WAAW,KAAK,QAAQ;CAE5C,OAAO,WAAW,KAAK,IAAI;AAC7B;AAEA,MAAa,qBACX,cACA,aAAa,4BACK;CAClB,IAAI,CAAC,cAAc,OAAO;CAE1B,KAAK,MAAM,QAAQ,aAAa,MAAM,GAAG,GAAG;EAC1C,MAAM,YAAY,KAAK,QAAQ,GAAG;EAElC,IAAI,YAAY,GAAG;EAInB,IAFa,KAAK,MAAM,GAAG,SAAS,CAAC,CAAC,KAE/B,MAAM,YAAY;EAEzB,IAAI;GACF,OAAO,mBAAmB,KAAK,MAAM,YAAY,CAAC,CAAC,CAAC,KAAK,CAAC;EAC5D,QAAQ;GACN,OAAO;EACT;CACF;CAEA,OAAO;AACT"}
@@ -0,0 +1,10 @@
1
+ import { SessionCookieOptions } from "./types/session.types.cjs";
2
+ export type SessionCookieEnvironment = {
3
+ readonly production: boolean;
4
+ readonly name?: string;
5
+ readonly sameSite?: "lax" | "strict" | "none";
6
+ };
7
+ export declare const createSessionCookieOptions: (environment: SessionCookieEnvironment) => SessionCookieOptions;
8
+ export declare const serializeSessionCookie: (token: string, options: SessionCookieOptions, maxAge: number) => string;
9
+ export declare const serializeClearedSessionCookie: (options: SessionCookieOptions) => string;
10
+ export declare const readSessionCookie: (cookieHeader: string | null, cookieName?: string) => string | null;
@@ -0,0 +1,10 @@
1
+ import { SessionCookieOptions } from "./types/session.types.js";
2
+ export type SessionCookieEnvironment = {
3
+ readonly production: boolean;
4
+ readonly name?: string;
5
+ readonly sameSite?: "lax" | "strict" | "none";
6
+ };
7
+ export declare const createSessionCookieOptions: (environment: SessionCookieEnvironment) => SessionCookieOptions;
8
+ export declare const serializeSessionCookie: (token: string, options: SessionCookieOptions, maxAge: number) => string;
9
+ export declare const serializeClearedSessionCookie: (options: SessionCookieOptions) => string;
10
+ export declare const readSessionCookie: (cookieHeader: string | null, cookieName?: string) => string | null;
@@ -0,0 +1,61 @@
1
+ import { SessionConfigurationError } from "./errors/session.errors.js";
2
+ // src/session.cookies.ts
3
+ const createSessionCookieOptions = (environment) => {
4
+ const secure = environment.production;
5
+ const name = environment.name ?? (secure ? "__Host-puzzle.session" : "puzzle.session");
6
+ const sameSite = environment.sameSite ?? "lax";
7
+ if (name.startsWith("__Host-") && !secure) throw new SessionConfigurationError("__Host- cookies require Secure.");
8
+ if (sameSite === "none" && !secure) throw new SessionConfigurationError("SameSite=None cookies require Secure.");
9
+ return {
10
+ name,
11
+ path: "/",
12
+ httpOnly: true,
13
+ secure,
14
+ sameSite
15
+ };
16
+ };
17
+ const serializeCookieValue = (value) => encodeURIComponent(value);
18
+ const serializeSessionCookie = (token, options, maxAge) => {
19
+ if (!token) throw new SessionConfigurationError("A session token is required.");
20
+ if (!Number.isInteger(maxAge) || maxAge <= 0) throw new SessionConfigurationError("Cookie max-age must be a positive integer.");
21
+ const name = options.name ?? "__Host-puzzle.session";
22
+ if (name.startsWith("__Host-") && (!options.secure || options.path !== "/")) throw new SessionConfigurationError("__Host- cookies require Secure and Path=/.");
23
+ const attributes = [
24
+ `${name}=${serializeCookieValue(token)}`,
25
+ "Path=/",
26
+ "HttpOnly",
27
+ `SameSite=${options.sameSite}`,
28
+ `Max-Age=${maxAge}`
29
+ ];
30
+ if (options.secure) attributes.push("Secure");
31
+ return attributes.join("; ");
32
+ };
33
+ const serializeClearedSessionCookie = (options) => {
34
+ const attributes = [
35
+ `${options.name ?? "__Host-puzzle.session"}=`,
36
+ "Path=/",
37
+ "HttpOnly",
38
+ `SameSite=${options.sameSite}`,
39
+ "Max-Age=0",
40
+ "Expires=Thu, 01 Jan 1970 00:00:00 GMT"
41
+ ];
42
+ if (options.secure) attributes.push("Secure");
43
+ return attributes.join("; ");
44
+ };
45
+ const readSessionCookie = (cookieHeader, cookieName = "__Host-puzzle.session") => {
46
+ if (!cookieHeader) return null;
47
+ for (const part of cookieHeader.split(";")) {
48
+ const separator = part.indexOf("=");
49
+ if (separator < 0) continue;
50
+ if (part.slice(0, separator).trim() !== cookieName) continue;
51
+ try {
52
+ return decodeURIComponent(part.slice(separator + 1).trim());
53
+ } catch {
54
+ return null;
55
+ }
56
+ }
57
+ return null;
58
+ };
59
+ export { createSessionCookieOptions, readSessionCookie, serializeClearedSessionCookie, serializeSessionCookie };
60
+
61
+ //# sourceMappingURL=session.cookies.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"session.cookies.js","names":[],"sources":["../src/session.cookies.ts"],"sourcesContent":["import type { SessionCookieOptions } from \"./types/session.types\";\n\nimport { SessionConfigurationError } from \"./errors/session.errors\";\n\nexport type SessionCookieEnvironment = {\n readonly production: boolean;\n readonly name?: string;\n readonly sameSite?: \"lax\" | \"strict\" | \"none\";\n};\n\nexport const createSessionCookieOptions = (\n environment: SessionCookieEnvironment,\n): SessionCookieOptions => {\n const secure = environment.production;\n const name =\n environment.name ?? (secure ? \"__Host-puzzle.session\" : \"puzzle.session\");\n const sameSite = environment.sameSite ?? \"lax\";\n\n if (name.startsWith(\"__Host-\") && !secure) {\n throw new SessionConfigurationError(\"__Host- cookies require Secure.\");\n }\n\n if (sameSite === \"none\" && !secure) {\n throw new SessionConfigurationError(\n \"SameSite=None cookies require Secure.\",\n );\n }\n\n return {\n name,\n path: \"/\",\n httpOnly: true,\n secure,\n sameSite,\n };\n};\n\nconst serializeCookieValue = (value: string): string =>\n encodeURIComponent(value);\n\nexport const serializeSessionCookie = (\n token: string,\n options: SessionCookieOptions,\n maxAge: number,\n): string => {\n if (!token) {\n throw new SessionConfigurationError(\"A session token is required.\");\n }\n\n if (!Number.isInteger(maxAge) || maxAge <= 0) {\n throw new SessionConfigurationError(\n \"Cookie max-age must be a positive integer.\",\n );\n }\n\n const name = options.name ?? \"__Host-puzzle.session\";\n\n if (name.startsWith(\"__Host-\") && (!options.secure || options.path !== \"/\")) {\n throw new SessionConfigurationError(\n \"__Host- cookies require Secure and Path=/.\",\n );\n }\n\n const attributes = [\n `${name}=${serializeCookieValue(token)}`,\n \"Path=/\",\n \"HttpOnly\",\n `SameSite=${options.sameSite}`,\n `Max-Age=${maxAge}`,\n ];\n\n if (options.secure) attributes.push(\"Secure\");\n\n return attributes.join(\"; \");\n};\n\nexport const serializeClearedSessionCookie = (\n options: SessionCookieOptions,\n): string => {\n const name = options.name ?? \"__Host-puzzle.session\";\n\n const attributes = [\n `${name}=`,\n \"Path=/\",\n \"HttpOnly\",\n `SameSite=${options.sameSite}`,\n \"Max-Age=0\",\n \"Expires=Thu, 01 Jan 1970 00:00:00 GMT\",\n ];\n\n if (options.secure) attributes.push(\"Secure\");\n\n return attributes.join(\"; \");\n};\n\nexport const readSessionCookie = (\n cookieHeader: string | null,\n cookieName = \"__Host-puzzle.session\",\n): string | null => {\n if (!cookieHeader) return null;\n\n for (const part of cookieHeader.split(\";\")) {\n const separator = part.indexOf(\"=\");\n\n if (separator < 0) continue;\n\n const name = part.slice(0, separator).trim();\n\n if (name !== cookieName) continue;\n\n try {\n return decodeURIComponent(part.slice(separator + 1).trim());\n } catch {\n return null;\n }\n }\n\n return null;\n};\n"],"mappings":";;AAUA,MAAa,8BACX,gBACyB;CACzB,MAAM,SAAS,YAAY;CAC3B,MAAM,OACJ,YAAY,SAAS,SAAS,0BAA0B;CAC1D,MAAM,WAAW,YAAY,YAAY;CAEzC,IAAI,KAAK,WAAW,SAAS,KAAK,CAAC,QACjC,MAAM,IAAI,0BAA0B,iCAAiC;CAGvE,IAAI,aAAa,UAAU,CAAC,QAC1B,MAAM,IAAI,0BACR,uCACF;CAGF,OAAO;EACL;EACA,MAAM;EACN,UAAU;EACV;EACA;CACF;AACF;AAEA,MAAM,wBAAwB,UAC5B,mBAAmB,KAAK;AAE1B,MAAa,0BACX,OACA,SACA,WACW;CACX,IAAI,CAAC,OACH,MAAM,IAAI,0BAA0B,8BAA8B;CAGpE,IAAI,CAAC,OAAO,UAAU,MAAM,KAAK,UAAU,GACzC,MAAM,IAAI,0BACR,4CACF;CAGF,MAAM,OAAO,QAAQ,QAAQ;CAE7B,IAAI,KAAK,WAAW,SAAS,MAAM,CAAC,QAAQ,UAAU,QAAQ,SAAS,MACrE,MAAM,IAAI,0BACR,4CACF;CAGF,MAAM,aAAa;EACjB,GAAG,KAAK,GAAG,qBAAqB,KAAK;EACrC;EACA;EACA,YAAY,QAAQ;EACpB,WAAW;CACb;CAEA,IAAI,QAAQ,QAAQ,WAAW,KAAK,QAAQ;CAE5C,OAAO,WAAW,KAAK,IAAI;AAC7B;AAEA,MAAa,iCACX,YACW;CAGX,MAAM,aAAa;EACjB,GAHW,QAAQ,QAAQ,wBAGnB;EACR;EACA;EACA,YAAY,QAAQ;EACpB;EACA;CACF;CAEA,IAAI,QAAQ,QAAQ,WAAW,KAAK,QAAQ;CAE5C,OAAO,WAAW,KAAK,IAAI;AAC7B;AAEA,MAAa,qBACX,cACA,aAAa,4BACK;CAClB,IAAI,CAAC,cAAc,OAAO;CAE1B,KAAK,MAAM,QAAQ,aAAa,MAAM,GAAG,GAAG;EAC1C,MAAM,YAAY,KAAK,QAAQ,GAAG;EAElC,IAAI,YAAY,GAAG;EAInB,IAFa,KAAK,MAAM,GAAG,SAAS,CAAC,CAAC,KAE/B,MAAM,YAAY;EAEzB,IAAI;GACF,OAAO,mBAAmB,KAAK,MAAM,YAAY,CAAC,CAAC,CAAC,KAAK,CAAC;EAC5D,QAAQ;GACN,OAAO;EACT;CACF;CAEA,OAAO;AACT"}
@@ -0,0 +1,42 @@
1
+ const require_session_errors = require("./errors/session.errors.cjs");
2
+ // src/session.csrf.ts
3
+ const normalizeOrigin = (value) => {
4
+ const url = new URL(value);
5
+ if (url.protocol !== "https:" && url.protocol !== "http:") throw new require_session_errors.SessionSecurityError("Unsupported request origin.");
6
+ if (url.username.length > 0 || url.password.length > 0 || url.pathname !== "/" || url.search.length > 0 || url.hash.length > 0) throw new require_session_errors.SessionSecurityError("The request origin must contain only an origin.");
7
+ return url.origin;
8
+ };
9
+ const validateRequestOrigin = (request, options) => {
10
+ const origin = request.headers.get("origin");
11
+ if (!origin) {
12
+ if (options.allowMissingOrigin === true) return;
13
+ throw new require_session_errors.SessionSecurityError("Origin header is required for this request.");
14
+ }
15
+ let normalizedOrigin;
16
+ try {
17
+ normalizedOrigin = normalizeOrigin(origin);
18
+ } catch {
19
+ throw new require_session_errors.SessionSecurityError("Invalid request origin.");
20
+ }
21
+ if (!options.allowedOrigins.some((candidate) => {
22
+ try {
23
+ return normalizeOrigin(candidate) === normalizedOrigin;
24
+ } catch {
25
+ return false;
26
+ }
27
+ })) throw new require_session_errors.SessionSecurityError("Request origin is not allowed.");
28
+ };
29
+ const protectStateChangingRequest = (request, options) => {
30
+ const method = request.method.toUpperCase();
31
+ if ([
32
+ "GET",
33
+ "HEAD",
34
+ "OPTIONS"
35
+ ].includes(method)) return;
36
+ validateRequestOrigin(request, options);
37
+ if (request.headers.get("sec-fetch-site") === "cross-site") throw new require_session_errors.SessionSecurityError("Cross-site state-changing requests are not allowed.");
38
+ };
39
+ exports.protectStateChangingRequest = protectStateChangingRequest;
40
+ exports.validateRequestOrigin = validateRequestOrigin;
41
+
42
+ //# sourceMappingURL=session.csrf.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"session.csrf.cjs","names":["SessionSecurityError"],"sources":["../src/session.csrf.ts"],"sourcesContent":["import { SessionSecurityError } from \"./errors/session.errors\";\n\nexport type CsrfOptions = {\n readonly allowedOrigins: readonly string[];\n readonly allowMissingOrigin?: boolean;\n};\n\nconst normalizeOrigin = (value: string): string => {\n const url = new URL(value);\n\n if (url.protocol !== \"https:\" && url.protocol !== \"http:\") {\n throw new SessionSecurityError(\"Unsupported request origin.\");\n }\n\n if (\n url.username.length > 0 ||\n url.password.length > 0 ||\n url.pathname !== \"/\" ||\n url.search.length > 0 ||\n url.hash.length > 0\n ) {\n throw new SessionSecurityError(\n \"The request origin must contain only an origin.\",\n );\n }\n\n return url.origin;\n};\n\nexport const validateRequestOrigin = (\n request: Request,\n options: CsrfOptions,\n): void => {\n const origin = request.headers.get(\"origin\");\n\n if (!origin) {\n if (options.allowMissingOrigin === true) return;\n\n throw new SessionSecurityError(\n \"Origin header is required for this request.\",\n );\n }\n\n let normalizedOrigin: string;\n\n try {\n normalizedOrigin = normalizeOrigin(origin);\n } catch {\n throw new SessionSecurityError(\"Invalid request origin.\");\n }\n\n const allowed = options.allowedOrigins.some((candidate) => {\n try {\n return normalizeOrigin(candidate) === normalizedOrigin;\n } catch {\n return false;\n }\n });\n\n if (!allowed) {\n throw new SessionSecurityError(\"Request origin is not allowed.\");\n }\n};\n\nexport const protectStateChangingRequest = (\n request: Request,\n options: CsrfOptions,\n): void => {\n const method = request.method.toUpperCase();\n\n if ([\"GET\", \"HEAD\", \"OPTIONS\"].includes(method)) return;\n\n validateRequestOrigin(request, options);\n\n const fetchSite = request.headers.get(\"sec-fetch-site\");\n\n if (fetchSite === \"cross-site\") {\n throw new SessionSecurityError(\n \"Cross-site state-changing requests are not allowed.\",\n );\n }\n};\n"],"mappings":";;AAOA,MAAM,mBAAmB,UAA0B;CACjD,MAAM,MAAM,IAAI,IAAI,KAAK;CAEzB,IAAI,IAAI,aAAa,YAAY,IAAI,aAAa,SAChD,MAAM,IAAIA,uBAAAA,qBAAqB,6BAA6B;CAG9D,IACE,IAAI,SAAS,SAAS,KACtB,IAAI,SAAS,SAAS,KACtB,IAAI,aAAa,OACjB,IAAI,OAAO,SAAS,KACpB,IAAI,KAAK,SAAS,GAElB,MAAM,IAAIA,uBAAAA,qBACR,iDACF;CAGF,OAAO,IAAI;AACb;AAEA,MAAa,yBACX,SACA,YACS;CACT,MAAM,SAAS,QAAQ,QAAQ,IAAI,QAAQ;CAE3C,IAAI,CAAC,QAAQ;EACX,IAAI,QAAQ,uBAAuB,MAAM;EAEzC,MAAM,IAAIA,uBAAAA,qBACR,6CACF;CACF;CAEA,IAAI;CAEJ,IAAI;EACF,mBAAmB,gBAAgB,MAAM;CAC3C,QAAQ;EACN,MAAM,IAAIA,uBAAAA,qBAAqB,yBAAyB;CAC1D;CAUA,IAAI,CARY,QAAQ,eAAe,MAAM,cAAc;EACzD,IAAI;GACF,OAAO,gBAAgB,SAAS,MAAM;EACxC,QAAQ;GACN,OAAO;EACT;CACF,CAEW,GACT,MAAM,IAAIA,uBAAAA,qBAAqB,gCAAgC;AAEnE;AAEA,MAAa,+BACX,SACA,YACS;CACT,MAAM,SAAS,QAAQ,OAAO,YAAY;CAE1C,IAAI;EAAC;EAAO;EAAQ;CAAS,CAAC,CAAC,SAAS,MAAM,GAAG;CAEjD,sBAAsB,SAAS,OAAO;CAItC,IAFkB,QAAQ,QAAQ,IAAI,gBAE1B,MAAM,cAChB,MAAM,IAAIA,uBAAAA,qBACR,qDACF;AAEJ"}
@@ -0,0 +1,6 @@
1
+ export type CsrfOptions = {
2
+ readonly allowedOrigins: readonly string[];
3
+ readonly allowMissingOrigin?: boolean;
4
+ };
5
+ export declare const validateRequestOrigin: (request: Request, options: CsrfOptions) => void;
6
+ export declare const protectStateChangingRequest: (request: Request, options: CsrfOptions) => void;
@@ -0,0 +1,6 @@
1
+ export type CsrfOptions = {
2
+ readonly allowedOrigins: readonly string[];
3
+ readonly allowMissingOrigin?: boolean;
4
+ };
5
+ export declare const validateRequestOrigin: (request: Request, options: CsrfOptions) => void;
6
+ export declare const protectStateChangingRequest: (request: Request, options: CsrfOptions) => void;
@@ -0,0 +1,41 @@
1
+ import { SessionSecurityError } from "./errors/session.errors.js";
2
+ // src/session.csrf.ts
3
+ const normalizeOrigin = (value) => {
4
+ const url = new URL(value);
5
+ if (url.protocol !== "https:" && url.protocol !== "http:") throw new SessionSecurityError("Unsupported request origin.");
6
+ if (url.username.length > 0 || url.password.length > 0 || url.pathname !== "/" || url.search.length > 0 || url.hash.length > 0) throw new SessionSecurityError("The request origin must contain only an origin.");
7
+ return url.origin;
8
+ };
9
+ const validateRequestOrigin = (request, options) => {
10
+ const origin = request.headers.get("origin");
11
+ if (!origin) {
12
+ if (options.allowMissingOrigin === true) return;
13
+ throw new SessionSecurityError("Origin header is required for this request.");
14
+ }
15
+ let normalizedOrigin;
16
+ try {
17
+ normalizedOrigin = normalizeOrigin(origin);
18
+ } catch {
19
+ throw new SessionSecurityError("Invalid request origin.");
20
+ }
21
+ if (!options.allowedOrigins.some((candidate) => {
22
+ try {
23
+ return normalizeOrigin(candidate) === normalizedOrigin;
24
+ } catch {
25
+ return false;
26
+ }
27
+ })) throw new SessionSecurityError("Request origin is not allowed.");
28
+ };
29
+ const protectStateChangingRequest = (request, options) => {
30
+ const method = request.method.toUpperCase();
31
+ if ([
32
+ "GET",
33
+ "HEAD",
34
+ "OPTIONS"
35
+ ].includes(method)) return;
36
+ validateRequestOrigin(request, options);
37
+ if (request.headers.get("sec-fetch-site") === "cross-site") throw new SessionSecurityError("Cross-site state-changing requests are not allowed.");
38
+ };
39
+ export { protectStateChangingRequest, validateRequestOrigin };
40
+
41
+ //# sourceMappingURL=session.csrf.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"session.csrf.js","names":[],"sources":["../src/session.csrf.ts"],"sourcesContent":["import { SessionSecurityError } from \"./errors/session.errors\";\n\nexport type CsrfOptions = {\n readonly allowedOrigins: readonly string[];\n readonly allowMissingOrigin?: boolean;\n};\n\nconst normalizeOrigin = (value: string): string => {\n const url = new URL(value);\n\n if (url.protocol !== \"https:\" && url.protocol !== \"http:\") {\n throw new SessionSecurityError(\"Unsupported request origin.\");\n }\n\n if (\n url.username.length > 0 ||\n url.password.length > 0 ||\n url.pathname !== \"/\" ||\n url.search.length > 0 ||\n url.hash.length > 0\n ) {\n throw new SessionSecurityError(\n \"The request origin must contain only an origin.\",\n );\n }\n\n return url.origin;\n};\n\nexport const validateRequestOrigin = (\n request: Request,\n options: CsrfOptions,\n): void => {\n const origin = request.headers.get(\"origin\");\n\n if (!origin) {\n if (options.allowMissingOrigin === true) return;\n\n throw new SessionSecurityError(\n \"Origin header is required for this request.\",\n );\n }\n\n let normalizedOrigin: string;\n\n try {\n normalizedOrigin = normalizeOrigin(origin);\n } catch {\n throw new SessionSecurityError(\"Invalid request origin.\");\n }\n\n const allowed = options.allowedOrigins.some((candidate) => {\n try {\n return normalizeOrigin(candidate) === normalizedOrigin;\n } catch {\n return false;\n }\n });\n\n if (!allowed) {\n throw new SessionSecurityError(\"Request origin is not allowed.\");\n }\n};\n\nexport const protectStateChangingRequest = (\n request: Request,\n options: CsrfOptions,\n): void => {\n const method = request.method.toUpperCase();\n\n if ([\"GET\", \"HEAD\", \"OPTIONS\"].includes(method)) return;\n\n validateRequestOrigin(request, options);\n\n const fetchSite = request.headers.get(\"sec-fetch-site\");\n\n if (fetchSite === \"cross-site\") {\n throw new SessionSecurityError(\n \"Cross-site state-changing requests are not allowed.\",\n );\n }\n};\n"],"mappings":";;AAOA,MAAM,mBAAmB,UAA0B;CACjD,MAAM,MAAM,IAAI,IAAI,KAAK;CAEzB,IAAI,IAAI,aAAa,YAAY,IAAI,aAAa,SAChD,MAAM,IAAI,qBAAqB,6BAA6B;CAG9D,IACE,IAAI,SAAS,SAAS,KACtB,IAAI,SAAS,SAAS,KACtB,IAAI,aAAa,OACjB,IAAI,OAAO,SAAS,KACpB,IAAI,KAAK,SAAS,GAElB,MAAM,IAAI,qBACR,iDACF;CAGF,OAAO,IAAI;AACb;AAEA,MAAa,yBACX,SACA,YACS;CACT,MAAM,SAAS,QAAQ,QAAQ,IAAI,QAAQ;CAE3C,IAAI,CAAC,QAAQ;EACX,IAAI,QAAQ,uBAAuB,MAAM;EAEzC,MAAM,IAAI,qBACR,6CACF;CACF;CAEA,IAAI;CAEJ,IAAI;EACF,mBAAmB,gBAAgB,MAAM;CAC3C,QAAQ;EACN,MAAM,IAAI,qBAAqB,yBAAyB;CAC1D;CAUA,IAAI,CARY,QAAQ,eAAe,MAAM,cAAc;EACzD,IAAI;GACF,OAAO,gBAAgB,SAAS,MAAM;EACxC,QAAQ;GACN,OAAO;EACT;CACF,CAEW,GACT,MAAM,IAAI,qBAAqB,gCAAgC;AAEnE;AAEA,MAAa,+BACX,SACA,YACS;CACT,MAAM,SAAS,QAAQ,OAAO,YAAY;CAE1C,IAAI;EAAC;EAAO;EAAQ;CAAS,CAAC,CAAC,SAAS,MAAM,GAAG;CAEjD,sBAAsB,SAAS,OAAO;CAItC,IAFkB,QAAQ,QAAQ,IAAI,gBAE1B,MAAM,cAChB,MAAM,IAAI,qBACR,qDACF;AAEJ"}