@shivaedev/identity 0.0.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (163) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +516 -1
  3. package/dist/identity.d.ts +36 -0
  4. package/dist/identity.d.ts.map +1 -0
  5. package/dist/identity.js +12 -0
  6. package/dist/identity.js.map +1 -0
  7. package/dist/middleware.d.ts +21 -0
  8. package/dist/middleware.d.ts.map +1 -0
  9. package/dist/middleware.js +20 -0
  10. package/dist/middleware.js.map +1 -0
  11. package/dist/permission.d.ts +11 -0
  12. package/dist/permission.d.ts.map +1 -0
  13. package/dist/permission.js +17 -0
  14. package/dist/permission.js.map +1 -0
  15. package/dist/server/adapters/betterAuthSessions.d.ts +14 -0
  16. package/dist/server/adapters/betterAuthSessions.d.ts.map +1 -0
  17. package/dist/server/adapters/betterAuthSessions.js +24 -0
  18. package/dist/server/adapters/betterAuthSessions.js.map +1 -0
  19. package/dist/server/better-auth/allowedAccounts.d.ts +10 -0
  20. package/dist/server/better-auth/allowedAccounts.d.ts.map +1 -0
  21. package/dist/server/better-auth/allowedAccounts.js +20 -0
  22. package/dist/server/better-auth/allowedAccounts.js.map +1 -0
  23. package/dist/server/better-auth/bearerSessions.d.ts +9 -0
  24. package/dist/server/better-auth/bearerSessions.d.ts.map +1 -0
  25. package/dist/server/better-auth/bearerSessions.js +38 -0
  26. package/dist/server/better-auth/bearerSessions.js.map +1 -0
  27. package/dist/server/better-auth/clientIp.d.ts +8 -0
  28. package/dist/server/better-auth/clientIp.d.ts.map +1 -0
  29. package/dist/server/better-auth/clientIp.js +28 -0
  30. package/dist/server/better-auth/clientIp.js.map +1 -0
  31. package/dist/server/better-auth/memoryRateLimit.d.ts +3 -0
  32. package/dist/server/better-auth/memoryRateLimit.d.ts.map +1 -0
  33. package/dist/server/better-auth/memoryRateLimit.js +46 -0
  34. package/dist/server/better-auth/memoryRateLimit.js.map +1 -0
  35. package/dist/server/better-auth/mount.d.ts +265 -0
  36. package/dist/server/better-auth/mount.d.ts.map +1 -0
  37. package/dist/server/better-auth/mount.js +111 -0
  38. package/dist/server/better-auth/mount.js.map +1 -0
  39. package/dist/server/better-auth/mountRoutes.d.ts +23 -0
  40. package/dist/server/better-auth/mountRoutes.d.ts.map +1 -0
  41. package/dist/server/better-auth/mountRoutes.js +51 -0
  42. package/dist/server/better-auth/mountRoutes.js.map +1 -0
  43. package/dist/server/better-auth/mountedSessions.d.ts +15 -0
  44. package/dist/server/better-auth/mountedSessions.d.ts.map +1 -0
  45. package/dist/server/better-auth/mountedSessions.js +12 -0
  46. package/dist/server/better-auth/mountedSessions.js.map +1 -0
  47. package/dist/server/better-auth/revokeUnadmitted.d.ts +10 -0
  48. package/dist/server/better-auth/revokeUnadmitted.d.ts.map +1 -0
  49. package/dist/server/better-auth/revokeUnadmitted.js +29 -0
  50. package/dist/server/better-auth/revokeUnadmitted.js.map +1 -0
  51. package/dist/server/better-auth/sessionPermissions.d.ts +3 -0
  52. package/dist/server/better-auth/sessionPermissions.d.ts.map +1 -0
  53. package/dist/server/better-auth/sessionPermissions.js +16 -0
  54. package/dist/server/better-auth/sessionPermissions.js.map +1 -0
  55. package/dist/server/better-auth/signInGate.d.ts +37 -0
  56. package/dist/server/better-auth/signInGate.d.ts.map +1 -0
  57. package/dist/server/better-auth/signInGate.js +38 -0
  58. package/dist/server/better-auth/signInGate.js.map +1 -0
  59. package/dist/server/better-auth/signInPolicy.d.ts +193 -0
  60. package/dist/server/better-auth/signInPolicy.d.ts.map +1 -0
  61. package/dist/server/better-auth/signInPolicy.js +61 -0
  62. package/dist/server/better-auth/signInPolicy.js.map +1 -0
  63. package/dist/server/better-auth/testDatabase.d.ts +19 -0
  64. package/dist/server/better-auth/testDatabase.d.ts.map +1 -0
  65. package/dist/server/better-auth/testDatabase.js +24 -0
  66. package/dist/server/better-auth/testDatabase.js.map +1 -0
  67. package/dist/server/better-auth/testing.d.ts +24 -0
  68. package/dist/server/better-auth/testing.d.ts.map +1 -0
  69. package/dist/server/better-auth/testing.js +21 -0
  70. package/dist/server/better-auth/testing.js.map +1 -0
  71. package/dist/server/better-auth/userProfiles.d.ts +14 -0
  72. package/dist/server/better-auth/userProfiles.d.ts.map +1 -0
  73. package/dist/server/better-auth/userProfiles.js +20 -0
  74. package/dist/server/better-auth/userProfiles.js.map +1 -0
  75. package/dist/server/commandChanges.d.ts +5 -0
  76. package/dist/server/commandChanges.d.ts.map +1 -0
  77. package/dist/server/commandChanges.js +44 -0
  78. package/dist/server/commandChanges.js.map +1 -0
  79. package/dist/server/local.d.ts +10 -0
  80. package/dist/server/local.d.ts.map +1 -0
  81. package/dist/server/local.js +93 -0
  82. package/dist/server/local.js.map +1 -0
  83. package/dist/server/origin.d.ts +15 -0
  84. package/dist/server/origin.d.ts.map +1 -0
  85. package/dist/server/origin.js +16 -0
  86. package/dist/server/origin.js.map +1 -0
  87. package/dist/server/permissions.d.ts +9 -0
  88. package/dist/server/permissions.d.ts.map +1 -0
  89. package/dist/server/permissions.js +31 -0
  90. package/dist/server/permissions.js.map +1 -0
  91. package/dist/server/proxyRanges.d.ts +4 -0
  92. package/dist/server/proxyRanges.d.ts.map +1 -0
  93. package/dist/server/proxyRanges.js +62 -0
  94. package/dist/server/proxyRanges.js.map +1 -0
  95. package/dist/server/resolveRole.d.ts +11 -0
  96. package/dist/server/resolveRole.d.ts.map +1 -0
  97. package/dist/server/resolveRole.js +6 -0
  98. package/dist/server/resolveRole.js.map +1 -0
  99. package/dist/server/revalidateStream.d.ts +6 -0
  100. package/dist/server/revalidateStream.d.ts.map +1 -0
  101. package/dist/server/revalidateStream.js +19 -0
  102. package/dist/server/revalidateStream.js.map +1 -0
  103. package/dist/server/session.d.ts +39 -0
  104. package/dist/server/session.d.ts.map +1 -0
  105. package/dist/server/session.js +73 -0
  106. package/dist/server/session.js.map +1 -0
  107. package/dist/server/sessionKeys.d.ts +10 -0
  108. package/dist/server/sessionKeys.d.ts.map +1 -0
  109. package/dist/server/sessionKeys.js +22 -0
  110. package/dist/server/sessionKeys.js.map +1 -0
  111. package/dist/server/sessionProfile.d.ts +3 -0
  112. package/dist/server/sessionProfile.d.ts.map +1 -0
  113. package/dist/server/sessionProfile.js +4 -0
  114. package/dist/server/sessionProfile.js.map +1 -0
  115. package/dist/server/tracing.d.ts +9 -0
  116. package/dist/server/tracing.d.ts.map +1 -0
  117. package/dist/server/tracing.js +51 -0
  118. package/dist/server/tracing.js.map +1 -0
  119. package/dist/server/transportHeaders.d.ts +4 -0
  120. package/dist/server/transportHeaders.d.ts.map +1 -0
  121. package/dist/server/transportHeaders.js +6 -0
  122. package/dist/server/transportHeaders.js.map +1 -0
  123. package/dist/server/trustedProxy.d.ts +7 -0
  124. package/dist/server/trustedProxy.d.ts.map +1 -0
  125. package/dist/server/trustedProxy.js +19 -0
  126. package/dist/server/trustedProxy.js.map +1 -0
  127. package/dist/server/trustedProxyRanges.d.ts +2 -0
  128. package/dist/server/trustedProxyRanges.d.ts.map +1 -0
  129. package/dist/server/trustedProxyRanges.js +5 -0
  130. package/dist/server/trustedProxyRanges.js.map +1 -0
  131. package/package.json +86 -4
  132. package/src/identity.ts +26 -0
  133. package/src/middleware.ts +38 -0
  134. package/src/permission.ts +24 -0
  135. package/src/server/adapters/betterAuthSessions.ts +44 -0
  136. package/src/server/better-auth/allowedAccounts.ts +29 -0
  137. package/src/server/better-auth/bearerSessions.ts +50 -0
  138. package/src/server/better-auth/clientIp.ts +34 -0
  139. package/src/server/better-auth/memoryRateLimit.ts +56 -0
  140. package/src/server/better-auth/mount.ts +146 -0
  141. package/src/server/better-auth/mountRoutes.ts +73 -0
  142. package/src/server/better-auth/mountedSessions.ts +21 -0
  143. package/src/server/better-auth/revokeUnadmitted.ts +41 -0
  144. package/src/server/better-auth/sessionPermissions.ts +18 -0
  145. package/src/server/better-auth/signInGate.ts +77 -0
  146. package/src/server/better-auth/signInPolicy.ts +72 -0
  147. package/src/server/better-auth/testDatabase.ts +27 -0
  148. package/src/server/better-auth/testing.ts +35 -0
  149. package/src/server/better-auth/userProfiles.ts +37 -0
  150. package/src/server/commandChanges.ts +52 -0
  151. package/src/server/local.ts +100 -0
  152. package/src/server/origin.ts +26 -0
  153. package/src/server/permissions.ts +51 -0
  154. package/src/server/proxyRanges.ts +61 -0
  155. package/src/server/resolveRole.ts +7 -0
  156. package/src/server/revalidateStream.ts +25 -0
  157. package/src/server/session.ts +148 -0
  158. package/src/server/sessionKeys.ts +25 -0
  159. package/src/server/sessionProfile.ts +5 -0
  160. package/src/server/tracing.ts +68 -0
  161. package/src/server/transportHeaders.ts +8 -0
  162. package/src/server/trustedProxy.ts +22 -0
  163. package/src/server/trustedProxyRanges.ts +5 -0
@@ -0,0 +1,51 @@
1
+ import { Cause, Context, Effect, Exit, Layer, Option, Predicate, Random } from "effect";
2
+ import { Headers } from "effect/http";
3
+ import { KeyScope } from "@shivaedev/contract/serverMetadata.ts";
4
+ import { redact } from "@shivaedev/errors/redact.ts";
5
+ import { redactCause } from "@shivaedev/errors/redactCause.ts";
6
+ import { isSensitiveKey, REDACTED, redactText } from "@shivaedev/errors/sensitive.ts";
7
+ import { RequestId } from "#identity.ts";
8
+ import { RequestTracing } from "#middleware.ts";
9
+ import { commandChanges } from "#server/commandChanges.ts";
10
+ const ACCEPTED_REQUEST_ID = /^[A-Za-z0-9._:-]{1,128}$/u;
11
+ function hex(byte) {
12
+ return byte.toString(16).padStart(2, "0");
13
+ }
14
+ const generateRequestId = Effect.map(Effect.all(Array.from({ length: 16 }, () => Random.nextIntBetween(0, 255))), (bytes) => bytes.map(hex).join(""));
15
+ function requestIdOf(headers, header) {
16
+ return Option.match(Option.filter(Headers.get(headers, header), (value) => ACCEPTED_REQUEST_ID.test(value)), {
17
+ onNone: () => generateRequestId,
18
+ onSome: Effect.succeed,
19
+ });
20
+ }
21
+ function failureTag(cause, sensitive = isSensitiveKey) {
22
+ return Option.match(Cause.findErrorOption(cause), {
23
+ onNone: () => "unknown",
24
+ onSome: (error) => {
25
+ if (!(Predicate.hasProperty(error, "_tag") && Predicate.isString(error._tag))) {
26
+ return "unknown";
27
+ }
28
+ return sensitive("_tag") ? REDACTED : redactText(error._tag, sensitive);
29
+ },
30
+ });
31
+ }
32
+ function logFailure(cause, payload, sensitive) {
33
+ if (Cause.hasInterruptsOnly(cause)) {
34
+ return Effect.annotateCurrentSpan("rpc.interrupted", true).pipe(Effect.andThen(Effect.logDebug("RPC interrupted")), Effect.annotateLogs({ "rpc.payload": redact(payload, sensitive) }));
35
+ }
36
+ const annotations = { "rpc.payload": redact(payload, sensitive) };
37
+ if (!Cause.hasDies(cause)) {
38
+ return Effect.annotateLogs(Effect.logInfo("RPC failure"), { ...annotations, "rpc.failure": failureTag(cause, sensitive) });
39
+ }
40
+ const defects = cause.reasons.filter(Cause.isDieReason).map((reason) => redact(reason.defect, sensitive));
41
+ return Effect.annotateLogs(Effect.logError("RPC defect"), { ...annotations, "rpc.defect": defects });
42
+ }
43
+ export function requestTracingLayer(options = {}) {
44
+ return Layer.succeed(RequestTracing, (effect, { headers, payload, rpc }) => Effect.gen(function* () {
45
+ const requestId = yield* requestIdOf(headers, options.header ?? "x-request-id");
46
+ yield* Effect.annotateCurrentSpan({ "request.id": requestId, "rpc.method": rpc._tag });
47
+ const invocation = Option.isSome(Context.getOption(rpc.annotations, KeyScope)) ? effect : commandChanges(effect, rpc, payload);
48
+ return yield* invocation.pipe(Effect.catchCauseIf(Cause.hasDies, (cause) => Effect.failCause(redactCause(cause, options.sensitive))), Effect.onExit((exit) => (Exit.isFailure(exit) ? logFailure(exit.cause, payload, options.sensitive) : Effect.void)), Effect.provideService(RequestId, requestId), Effect.annotateLogs({ requestId, "rpc.method": rpc._tag }), Effect.withLogSpan("rpc.duration"));
49
+ }));
50
+ }
51
+ //# sourceMappingURL=tracing.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tracing.js","sourceRoot":"","sources":["../../src/server/tracing.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,4 @@
1
+ import { Effect } from "effect";
2
+ import { type Headers } from "effect/http";
3
+ export declare function transportHeaders(rpcHeaders: Headers.Headers): Effect.Effect<Headers.Headers>;
4
+ //# sourceMappingURL=transportHeaders.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"transportHeaders.d.ts","sourceRoot":"","sources":["../../src/server/transportHeaders.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAU,MAAM,QAAQ,CAAC;AACxC,OAAO,EAAE,KAAK,OAAO,EAAqB,MAAM,aAAa,CAAC;AAC9D,wBAAgB,gBAAgB,CAAC,UAAU,EAAE,OAAO,CAAC,OAAO,GAAG,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC,OAAO,CAAC,CAK5F"}
@@ -0,0 +1,6 @@
1
+ import { Effect, Option } from "effect";
2
+ import { HttpServerRequest } from "effect/http";
3
+ export function transportHeaders(rpcHeaders) {
4
+ return Effect.map(Effect.serviceOption(HttpServerRequest.HttpServerRequest), Option.match({ onNone: () => rpcHeaders, onSome: (request) => request.headers }));
5
+ }
6
+ //# sourceMappingURL=transportHeaders.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"transportHeaders.js","sourceRoot":"","sources":["../../src/server/transportHeaders.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AACxC,OAAO,EAAgB,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAC9D,MAAM,UAAU,gBAAgB,CAAC,UAA2B;IAC3D,OAAO,MAAM,CAAC,GAAG,CAChB,MAAM,CAAC,aAAa,CAAC,iBAAiB,CAAC,iBAAiB,CAAC,EACzD,MAAM,CAAC,KAAK,CAAC,EAAE,MAAM,EAAE,GAAG,EAAE,CAAC,UAAU,EAAE,MAAM,EAAE,CAAC,OAAO,EAAE,EAAE,CAAC,OAAO,CAAC,OAAO,EAAE,CAAC,CAChF,CAAC;AACH,CAAC"}
@@ -0,0 +1,7 @@
1
+ import { BlockList } from "node:net";
2
+ import { Schema } from "effect";
3
+ export declare function proxyRanges(value: string): string[] | undefined;
4
+ export declare function proxyAddresses(value: string): BlockList | undefined;
5
+ export declare const TrustedProxy: Schema.String;
6
+ export declare function proxyMatches(addresses: BlockList, address: string): boolean;
7
+ //# sourceMappingURL=trustedProxy.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"trustedProxy.d.ts","sourceRoot":"","sources":["../../src/server/trustedProxy.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAQ,MAAM,UAAU,CAAC;AAC3C,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAGhC,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,SAAS,CAE/D;AAED,wBAAgB,cAAc,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,GAAG,SAAS,CAGnE;AACD,eAAO,MAAM,YAAY,eAIxB,CAAC;AACF,wBAAgB,YAAY,CAAC,SAAS,EAAE,SAAS,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAI3E"}
@@ -0,0 +1,19 @@
1
+ import { BlockList, isIP } from "node:net";
2
+ import { Schema } from "effect";
3
+ import { addProxyAddress, proxyRanges as normalizedRanges } from "./proxyRanges.js";
4
+ export function proxyRanges(value) {
5
+ return normalizedRanges(value);
6
+ }
7
+ export function proxyAddresses(value) {
8
+ const addresses = new BlockList();
9
+ return proxyRanges(value)?.every((entry) => addProxyAddress(addresses, entry)) ? addresses : undefined;
10
+ }
11
+ export const TrustedProxy = Schema.String.check(Schema.makeFilter((value) => proxyAddresses(value) !== undefined, {
12
+ message: "Expected a comma-separated list of IPv4 or IPv6 addresses or CIDR ranges",
13
+ }));
14
+ export function proxyMatches(addresses, address) {
15
+ const unmapped = address.replace(/^::ffff:(?=\d+\.)/iu, "");
16
+ const family = isIP(unmapped);
17
+ return family !== 0 && !unmapped.includes("%") && addresses.check(unmapped, family === 4 ? "ipv4" : "ipv6");
18
+ }
19
+ //# sourceMappingURL=trustedProxy.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"trustedProxy.js","sourceRoot":"","sources":["../../src/server/trustedProxy.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,IAAI,EAAE,MAAM,UAAU,CAAC;AAC3C,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAChC,OAAO,EAAE,eAAe,EAAE,WAAW,IAAI,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AAEpF,MAAM,UAAU,WAAW,CAAC,KAAa;IACxC,OAAO,gBAAgB,CAAC,KAAK,CAAC,CAAC;AAChC,CAAC;AAED,MAAM,UAAU,cAAc,CAAC,KAAa;IAC3C,MAAM,SAAS,GAAG,IAAI,SAAS,EAAE,CAAC;IAClC,OAAO,WAAW,CAAC,KAAK,CAAC,EAAE,KAAK,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,eAAe,CAAC,SAAS,EAAE,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC;AACxG,CAAC;AACD,MAAM,CAAC,MAAM,YAAY,GAAG,MAAM,CAAC,MAAM,CAAC,KAAK,CAC9C,MAAM,CAAC,UAAU,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,cAAc,CAAC,KAAK,CAAC,KAAK,SAAS,EAAE;IACjE,OAAO,EAAE,0EAA0E;CACnF,CAAC,CACF,CAAC;AACF,MAAM,UAAU,YAAY,CAAC,SAAoB,EAAE,OAAe;IACjE,MAAM,QAAQ,GAAG,OAAO,CAAC,OAAO,CAAC,qBAAqB,EAAE,EAAE,CAAC,CAAC;IAC5D,MAAM,MAAM,GAAG,IAAI,CAAC,QAAQ,CAAC,CAAC;IAC9B,OAAO,MAAM,KAAK,CAAC,IAAI,CAAC,QAAQ,CAAC,QAAQ,CAAC,GAAG,CAAC,IAAI,SAAS,CAAC,KAAK,CAAC,QAAQ,EAAE,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC;AAC7G,CAAC"}
@@ -0,0 +1,2 @@
1
+ export declare function trustedProxyRanges(trustedProxy?: string): string[];
2
+ //# sourceMappingURL=trustedProxyRanges.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"trustedProxyRanges.d.ts","sourceRoot":"","sources":["../../src/server/trustedProxyRanges.ts"],"names":[],"mappings":"AAEA,wBAAgB,kBAAkB,CAAC,YAAY,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,CAElE"}
@@ -0,0 +1,5 @@
1
+ import { proxyRanges } from "./proxyRanges.js";
2
+ export function trustedProxyRanges(trustedProxy) {
3
+ return trustedProxy === undefined ? [] : (proxyRanges(trustedProxy) ?? []);
4
+ }
5
+ //# sourceMappingURL=trustedProxyRanges.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"trustedProxyRanges.js","sourceRoot":"","sources":["../../src/server/trustedProxyRanges.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAE/C,MAAM,UAAU,kBAAkB,CAAC,YAAqB;IACvD,OAAO,YAAY,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,WAAW,CAAC,YAAY,CAAC,IAAI,EAAE,CAAC,CAAC;AAC5E,CAAC"}
package/package.json CHANGED
@@ -1,10 +1,92 @@
1
1
  {
2
2
  "name": "@shivaedev/identity",
3
- "version": "0.0.0",
4
- "description": "Placeholder for the ShivaeDev identity package.",
5
- "license": "MIT",
3
+ "version": "0.2.0",
4
+ "description": "Request identity, sessions, transport trust and permission enforcement for Effect RPC servers",
5
+ "keywords": [
6
+ "effect"
7
+ ],
8
+ "homepage": "https://github.com/ShivaeDev/platform/tree/main/packages/identity#readme",
9
+ "bugs": {
10
+ "url": "https://github.com/ShivaeDev/platform/issues"
11
+ },
6
12
  "repository": {
7
13
  "type": "git",
8
- "url": "git+https://github.com/ShivaeDev/platform.git"
14
+ "url": "git+https://github.com/ShivaeDev/platform.git",
15
+ "directory": "packages/identity"
16
+ },
17
+ "license": "MIT",
18
+ "sideEffects": false,
19
+ "type": "module",
20
+ "imports": {
21
+ "#test/*": "./src/test-support/*",
22
+ "#*.ts": {
23
+ "source": "./src/*.ts",
24
+ "types": "./dist/*.d.ts",
25
+ "default": "./dist/*.js"
26
+ }
27
+ },
28
+ "exports": {
29
+ "./*.ts": {
30
+ "source": "./src/*.ts",
31
+ "types": "./dist/*.d.ts",
32
+ "default": "./dist/*.js"
33
+ },
34
+ "./package.json": "./package.json"
35
+ },
36
+ "files": [
37
+ "dist",
38
+ "src",
39
+ "!src/**/*.test.ts",
40
+ "!src/**/*.test.tsx",
41
+ "!src/**/*.spec.ts",
42
+ "!src/**/*.spec.tsx",
43
+ "!src/**/test-support",
44
+ "README.md"
45
+ ],
46
+ "dependencies": {
47
+ "@shivaedev/access": "0.1.1",
48
+ "@shivaedev/contract": "0.18.0",
49
+ "@shivaedev/errors": "0.1.0",
50
+ "@shivaedev/sql": "0.23.0"
51
+ },
52
+ "devDependencies": {
53
+ "@effect/platform-node": "4.0.5",
54
+ "@effect/sql-pg": "4.0.5",
55
+ "@shivaedev/quality": "0.42.0",
56
+ "@shivaedev/test-sql": "0.10.1",
57
+ "@shivaedev/test-story": "0.21.1",
58
+ "@types/node": "24.19.2",
59
+ "@types/pg": "8.20.0",
60
+ "better-auth": "1.7.11",
61
+ "effect": "4.0.5",
62
+ "pg": "8.23.0",
63
+ "typescript": "7.0.2",
64
+ "vitest": "5.0.5"
65
+ },
66
+ "peerDependencies": {
67
+ "better-auth": "1.7.11",
68
+ "effect": "4.0.5",
69
+ "pg": "8.23.0"
70
+ },
71
+ "peerDependenciesMeta": {
72
+ "better-auth": {
73
+ "optional": true
74
+ },
75
+ "pg": {
76
+ "optional": true
77
+ }
78
+ },
79
+ "engines": {
80
+ "node": ">=24"
81
+ },
82
+ "publishConfig": {
83
+ "access": "public",
84
+ "provenance": true
85
+ },
86
+ "scripts": {
87
+ "build": "node --eval \"import('node:fs').then(({ rmSync }) => rmSync('dist', { force: true, recursive: true }))\" && tsc --project tsconfig.build.json",
88
+ "ready": "pnpm typecheck && pnpm test && pnpm build",
89
+ "test": "vitest run",
90
+ "typecheck": "tsc --noEmit"
9
91
  }
10
92
  }
@@ -0,0 +1,26 @@
1
+ import { Context, type Effect, type Option } from "effect";
2
+ import type { Permission } from "@shivaedev/access/permission.ts";
3
+ import type { AuthUnavailable, Unauthorized } from "@shivaedev/errors/taxonomy.ts";
4
+ export class RequestId extends Context.Service<RequestId, string>()("@shivaedev/identity/RequestId") {}
5
+ export interface IdentityValue {
6
+ readonly id: string;
7
+ }
8
+ export class Identity extends Context.Service<Identity, IdentityValue>()("@shivaedev/identity/Identity") {}
9
+ export class OptionalIdentity extends Context.Service<OptionalIdentity, Option.Option<IdentityValue>>()("@shivaedev/identity/OptionalIdentity") {}
10
+ export interface ProviderAccountValue {
11
+ readonly accountId: string;
12
+ readonly providerId: string;
13
+ }
14
+ export interface SessionUserValue extends IdentityValue {
15
+ readonly email: string;
16
+ readonly name: string;
17
+ readonly permissions?: readonly Permission[];
18
+ readonly role?: string | null | undefined;
19
+ }
20
+ export class SessionUser extends Context.Service<SessionUser, SessionUserValue>()("@shivaedev/identity/SessionUser") {}
21
+ export interface SessionUserWithProviderAccountsValue extends SessionUserValue {
22
+ readonly providerAccounts: Effect.Effect<readonly ProviderAccountValue[], AuthUnavailable | Unauthorized>;
23
+ }
24
+ export class SessionUserWithProviderAccounts extends Context.Service<SessionUserWithProviderAccounts, SessionUserWithProviderAccountsValue>()(
25
+ "@shivaedev/identity/SessionUserWithProviderAccounts",
26
+ ) {}
@@ -0,0 +1,38 @@
1
+ import { Schema } from "effect";
2
+ import { RpcMiddleware } from "effect/rpc";
3
+ import { AuthUnavailable, Forbidden, Unauthorized } from "@shivaedev/errors/taxonomy.ts";
4
+ import type { Identity, OptionalIdentity, RequestId, SessionUser, SessionUserWithProviderAccounts } from "./identity.ts";
5
+ export class RequestTracing extends RpcMiddleware.Service<
6
+ RequestTracing,
7
+ {
8
+ provides: RequestId;
9
+ }
10
+ >()("@shivaedev/identity/RequestTracing") {}
11
+ export class Authenticated extends RpcMiddleware.Service<
12
+ Authenticated,
13
+ {
14
+ provides: Identity;
15
+ }
16
+ >()("@shivaedev/identity/Authenticated", {
17
+ error: Schema.Union([Unauthorized, AuthUnavailable, Forbidden]),
18
+ }) {}
19
+ export class MaybeAuthenticated extends RpcMiddleware.Service<
20
+ MaybeAuthenticated,
21
+ {
22
+ provides: OptionalIdentity;
23
+ }
24
+ >()("@shivaedev/identity/MaybeAuthenticated", { error: Schema.Union([Unauthorized, AuthUnavailable, Forbidden]) }) {}
25
+ export class AuthenticatedUser extends RpcMiddleware.Service<
26
+ AuthenticatedUser,
27
+ {
28
+ provides: Identity | SessionUser;
29
+ }
30
+ >()("@shivaedev/identity/AuthenticatedUser", {
31
+ error: Schema.Union([Unauthorized, AuthUnavailable, Forbidden]),
32
+ }) {}
33
+ export class AuthenticatedUserWithProviderAccounts extends RpcMiddleware.Service<
34
+ AuthenticatedUserWithProviderAccounts,
35
+ { provides: Identity | SessionUser | SessionUserWithProviderAccounts }
36
+ >()("@shivaedev/identity/AuthenticatedUserWithProviderAccounts", {
37
+ error: Schema.Union([Unauthorized, AuthUnavailable, Forbidden]),
38
+ }) {}
@@ -0,0 +1,24 @@
1
+ import { Effect, Schema } from "effect";
2
+ import { RpcMiddleware } from "effect/rpc";
3
+ import type { Permission } from "@shivaedev/access/permission.ts";
4
+ import { AuthUnavailable, Forbidden, Unauthorized } from "@shivaedev/errors/taxonomy.ts";
5
+ import { type Identity, SessionUser, type SessionUserValue } from "./identity.ts";
6
+
7
+ const Failures = Schema.Union([Unauthorized, AuthUnavailable, Forbidden]);
8
+
9
+ export function permissionMiddleware<TSelf>() {
10
+ return <const TName extends string>(name: TName, permission: Permission) =>
11
+ Object.assign(RpcMiddleware.Service<TSelf, { provides: Identity | SessionUser }>()(name, { error: Failures }), { permission });
12
+ }
13
+
14
+ export function hasPermission(user: Pick<SessionUserValue, "id" | "permissions">, permission: Permission): boolean {
15
+ return user.permissions?.some((grant) => grant.name === permission.name && grant.strength === permission.strength) ?? false;
16
+ }
17
+
18
+ export function requirePermission(permission: Permission) {
19
+ return Effect.flatMap(SessionUser, (user) =>
20
+ hasPermission(user, permission)
21
+ ? Effect.succeed(user)
22
+ : Effect.fail(new Forbidden({ message: `Permission required: ${permission.name} (${permission.strength})` })),
23
+ );
24
+ }
@@ -0,0 +1,44 @@
1
+ import { Effect, Option, Predicate } from "effect";
2
+ import type { Headers } from "effect/http";
3
+ import { redact } from "@shivaedev/errors/redact.ts";
4
+ import { AuthUnavailable, Unauthorized } from "@shivaedev/errors/taxonomy.ts";
5
+ import type { ProviderAccountValue } from "#identity.ts";
6
+ import type { SessionProvider, SessionShape } from "#server/session.ts";
7
+ export type GetSession<TSession extends SessionShape> = (headers: globalThis.Headers) => Promise<TSession | null>;
8
+ export type GetProviderAccounts = (headers: globalThis.Headers) => Promise<readonly ProviderAccountValue[]>;
9
+ export type SessionWithProviderAccounts<TSession extends SessionShape> = TSession & {
10
+ readonly user: TSession["user"] & { readonly providerAccounts: Effect.Effect<readonly ProviderAccountValue[], AuthUnavailable | Unauthorized> };
11
+ };
12
+ function unavailable(cause: unknown) {
13
+ return Effect.andThen(
14
+ Effect.logError("Authentication provider failed", redact(cause)),
15
+ Effect.fail(new AuthUnavailable({ message: "Authentication is temporarily unavailable" })),
16
+ );
17
+ }
18
+ export function betterAuthSessions<TSession extends SessionShape>(getSession: GetSession<TSession>): SessionProvider<TSession>;
19
+ export function betterAuthSessions<TSession extends SessionShape>(
20
+ getSession: GetSession<TSession>,
21
+ getProviderAccounts: GetProviderAccounts,
22
+ ): SessionProvider<SessionWithProviderAccounts<TSession>>;
23
+ export function betterAuthSessions<TSession extends SessionShape>(getSession: GetSession<TSession>, getProviderAccounts?: GetProviderAccounts) {
24
+ const betterAuthSession = Effect.fn("PlatformRpc.betterAuthSession")(function* (headers: Headers.Headers) {
25
+ const transport = new globalThis.Headers(headers);
26
+ const session = yield* Effect.tryPromise(() => getSession(transport)).pipe(Effect.catch((error) => unavailable(error.cause)));
27
+ if (session === null || getProviderAccounts === undefined) {
28
+ return Option.fromNullishOr(session);
29
+ }
30
+ const providerAccounts = Effect.tryPromise({
31
+ catch: (cause) => cause,
32
+ try: () => getProviderAccounts(transport),
33
+ }).pipe(
34
+ Effect.catch(
35
+ (cause): Effect.Effect<never, AuthUnavailable | Unauthorized> =>
36
+ Predicate.hasProperty(cause, "statusCode") && cause.statusCode === 401
37
+ ? Effect.fail(new Unauthorized({ message: "Authentication required" }))
38
+ : unavailable(cause),
39
+ ),
40
+ );
41
+ return Option.some({ ...session, user: { ...session.user, providerAccounts } });
42
+ });
43
+ return { get: betterAuthSession };
44
+ }
@@ -0,0 +1,29 @@
1
+ import { Config, Schema, SchemaGetter } from "effect";
2
+
3
+ const ProviderAccount = Schema.Struct({ accountId: Schema.NonEmptyString, providerId: Schema.NonEmptyString });
4
+
5
+ const AccountEntry = Schema.String.pipe(
6
+ Schema.decodeTo(ProviderAccount, {
7
+ decode: SchemaGetter.transform((entry) => {
8
+ const separator = entry.indexOf(":");
9
+ return separator < 0
10
+ ? { accountId: "", providerId: entry }
11
+ : { accountId: entry.slice(separator + 1).trim(), providerId: entry.slice(0, separator).trim() };
12
+ }),
13
+ encode: SchemaGetter.transform(({ accountId, providerId }) => `${providerId}:${accountId}`),
14
+ }),
15
+ );
16
+
17
+ export const AllowedAccounts = Schema.String.pipe(
18
+ Schema.decodeTo(Schema.Array(AccountEntry).check(Schema.isMinLength(1)), {
19
+ decode: SchemaGetter.transform((value) =>
20
+ value
21
+ .split(",")
22
+ .map((entry) => entry.trim())
23
+ .filter((entry) => entry.length > 0),
24
+ ),
25
+ encode: SchemaGetter.transform((entries) => entries.join(",")),
26
+ }),
27
+ );
28
+
29
+ export const allowedAccounts = Config.schema(AllowedAccounts, "AUTH_ALLOWED_ACCOUNTS");
@@ -0,0 +1,50 @@
1
+ import type { BetterAuthOptions } from "better-auth";
2
+ import { bearer } from "better-auth/plugins/bearer";
3
+
4
+ export interface BearerSessionsOptions {
5
+ readonly origins: readonly string[];
6
+ }
7
+
8
+ export function withBearerSessions<TOptions extends BetterAuthOptions>(options: TOptions, enabled: boolean): TOptions {
9
+ return enabled ? { ...options, plugins: [...(options.plugins ?? []), bearer({ requireSignature: true })] } : options;
10
+ }
11
+
12
+ export function hasSessionCookie(headers: Headers, cookieName: string): boolean {
13
+ return (
14
+ headers
15
+ .get("cookie")
16
+ ?.split(";")
17
+ .some((cookie) => cookie.trim().startsWith(`${cookieName}=`)) === true
18
+ );
19
+ }
20
+
21
+ export function sessionHeaders(headers: Headers, cookieName: string): Headers {
22
+ const result = new Headers(headers);
23
+ if (hasSessionCookie(headers, cookieName)) {
24
+ result.delete("authorization");
25
+ } else if (/^bearer\s/iu.test(headers.get("authorization") ?? "")) {
26
+ result.delete("cookie");
27
+ }
28
+ return result;
29
+ }
30
+
31
+ export async function cookieSessionResponse(response: Response): Promise<Response> {
32
+ const headers = new Headers(response.headers);
33
+ headers.delete("set-auth-token");
34
+ const exposed = headers.get("access-control-expose-headers");
35
+ if (exposed !== null) {
36
+ headers.set(
37
+ "access-control-expose-headers",
38
+ exposed
39
+ .split(",")
40
+ .filter((header) => header.trim().toLowerCase() !== "set-auth-token")
41
+ .join(","),
42
+ );
43
+ }
44
+ if (!headers.get("content-type")?.includes("application/json")) {
45
+ return new Response(response.body, { headers, status: response.status });
46
+ }
47
+ const body: unknown = JSON.parse(await response.text(), (key, value: unknown) => (key === "token" ? undefined : value));
48
+ headers.delete("content-length");
49
+ return Response.json(body, { headers, status: response.status });
50
+ }
@@ -0,0 +1,34 @@
1
+ import { getIP } from "better-auth/api";
2
+ import { Option } from "effect";
3
+ import type { HttpServerRequest } from "effect/http";
4
+ import { trustedProxyPeer, trustedProxyRanges } from "#server/local.ts";
5
+
6
+ export const PEER_IP_HEADER = "x-platform-client-ip";
7
+
8
+ export function clientIpHeaders(options: {
9
+ readonly clientIpHeader: string;
10
+ readonly ipv6Subnet?: number | undefined;
11
+ readonly trustedProxy?: string | undefined;
12
+ }) {
13
+ const trustedPeer = trustedProxyPeer(options.trustedProxy);
14
+ const advanced = {
15
+ ipAddress: {
16
+ ipAddressHeaders: [options.clientIpHeader],
17
+ ...(options.ipv6Subnet === undefined ? {} : { ipv6Subnet: options.ipv6Subnet }),
18
+ trustedProxies: trustedProxyRanges(options.trustedProxy),
19
+ },
20
+ };
21
+ return (request: HttpServerRequest.HttpServerRequest, web: Request) => {
22
+ web.headers.delete(PEER_IP_HEADER);
23
+ const trusted = Option.exists(request.remoteAddress, trustedPeer);
24
+ const forwardedIp = trusted ? getIP(web, { advanced }) : null;
25
+ if (!trusted) {
26
+ web.headers.delete("x-forwarded-for");
27
+ web.headers.delete(options.clientIpHeader);
28
+ }
29
+ const address = forwardedIp ?? Option.getOrUndefined(request.remoteAddress);
30
+ if (address !== undefined) {
31
+ web.headers.set(PEER_IP_HEADER, address);
32
+ }
33
+ };
34
+ }
@@ -0,0 +1,56 @@
1
+ import type { BetterAuthRateLimitOptions } from "better-auth";
2
+ import { Clock, Effect } from "effect";
3
+
4
+ interface Bucket {
5
+ readonly count: number;
6
+ readonly expiresAt: number;
7
+ readonly lastRequest: number;
8
+ }
9
+
10
+ const MAX_ENTRIES = 100_000;
11
+
12
+ export const memoryRateLimit = Effect.gen(function* () {
13
+ const clock = yield* Clock.Clock;
14
+ const buckets = yield* Effect.acquireRelease(
15
+ Effect.sync(() => new Map<string, Bucket>()),
16
+ (entries) => Effect.sync(() => entries.clear()),
17
+ );
18
+ let nextExpiry = Number.POSITIVE_INFINITY;
19
+ function prune(now: number) {
20
+ if (now >= nextExpiry) {
21
+ nextExpiry = Number.POSITIVE_INFINITY;
22
+ for (const [entryKey, entry] of buckets) {
23
+ if (now >= entry.expiresAt) {
24
+ buckets.delete(entryKey);
25
+ } else {
26
+ nextExpiry = Math.min(nextExpiry, entry.expiresAt);
27
+ }
28
+ }
29
+ }
30
+ }
31
+ const storage: NonNullable<BetterAuthRateLimitOptions["customStorage"]> = {
32
+ consume(key, rule) {
33
+ const now = clock.currentTimeMillisUnsafe();
34
+ prune(now);
35
+ const bucket = buckets.get(key);
36
+ const window = rule.window * 1000;
37
+ if (bucket !== undefined && now - bucket.lastRequest < window && bucket.count >= rule.max) {
38
+ return Promise.resolve({ allowed: false, retryAfter: Math.ceil((bucket.lastRequest + window - now) / 1000) });
39
+ }
40
+ nextExpiry = Math.min(nextExpiry, now + window);
41
+ buckets.set(key, {
42
+ count: bucket === undefined || now - bucket.lastRequest >= window ? 1 : bucket.count + 1,
43
+ expiresAt: now + window,
44
+ lastRequest: now,
45
+ });
46
+ if (buckets.size > MAX_ENTRIES) {
47
+ for (const oldest of buckets.keys()) {
48
+ buckets.delete(oldest);
49
+ break;
50
+ }
51
+ }
52
+ return Promise.resolve({ allowed: true, retryAfter: null });
53
+ },
54
+ };
55
+ return storage;
56
+ });