@voidhash/mimic-effect 0.0.8 → 1.0.0-beta.1

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 (176) hide show
  1. package/.turbo/turbo-build.log +93 -89
  2. package/README.md +385 -0
  3. package/dist/ColdStorage.cjs +60 -0
  4. package/dist/ColdStorage.d.cts +53 -0
  5. package/dist/ColdStorage.d.cts.map +1 -0
  6. package/dist/ColdStorage.d.mts +53 -0
  7. package/dist/ColdStorage.d.mts.map +1 -0
  8. package/dist/ColdStorage.mjs +60 -0
  9. package/dist/ColdStorage.mjs.map +1 -0
  10. package/dist/DocumentManager.cjs +193 -82
  11. package/dist/DocumentManager.d.cts +33 -19
  12. package/dist/DocumentManager.d.cts.map +1 -1
  13. package/dist/DocumentManager.d.mts +33 -19
  14. package/dist/DocumentManager.d.mts.map +1 -1
  15. package/dist/DocumentManager.mjs +189 -67
  16. package/dist/DocumentManager.mjs.map +1 -1
  17. package/dist/Errors.cjs +45 -0
  18. package/dist/Errors.d.cts +81 -0
  19. package/dist/Errors.d.cts.map +1 -0
  20. package/dist/Errors.d.mts +81 -0
  21. package/dist/Errors.d.mts.map +1 -0
  22. package/dist/Errors.mjs +40 -0
  23. package/dist/Errors.mjs.map +1 -0
  24. package/dist/HotStorage.cjs +77 -0
  25. package/dist/HotStorage.d.cts +54 -0
  26. package/dist/HotStorage.d.cts.map +1 -0
  27. package/dist/HotStorage.d.mts +54 -0
  28. package/dist/HotStorage.d.mts.map +1 -0
  29. package/dist/HotStorage.mjs +77 -0
  30. package/dist/HotStorage.mjs.map +1 -0
  31. package/dist/Metrics.cjs +121 -0
  32. package/dist/Metrics.d.cts +27 -0
  33. package/dist/Metrics.d.cts.map +1 -0
  34. package/dist/Metrics.d.mts +27 -0
  35. package/dist/Metrics.d.mts.map +1 -0
  36. package/dist/Metrics.mjs +106 -0
  37. package/dist/Metrics.mjs.map +1 -0
  38. package/dist/MimicAuthService.cjs +61 -45
  39. package/dist/MimicAuthService.d.cts +61 -48
  40. package/dist/MimicAuthService.d.cts.map +1 -1
  41. package/dist/MimicAuthService.d.mts +61 -48
  42. package/dist/MimicAuthService.d.mts.map +1 -1
  43. package/dist/MimicAuthService.mjs +60 -36
  44. package/dist/MimicAuthService.mjs.map +1 -1
  45. package/dist/MimicClusterServerEngine.cjs +443 -0
  46. package/dist/MimicClusterServerEngine.d.cts +17 -0
  47. package/dist/MimicClusterServerEngine.d.cts.map +1 -0
  48. package/dist/MimicClusterServerEngine.d.mts +17 -0
  49. package/dist/MimicClusterServerEngine.d.mts.map +1 -0
  50. package/dist/MimicClusterServerEngine.mjs +445 -0
  51. package/dist/MimicClusterServerEngine.mjs.map +1 -0
  52. package/dist/MimicServer.cjs +205 -96
  53. package/dist/MimicServer.d.cts +9 -110
  54. package/dist/MimicServer.d.cts.map +1 -1
  55. package/dist/MimicServer.d.mts +9 -110
  56. package/dist/MimicServer.d.mts.map +1 -1
  57. package/dist/MimicServer.mjs +206 -90
  58. package/dist/MimicServer.mjs.map +1 -1
  59. package/dist/MimicServerEngine.cjs +97 -0
  60. package/dist/MimicServerEngine.d.cts +75 -0
  61. package/dist/MimicServerEngine.d.cts.map +1 -0
  62. package/dist/MimicServerEngine.d.mts +75 -0
  63. package/dist/MimicServerEngine.d.mts.map +1 -0
  64. package/dist/MimicServerEngine.mjs +97 -0
  65. package/dist/MimicServerEngine.mjs.map +1 -0
  66. package/dist/PresenceManager.cjs +75 -91
  67. package/dist/PresenceManager.d.cts +17 -66
  68. package/dist/PresenceManager.d.cts.map +1 -1
  69. package/dist/PresenceManager.d.mts +17 -66
  70. package/dist/PresenceManager.d.mts.map +1 -1
  71. package/dist/PresenceManager.mjs +74 -78
  72. package/dist/PresenceManager.mjs.map +1 -1
  73. package/dist/Protocol.cjs +146 -0
  74. package/dist/Protocol.d.cts +203 -0
  75. package/dist/Protocol.d.cts.map +1 -0
  76. package/dist/Protocol.d.mts +203 -0
  77. package/dist/Protocol.d.mts.map +1 -0
  78. package/dist/Protocol.mjs +132 -0
  79. package/dist/Protocol.mjs.map +1 -0
  80. package/dist/Types.d.cts +172 -0
  81. package/dist/Types.d.cts.map +1 -0
  82. package/dist/Types.d.mts +172 -0
  83. package/dist/Types.d.mts.map +1 -0
  84. package/dist/_virtual/rolldown_runtime.cjs +1 -25
  85. package/dist/_virtual/rolldown_runtime.mjs +4 -1
  86. package/dist/index.cjs +37 -75
  87. package/dist/index.d.cts +13 -12
  88. package/dist/index.d.mts +13 -12
  89. package/dist/index.mjs +12 -12
  90. package/package.json +14 -6
  91. package/src/ColdStorage.ts +136 -0
  92. package/src/DocumentManager.ts +445 -193
  93. package/src/Errors.ts +100 -0
  94. package/src/HotStorage.ts +165 -0
  95. package/src/Metrics.ts +163 -0
  96. package/src/MimicAuthService.ts +126 -64
  97. package/src/MimicClusterServerEngine.ts +824 -0
  98. package/src/MimicServer.ts +448 -195
  99. package/src/MimicServerEngine.ts +272 -0
  100. package/src/PresenceManager.ts +169 -240
  101. package/src/Protocol.ts +350 -0
  102. package/src/Types.ts +231 -0
  103. package/src/index.ts +57 -23
  104. package/tests/ColdStorage.test.ts +136 -0
  105. package/tests/DocumentManager.test.ts +158 -287
  106. package/tests/HotStorage.test.ts +143 -0
  107. package/tests/MimicAuthService.test.ts +102 -134
  108. package/tests/MimicClusterServerEngine.test.ts +587 -0
  109. package/tests/MimicServer.test.ts +90 -226
  110. package/tests/MimicServerEngine.test.ts +521 -0
  111. package/tests/PresenceManager.test.ts +22 -63
  112. package/tests/Protocol.test.ts +190 -0
  113. package/tsconfig.json +1 -1
  114. package/dist/DocumentProtocol.cjs +0 -94
  115. package/dist/DocumentProtocol.d.cts +0 -113
  116. package/dist/DocumentProtocol.d.cts.map +0 -1
  117. package/dist/DocumentProtocol.d.mts +0 -113
  118. package/dist/DocumentProtocol.d.mts.map +0 -1
  119. package/dist/DocumentProtocol.mjs +0 -89
  120. package/dist/DocumentProtocol.mjs.map +0 -1
  121. package/dist/MimicConfig.cjs +0 -60
  122. package/dist/MimicConfig.d.cts +0 -141
  123. package/dist/MimicConfig.d.cts.map +0 -1
  124. package/dist/MimicConfig.d.mts +0 -141
  125. package/dist/MimicConfig.d.mts.map +0 -1
  126. package/dist/MimicConfig.mjs +0 -50
  127. package/dist/MimicConfig.mjs.map +0 -1
  128. package/dist/MimicDataStorage.cjs +0 -83
  129. package/dist/MimicDataStorage.d.cts +0 -113
  130. package/dist/MimicDataStorage.d.cts.map +0 -1
  131. package/dist/MimicDataStorage.d.mts +0 -113
  132. package/dist/MimicDataStorage.d.mts.map +0 -1
  133. package/dist/MimicDataStorage.mjs +0 -74
  134. package/dist/MimicDataStorage.mjs.map +0 -1
  135. package/dist/WebSocketHandler.cjs +0 -365
  136. package/dist/WebSocketHandler.d.cts +0 -34
  137. package/dist/WebSocketHandler.d.cts.map +0 -1
  138. package/dist/WebSocketHandler.d.mts +0 -34
  139. package/dist/WebSocketHandler.d.mts.map +0 -1
  140. package/dist/WebSocketHandler.mjs +0 -355
  141. package/dist/WebSocketHandler.mjs.map +0 -1
  142. package/dist/auth/NoAuth.cjs +0 -43
  143. package/dist/auth/NoAuth.d.cts +0 -22
  144. package/dist/auth/NoAuth.d.cts.map +0 -1
  145. package/dist/auth/NoAuth.d.mts +0 -22
  146. package/dist/auth/NoAuth.d.mts.map +0 -1
  147. package/dist/auth/NoAuth.mjs +0 -36
  148. package/dist/auth/NoAuth.mjs.map +0 -1
  149. package/dist/errors.cjs +0 -74
  150. package/dist/errors.d.cts +0 -89
  151. package/dist/errors.d.cts.map +0 -1
  152. package/dist/errors.d.mts +0 -89
  153. package/dist/errors.d.mts.map +0 -1
  154. package/dist/errors.mjs +0 -67
  155. package/dist/errors.mjs.map +0 -1
  156. package/dist/storage/InMemoryDataStorage.cjs +0 -57
  157. package/dist/storage/InMemoryDataStorage.d.cts +0 -19
  158. package/dist/storage/InMemoryDataStorage.d.cts.map +0 -1
  159. package/dist/storage/InMemoryDataStorage.d.mts +0 -19
  160. package/dist/storage/InMemoryDataStorage.d.mts.map +0 -1
  161. package/dist/storage/InMemoryDataStorage.mjs +0 -48
  162. package/dist/storage/InMemoryDataStorage.mjs.map +0 -1
  163. package/src/DocumentProtocol.ts +0 -112
  164. package/src/MimicConfig.ts +0 -211
  165. package/src/MimicDataStorage.ts +0 -157
  166. package/src/WebSocketHandler.ts +0 -735
  167. package/src/auth/NoAuth.ts +0 -46
  168. package/src/errors.ts +0 -113
  169. package/src/storage/InMemoryDataStorage.ts +0 -66
  170. package/tests/DocumentProtocol.test.ts +0 -113
  171. package/tests/InMemoryDataStorage.test.ts +0 -190
  172. package/tests/MimicConfig.test.ts +0 -290
  173. package/tests/MimicDataStorage.test.ts +0 -190
  174. package/tests/NoAuth.test.ts +0 -94
  175. package/tests/WebSocketHandler.test.ts +0 -321
  176. package/tests/errors.test.ts +0 -77
package/src/Errors.ts ADDED
@@ -0,0 +1,100 @@
1
+ /**
2
+ * @voidhash/mimic-effect - Error Types
3
+ *
4
+ * All error types used throughout the mimic-effect package.
5
+ */
6
+ import { Data } from "effect";
7
+
8
+ // =============================================================================
9
+ // Storage Errors
10
+ // =============================================================================
11
+
12
+ /**
13
+ * Error when ColdStorage (snapshot storage) operations fail
14
+ */
15
+ export class ColdStorageError extends Data.TaggedError("ColdStorageError")<{
16
+ readonly documentId: string;
17
+ readonly operation: "load" | "save" | "delete";
18
+ readonly cause: unknown;
19
+ }> {}
20
+
21
+ /**
22
+ * Error when HotStorage (WAL storage) operations fail
23
+ */
24
+ export class HotStorageError extends Data.TaggedError("HotStorageError")<{
25
+ readonly documentId: string;
26
+ readonly operation: "append" | "getEntries" | "truncate";
27
+ readonly cause: unknown;
28
+ }> {}
29
+
30
+ // =============================================================================
31
+ // Auth Errors
32
+ // =============================================================================
33
+
34
+ /**
35
+ * Error when authentication fails (invalid token, expired, etc.)
36
+ */
37
+ export class AuthenticationError extends Data.TaggedError(
38
+ "AuthenticationError"
39
+ )<{
40
+ readonly reason: string;
41
+ }> {}
42
+
43
+ /**
44
+ * Error when authorization fails (user doesn't have required permission)
45
+ */
46
+ export class AuthorizationError extends Data.TaggedError("AuthorizationError")<{
47
+ readonly reason: string;
48
+ readonly required: "read" | "write";
49
+ readonly actual: "read" | "write";
50
+ }> {}
51
+
52
+ // =============================================================================
53
+ // Connection Errors
54
+ // =============================================================================
55
+
56
+ /**
57
+ * Error when document ID is missing from WebSocket request path
58
+ */
59
+ export class MissingDocumentIdError extends Data.TaggedError(
60
+ "MissingDocumentIdError"
61
+ )<{
62
+ readonly path: string;
63
+ }> {}
64
+
65
+ /**
66
+ * Error when WebSocket message cannot be parsed
67
+ */
68
+ export class MessageParseError extends Data.TaggedError("MessageParseError")<{
69
+ readonly cause: unknown;
70
+ }> {}
71
+
72
+ // =============================================================================
73
+ // Transaction Errors
74
+ // =============================================================================
75
+
76
+ /**
77
+ * Error when a transaction is rejected by the document
78
+ */
79
+ export class TransactionRejectedError extends Data.TaggedError(
80
+ "TransactionRejectedError"
81
+ )<{
82
+ readonly transactionId: string;
83
+ readonly reason: string;
84
+ }> {}
85
+
86
+ // =============================================================================
87
+ // Union Type
88
+ // =============================================================================
89
+
90
+ /**
91
+ * Union of all mimic-effect errors
92
+ */
93
+ export type MimicError =
94
+ | ColdStorageError
95
+ | HotStorageError
96
+ | AuthenticationError
97
+ | AuthorizationError
98
+ | MissingDocumentIdError
99
+ | MessageParseError
100
+ | TransactionRejectedError;
@@ -0,0 +1,165 @@
1
+ /**
2
+ * @voidhash/mimic-effect - HotStorage
3
+ *
4
+ * Interface and implementations for Write-Ahead Log (WAL) storage.
5
+ */
6
+ import { Context, Effect, HashMap, Layer, Ref } from "effect";
7
+ import type { WalEntry } from "./Types";
8
+ import { HotStorageError } from "./Errors";
9
+
10
+ // =============================================================================
11
+ // HotStorage Interface
12
+ // =============================================================================
13
+
14
+ /**
15
+ * HotStorage interface for storing Write-Ahead Log entries.
16
+ *
17
+ * This is the "hot" tier of the two-tier storage system.
18
+ * It stores every transaction as a WAL entry for durability between snapshots.
19
+ * WAL entries are small (just the transaction) and writes are append-only.
20
+ */
21
+ export interface HotStorage {
22
+ /**
23
+ * Append a WAL entry for a document.
24
+ */
25
+ readonly append: (
26
+ documentId: string,
27
+ entry: WalEntry
28
+ ) => Effect.Effect<void, HotStorageError>;
29
+
30
+ /**
31
+ * Get all WAL entries for a document since a given version.
32
+ * Returns entries with version > sinceVersion, ordered by version.
33
+ */
34
+ readonly getEntries: (
35
+ documentId: string,
36
+ sinceVersion: number
37
+ ) => Effect.Effect<WalEntry[], HotStorageError>;
38
+
39
+ /**
40
+ * Truncate WAL entries up to (and including) a given version.
41
+ * Called after a snapshot is saved to remove entries that are now in the snapshot.
42
+ */
43
+ readonly truncate: (
44
+ documentId: string,
45
+ upToVersion: number
46
+ ) => Effect.Effect<void, HotStorageError>;
47
+ }
48
+
49
+ // =============================================================================
50
+ // Context Tag
51
+ // =============================================================================
52
+
53
+ /**
54
+ * Context tag for HotStorage service
55
+ */
56
+ export class HotStorageTag extends Context.Tag("@voidhash/mimic-effect/HotStorage")<
57
+ HotStorageTag,
58
+ HotStorage
59
+ >() {}
60
+
61
+ // =============================================================================
62
+ // Factory
63
+ // =============================================================================
64
+
65
+ /**
66
+ * Create a HotStorage layer from an Effect that produces a HotStorage service.
67
+ *
68
+ * This allows you to access other Effect services when implementing custom storage.
69
+ *
70
+ * @example
71
+ * ```typescript
72
+ * const Hot = HotStorage.make(
73
+ * Effect.gen(function*() {
74
+ * const redis = yield* RedisService
75
+ *
76
+ * return {
77
+ * append: (documentId, entry) =>
78
+ * redis.rpush(`wal:${documentId}`, JSON.stringify(entry)),
79
+ * getEntries: (documentId, sinceVersion) =>
80
+ * redis.lrange(`wal:${documentId}`, 0, -1).pipe(
81
+ * Effect.map(entries =>
82
+ * entries
83
+ * .map(e => JSON.parse(e))
84
+ * .filter(e => e.version > sinceVersion)
85
+ * .sort((a, b) => a.version - b.version)
86
+ * )
87
+ * ),
88
+ * truncate: (documentId, upToVersion) =>
89
+ * // Implementation depends on Redis data structure
90
+ * Effect.void,
91
+ * }
92
+ * })
93
+ * )
94
+ * ```
95
+ */
96
+ export const make = <E, R>(
97
+ effect: Effect.Effect<HotStorage, E, R>
98
+ ): Layer.Layer<HotStorageTag, E, R> =>
99
+ Layer.effect(HotStorageTag, effect);
100
+
101
+ // =============================================================================
102
+ // InMemory Implementation
103
+ // =============================================================================
104
+
105
+ /**
106
+ * In-memory HotStorage implementation.
107
+ *
108
+ * Useful for testing and development. Not suitable for production
109
+ * as data is lost when the process restarts.
110
+ */
111
+ export namespace InMemory {
112
+ /**
113
+ * Create an in-memory HotStorage layer.
114
+ */
115
+ export const make = (): Layer.Layer<HotStorageTag> =>
116
+ Layer.effect(
117
+ HotStorageTag,
118
+ Effect.gen(function* () {
119
+ const store = yield* Ref.make(HashMap.empty<string, WalEntry[]>());
120
+
121
+ return {
122
+ append: (documentId, entry) =>
123
+ Ref.update(store, (map) => {
124
+ const existing = HashMap.get(map, documentId);
125
+ const entries =
126
+ existing._tag === "Some" ? existing.value : [];
127
+ return HashMap.set(map, documentId, [...entries, entry]);
128
+ }),
129
+
130
+ getEntries: (documentId, sinceVersion) =>
131
+ Effect.gen(function* () {
132
+ const current = yield* Ref.get(store);
133
+ const existing = HashMap.get(current, documentId);
134
+ const entries =
135
+ existing._tag === "Some" ? existing.value : [];
136
+ return entries
137
+ .filter((e) => e.version > sinceVersion)
138
+ .sort((a, b) => a.version - b.version);
139
+ }),
140
+
141
+ truncate: (documentId, upToVersion) =>
142
+ Ref.update(store, (map) => {
143
+ const existing = HashMap.get(map, documentId);
144
+ if (existing._tag === "None") {
145
+ return map;
146
+ }
147
+ const filtered = existing.value.filter(
148
+ (e) => e.version > upToVersion
149
+ );
150
+ return HashMap.set(map, documentId, filtered);
151
+ }),
152
+ };
153
+ })
154
+ );
155
+ }
156
+
157
+ // =============================================================================
158
+ // Re-export namespace
159
+ // =============================================================================
160
+
161
+ export const HotStorage = {
162
+ Tag: HotStorageTag,
163
+ make,
164
+ InMemory,
165
+ };
package/src/Metrics.ts ADDED
@@ -0,0 +1,163 @@
1
+ /**
2
+ * @voidhash/mimic-effect - Metrics
3
+ *
4
+ * Observability metrics using Effect's Metric API.
5
+ */
6
+ import { Metric, MetricBoundaries } from "effect";
7
+
8
+ // =============================================================================
9
+ // Connection Metrics
10
+ // =============================================================================
11
+
12
+ /**
13
+ * Current active WebSocket connections
14
+ */
15
+ export const connectionsActive = Metric.gauge("mimic.connections.active");
16
+
17
+ /**
18
+ * Total connections over lifetime
19
+ */
20
+ export const connectionsTotal = Metric.counter("mimic.connections.total");
21
+
22
+ /**
23
+ * Connection duration histogram (milliseconds)
24
+ */
25
+ export const connectionsDuration = Metric.histogram(
26
+ "mimic.connections.duration_ms",
27
+ MetricBoundaries.exponential({
28
+ start: 100,
29
+ factor: 2,
30
+ count: 15, // Up to ~3.2 million ms (~53 minutes)
31
+ })
32
+ );
33
+
34
+ /**
35
+ * Connection errors (auth failures, etc.)
36
+ */
37
+ export const connectionsErrors = Metric.counter("mimic.connections.errors");
38
+
39
+ // =============================================================================
40
+ // Document Metrics
41
+ // =============================================================================
42
+
43
+ /**
44
+ * Documents currently in memory
45
+ */
46
+ export const documentsActive = Metric.gauge("mimic.documents.active");
47
+
48
+ /**
49
+ * New documents created
50
+ */
51
+ export const documentsCreated = Metric.counter("mimic.documents.created");
52
+
53
+ /**
54
+ * Documents restored from storage
55
+ */
56
+ export const documentsRestored = Metric.counter("mimic.documents.restored");
57
+
58
+ /**
59
+ * Documents garbage collected (evicted)
60
+ */
61
+ export const documentsEvicted = Metric.counter("mimic.documents.evicted");
62
+
63
+ // =============================================================================
64
+ // Transaction Metrics
65
+ // =============================================================================
66
+
67
+ /**
68
+ * Successfully processed transactions
69
+ */
70
+ export const transactionsProcessed = Metric.counter(
71
+ "mimic.transactions.processed"
72
+ );
73
+
74
+ /**
75
+ * Rejected transactions
76
+ */
77
+ export const transactionsRejected = Metric.counter(
78
+ "mimic.transactions.rejected"
79
+ );
80
+
81
+ /**
82
+ * Transaction processing latency histogram (milliseconds)
83
+ */
84
+ export const transactionsLatency = Metric.histogram(
85
+ "mimic.transactions.latency_ms",
86
+ MetricBoundaries.exponential({
87
+ start: 0.1,
88
+ factor: 2,
89
+ count: 15, // Up to ~1638 ms
90
+ })
91
+ );
92
+
93
+ // =============================================================================
94
+ // Storage Metrics
95
+ // =============================================================================
96
+
97
+ /**
98
+ * Snapshots saved to ColdStorage
99
+ */
100
+ export const storageSnapshots = Metric.counter("mimic.storage.snapshots");
101
+
102
+ /**
103
+ * Snapshot save duration histogram (milliseconds)
104
+ */
105
+ export const storageSnapshotLatency = Metric.histogram(
106
+ "mimic.storage.snapshot_latency_ms",
107
+ MetricBoundaries.exponential({
108
+ start: 1,
109
+ factor: 2,
110
+ count: 12, // Up to ~4 seconds
111
+ })
112
+ );
113
+
114
+ /**
115
+ * WAL entries written to HotStorage
116
+ */
117
+ export const storageWalAppends = Metric.counter("mimic.storage.wal_appends");
118
+
119
+ // =============================================================================
120
+ // Presence Metrics
121
+ // =============================================================================
122
+
123
+ /**
124
+ * Presence set operations
125
+ */
126
+ export const presenceUpdates = Metric.counter("mimic.presence.updates");
127
+
128
+ /**
129
+ * Active presence entries
130
+ */
131
+ export const presenceActive = Metric.gauge("mimic.presence.active");
132
+
133
+ // =============================================================================
134
+ // Export namespace
135
+ // =============================================================================
136
+
137
+ export const MimicMetrics = {
138
+ // Connection
139
+ connectionsActive,
140
+ connectionsTotal,
141
+ connectionsDuration,
142
+ connectionsErrors,
143
+
144
+ // Document
145
+ documentsActive,
146
+ documentsCreated,
147
+ documentsRestored,
148
+ documentsEvicted,
149
+
150
+ // Transaction
151
+ transactionsProcessed,
152
+ transactionsRejected,
153
+ transactionsLatency,
154
+
155
+ // Storage
156
+ storageSnapshots,
157
+ storageSnapshotLatency,
158
+ storageWalAppends,
159
+
160
+ // Presence
161
+ presenceUpdates,
162
+ presenceActive,
163
+ };
@@ -1,44 +1,39 @@
1
1
  /**
2
- * @since 0.0.1
3
- * Authentication service interface for Mimic connections.
4
- * Provides pluggable authentication adapters.
2
+ * @voidhash/mimic-effect - MimicAuthService
3
+ *
4
+ * Authentication and authorization service interface and implementations.
5
5
  */
6
- import * as Effect from "effect/Effect";
7
- import * as Context from "effect/Context";
8
- import * as Layer from "effect/Layer";
6
+ import { Context, Effect, Layer } from "effect";
7
+ import type { AuthContext, Permission } from "./Types";
8
+ import { AuthenticationError } from "./Errors";
9
9
 
10
10
  // =============================================================================
11
- // Authentication Types
11
+ // MimicAuthService Interface
12
12
  // =============================================================================
13
13
 
14
14
  /**
15
- * Result of an authentication attempt.
16
- */
17
- export type AuthResult =
18
- | { readonly success: true; readonly userId?: string }
19
- | { readonly success: false; readonly error: string };
20
-
21
- /**
22
- * Authentication handler function type.
23
- * Can be synchronous or return a Promise.
24
- */
25
- export type AuthHandler = (token: string) => Promise<AuthResult> | AuthResult;
26
-
27
- // =============================================================================
28
- // Auth Service Interface
29
- // =============================================================================
30
-
31
- /**
32
- * Authentication service interface.
33
- * Implementations can authenticate connections using various methods (JWT, API keys, etc.)
15
+ * MimicAuthService interface for authentication and authorization.
16
+ *
17
+ * The `authenticate` method receives the token from the client's auth message
18
+ * and the document ID being accessed. It should return an AuthContext on success
19
+ * or fail with AuthenticationError on failure.
20
+ *
21
+ * The permission in AuthContext determines what the user can do:
22
+ * - "read": Can subscribe, receive transactions, get snapshots
23
+ * - "write": All of the above, plus can submit transactions and set presence
34
24
  */
35
25
  export interface MimicAuthService {
36
26
  /**
37
- * Authenticate a connection using the provided token.
38
- * @param token - The authentication token provided by the client
39
- * @returns The authentication result
27
+ * Authenticate a connection and return authorization context.
28
+ *
29
+ * @param token - The token provided by the client
30
+ * @param documentId - The document ID being accessed
31
+ * @returns AuthContext with userId and permission level
40
32
  */
41
- readonly authenticate: (token: string) => Effect.Effect<AuthResult>;
33
+ readonly authenticate: (
34
+ token: string,
35
+ documentId: string
36
+ ) => Effect.Effect<AuthContext, AuthenticationError>;
42
37
  }
43
38
 
44
39
  // =============================================================================
@@ -46,58 +41,125 @@ export interface MimicAuthService {
46
41
  // =============================================================================
47
42
 
48
43
  /**
49
- * Context tag for MimicAuthService service.
44
+ * Context tag for MimicAuthService
50
45
  */
51
46
  export class MimicAuthServiceTag extends Context.Tag(
52
- "@voidhash/mimic-server-effect/MimicAuthService"
47
+ "@voidhash/mimic-effect/MimicAuthService"
53
48
  )<MimicAuthServiceTag, MimicAuthService>() {}
54
49
 
55
50
  // =============================================================================
56
- // Layer Constructors
51
+ // Factory
57
52
  // =============================================================================
58
53
 
59
54
  /**
60
- * Create a MimicAuthService layer from an auth handler function.
61
- */
62
- export const layer = (options: {
63
- readonly authHandler: AuthHandler;
64
- }): Layer.Layer<MimicAuthServiceTag> =>
65
- Layer.succeed(MimicAuthServiceTag, {
66
- authenticate: (token: string) =>
67
- Effect.promise(() => Promise.resolve(options.authHandler(token))),
68
- });
69
-
70
- /**
71
- * Create a MimicAuthService layer from an auth service implementation.
72
- */
73
- export const layerService = (service: MimicAuthService): Layer.Layer<MimicAuthServiceTag> =>
74
- Layer.succeed(MimicAuthServiceTag, service);
75
-
76
- /**
77
- * Create a MimicAuthService layer from an Effect that produces an auth service.
55
+ * Create a MimicAuthService layer from an Effect that produces the service.
56
+ *
57
+ * This allows you to access other Effect services when implementing authentication.
58
+ *
59
+ * @example
60
+ * ```typescript
61
+ * const Auth = MimicAuthService.make(
62
+ * Effect.gen(function*() {
63
+ * const db = yield* DatabaseService
64
+ * const jwt = yield* JwtService
65
+ *
66
+ * return {
67
+ * authenticate: (token, documentId) =>
68
+ * Effect.gen(function*() {
69
+ * const payload = yield* jwt.verify(token).pipe(
70
+ * Effect.mapError(() => new AuthenticationError({ reason: "Invalid token" }))
71
+ * )
72
+ *
73
+ * const permission = yield* db.getDocumentPermission(payload.userId, documentId)
74
+ *
75
+ * return { userId: payload.userId, permission }
76
+ * })
77
+ * }
78
+ * })
79
+ * )
80
+ * ```
78
81
  */
79
- export const layerEffect = <E, R>(
82
+ export const make = <E, R>(
80
83
  effect: Effect.Effect<MimicAuthService, E, R>
81
84
  ): Layer.Layer<MimicAuthServiceTag, E, R> =>
82
85
  Layer.effect(MimicAuthServiceTag, effect);
83
86
 
84
87
  // =============================================================================
85
- // Helper Functions
88
+ // NoAuth Implementation
86
89
  // =============================================================================
87
90
 
88
91
  /**
89
- * Create an auth service from an auth handler function.
92
+ * No-authentication implementation.
93
+ *
94
+ * Everyone gets write access with userId "anonymous".
95
+ * ONLY USE FOR DEVELOPMENT/TESTING.
90
96
  */
91
- export const make = (authHandler: AuthHandler): MimicAuthService => ({
92
- authenticate: (token: string) =>
93
- Effect.promise(() => Promise.resolve(authHandler(token))),
94
- });
97
+ export namespace NoAuth {
98
+ /**
99
+ * Create a NoAuth layer.
100
+ * All connections are authenticated with write permission.
101
+ */
102
+ export const make = (): Layer.Layer<MimicAuthServiceTag> =>
103
+ Layer.succeed(MimicAuthServiceTag, {
104
+ authenticate: (_token, _documentId) =>
105
+ Effect.succeed({
106
+ userId: "anonymous",
107
+ permission: "write" as const,
108
+ }),
109
+ });
110
+ }
111
+
112
+ // =============================================================================
113
+ // Static Implementation
114
+ // =============================================================================
95
115
 
96
116
  /**
97
- * Create an auth service from an Effect-based authenticate function.
117
+ * Static permissions implementation.
118
+ *
119
+ * Permissions are defined at configuration time.
120
+ * The token is treated as the userId.
98
121
  */
99
- export const makeEffect = (
100
- authenticate: (token: string) => Effect.Effect<AuthResult>
101
- ): MimicAuthService => ({
102
- authenticate,
103
- });
122
+ export namespace Static {
123
+ export interface Options {
124
+ /**
125
+ * Map of userId (token) to permission level
126
+ */
127
+ readonly permissions: Record<string, Permission>;
128
+ /**
129
+ * Default permission for users not in the permissions map.
130
+ * If undefined, unknown users will fail authentication.
131
+ */
132
+ readonly defaultPermission?: Permission;
133
+ }
134
+
135
+ /**
136
+ * Create a Static auth layer.
137
+ * The token is treated as the userId, and permissions are looked up from the config.
138
+ */
139
+ export const make = (options: Options): Layer.Layer<MimicAuthServiceTag> =>
140
+ Layer.succeed(MimicAuthServiceTag, {
141
+ authenticate: (token, _documentId) => {
142
+ const permission = options.permissions[token] ?? options.defaultPermission;
143
+ if (permission === undefined) {
144
+ return Effect.fail(
145
+ new AuthenticationError({ reason: "Unknown user" })
146
+ );
147
+ }
148
+ return Effect.succeed({
149
+ userId: token,
150
+ permission,
151
+ });
152
+ },
153
+ });
154
+ }
155
+
156
+ // =============================================================================
157
+ // Re-export namespace
158
+ // =============================================================================
159
+
160
+ export const MimicAuthService = {
161
+ Tag: MimicAuthServiceTag,
162
+ make,
163
+ NoAuth,
164
+ Static,
165
+ };