aboutsummaryrefslogtreecommitdiff
path: root/src/lib
diff options
context:
space:
mode:
authorFuwn <[email protected]>2026-03-03 08:57:37 -0800
committerFuwn <[email protected]>2026-03-03 08:57:43 -0800
commitbb8a71326a85a3d70bad36a35d300c614d8e8b7f (patch)
tree7bb0803f3bf2bcc1378f0cfd03d53e4dc81aeefa /src/lib
parentfix(match): prevent cached airing injection mutation regressions (diff)
downloaddue.moe-bb8a71326a85a3d70bad36a35d300c614d8e8b7f.tar.xz
due.moe-bb8a71326a85a3d70bad36a35d300c614d8e8b7f.zip
chore(effect): add v4 cookie decode foundation and tests
Diffstat (limited to 'src/lib')
-rw-r--r--src/lib/Effect/authCookie.test.ts47
-rw-r--r--src/lib/Effect/authCookie.ts45
2 files changed, 92 insertions, 0 deletions
diff --git a/src/lib/Effect/authCookie.test.ts b/src/lib/Effect/authCookie.test.ts
new file mode 100644
index 00000000..2a27f0ce
--- /dev/null
+++ b/src/lib/Effect/authCookie.test.ts
@@ -0,0 +1,47 @@
+import { describe, expect, it } from "vitest";
+import { Result } from "effect";
+import {
+ decodeAuthCookieEither,
+ decodeAuthCookieOrThrow,
+} from "$lib/Effect/authCookie";
+
+describe("decodeAuthCookie", () => {
+ it("decodes a valid user cookie payload", () => {
+ const payload = JSON.stringify({
+ token_type: "Bearer",
+ expires_in: 3600,
+ access_token: "access-token",
+ refresh_token: "refresh-token",
+ });
+ const decoded = decodeAuthCookieEither(payload);
+
+ expect(Result.isSuccess(decoded)).toBe(true);
+
+ if (Result.isSuccess(decoded))
+ expect(decoded.success).toEqual({
+ tokenType: "Bearer",
+ expiresIn: 3600,
+ accessToken: "access-token",
+ refreshToken: "refresh-token",
+ });
+ });
+
+ it("returns a left when cookie JSON is invalid", () => {
+ const decoded = decodeAuthCookieEither("{oops");
+
+ expect(Result.isFailure(decoded)).toBe(true);
+ });
+
+ it("returns a left when required fields are missing", () => {
+ const payload = JSON.stringify({
+ token_type: "Bearer",
+ });
+ const decoded = decodeAuthCookieEither(payload);
+
+ expect(Result.isFailure(decoded)).toBe(true);
+ });
+
+ it("throws on invalid payload through decodeAuthCookieOrThrow", () => {
+ expect(() => decodeAuthCookieOrThrow("{oops")).toThrowError();
+ });
+});
diff --git a/src/lib/Effect/authCookie.ts b/src/lib/Effect/authCookie.ts
new file mode 100644
index 00000000..e716f5e9
--- /dev/null
+++ b/src/lib/Effect/authCookie.ts
@@ -0,0 +1,45 @@
+import type { AniListAuthorisation } from "$lib/Data/AniList/identity";
+import { Effect, Result, Schema } from "effect";
+
+const UserCookieSchema = Schema.Struct({
+ token_type: Schema.String,
+ expires_in: Schema.Number,
+ access_token: Schema.String,
+ refresh_token: Schema.String,
+});
+
+export const decodeAuthCookieEffect = (cookie: string) =>
+ Effect.gen(function* () {
+ const parsedCookie = yield* Effect.try({
+ try: () => JSON.parse(cookie) as unknown,
+ catch: (cause) => new Error("Invalid user cookie JSON", { cause }),
+ });
+ const decodedCookie = yield* Effect.try({
+ try: () => Schema.decodeUnknownSync(UserCookieSchema)(parsedCookie),
+ catch: (cause) => new Error("Invalid user cookie payload", { cause }),
+ });
+
+ return {
+ tokenType: decodedCookie.token_type,
+ expiresIn: decodedCookie.expires_in,
+ accessToken: decodedCookie.access_token,
+ refreshToken: decodedCookie.refresh_token,
+ } as AniListAuthorisation;
+ });
+
+export const decodeAuthCookieEither = (cookie: string) =>
+ Result.try({
+ try: () => Effect.runSync(decodeAuthCookieEffect(cookie)),
+ catch: (cause) =>
+ cause instanceof Error
+ ? cause
+ : new Error("Failed to decode user cookie", { cause }),
+ });
+
+export const decodeAuthCookieOrThrow = (
+ cookie: string,
+): AniListAuthorisation => {
+ const decoded = decodeAuthCookieEither(cookie);
+
+ return Result.getOrThrow(decoded);
+};