@wopr-network/platform-core 1.0.4 → 1.0.6
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.
- package/.env.example +5 -0
- package/dist/account/deletion-executor-repository.d.ts +17 -0
- package/dist/account/deletion-executor-repository.js +41 -0
- package/dist/account/deletion-executor-repository.test.d.ts +1 -0
- package/dist/account/deletion-executor-repository.test.js +89 -0
- package/dist/account/deletion-repository.d.ts +19 -0
- package/dist/account/deletion-repository.js +62 -0
- package/dist/account/deletion-repository.test.d.ts +1 -0
- package/dist/account/deletion-repository.test.js +85 -0
- package/dist/account/export-repository.d.ts +21 -0
- package/dist/account/export-repository.js +77 -0
- package/dist/account/export-repository.test.d.ts +1 -0
- package/dist/account/export-repository.test.js +109 -0
- package/dist/account/index.d.ts +4 -0
- package/dist/account/index.js +3 -0
- package/dist/account/repository-types.d.ts +38 -0
- package/dist/account/repository-types.js +4 -0
- package/dist/auth/auth-route-handler.test.d.ts +1 -0
- package/dist/auth/auth-route-handler.test.js +191 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +2 -0
- package/dist/metering/emitter.d.ts +2 -2
- package/dist/metering/emitter.js +4 -4
- package/dist/metering/emitter.test.js +7 -7
- package/dist/metering/metering.test.js +73 -73
- package/dist/metering/wal.d.ts +6 -4
- package/dist/metering/wal.js +14 -10
- package/dist/metering/wal.test.js +21 -0
- package/dist/security/redirect-allowlist.js +20 -1
- package/dist/security/redirect-allowlist.test.js +34 -0
- package/package.json +1 -1
- package/src/account/deletion-executor-repository.test.ts +109 -0
- package/src/account/deletion-executor-repository.ts +58 -0
- package/src/account/deletion-repository.test.ts +103 -0
- package/src/account/deletion-repository.ts +82 -0
- package/src/account/export-repository.test.ts +135 -0
- package/src/account/export-repository.ts +101 -0
- package/src/account/index.ts +14 -0
- package/src/account/repository-types.ts +46 -0
- package/src/auth/auth-route-handler.test.ts +243 -0
- package/src/index.ts +3 -0
- package/src/metering/emitter.test.ts +7 -7
- package/src/metering/emitter.ts +5 -5
- package/src/metering/metering.test.ts +75 -73
- package/src/metering/wal.test.ts +26 -0
- package/src/metering/wal.ts +14 -10
- package/src/security/redirect-allowlist.test.ts +41 -0
- package/src/security/redirect-allowlist.ts +19 -1
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import { Hono } from "hono";
|
|
2
|
+
import { describe, expect, it, vi } from "vitest";
|
|
3
|
+
/**
|
|
4
|
+
* Build a mock Auth whose `.handler` returns the given Response for any request.
|
|
5
|
+
* Also exposes `.api.getSession` for middleware compatibility.
|
|
6
|
+
*/
|
|
7
|
+
function mockAuthWithHandler(handlerResponse) {
|
|
8
|
+
const handler = vi.fn().mockResolvedValue(handlerResponse);
|
|
9
|
+
return {
|
|
10
|
+
handler,
|
|
11
|
+
api: {
|
|
12
|
+
getSession: vi.fn().mockResolvedValue(null),
|
|
13
|
+
},
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Mount auth.handler on a Hono app at /api/auth/* -- mirrors the pattern
|
|
18
|
+
* used by consuming apps (wopr-platform, wopr-platform-ui).
|
|
19
|
+
*/
|
|
20
|
+
function createAuthApp(auth) {
|
|
21
|
+
const app = new Hono();
|
|
22
|
+
app.on(["GET", "POST"], "/api/auth/*", (c) => {
|
|
23
|
+
return auth.handler(c.req.raw);
|
|
24
|
+
});
|
|
25
|
+
return app;
|
|
26
|
+
}
|
|
27
|
+
// ---------------------------------------------------------------------------
|
|
28
|
+
// GET /api/auth/get-session
|
|
29
|
+
// ---------------------------------------------------------------------------
|
|
30
|
+
describe("GET /api/auth/get-session", () => {
|
|
31
|
+
it("delegates to auth.handler and returns its response", async () => {
|
|
32
|
+
const sessionPayload = { user: { id: "u-1", email: "a@b.com" }, session: { id: "s-1" } };
|
|
33
|
+
const auth = mockAuthWithHandler(new Response(JSON.stringify(sessionPayload), {
|
|
34
|
+
status: 200,
|
|
35
|
+
headers: { "Content-Type": "application/json" },
|
|
36
|
+
}));
|
|
37
|
+
const app = createAuthApp(auth);
|
|
38
|
+
const res = await app.request("/api/auth/get-session");
|
|
39
|
+
expect(res.status).toBe(200);
|
|
40
|
+
const body = await res.json();
|
|
41
|
+
expect(body).toEqual(sessionPayload);
|
|
42
|
+
expect(auth.handler).toHaveBeenCalledOnce();
|
|
43
|
+
});
|
|
44
|
+
it("returns 401 when handler returns 401 (no session cookie)", async () => {
|
|
45
|
+
const auth = mockAuthWithHandler(new Response(JSON.stringify({ error: "Unauthorized" }), {
|
|
46
|
+
status: 401,
|
|
47
|
+
headers: { "Content-Type": "application/json" },
|
|
48
|
+
}));
|
|
49
|
+
const app = createAuthApp(auth);
|
|
50
|
+
const res = await app.request("/api/auth/get-session");
|
|
51
|
+
expect(res.status).toBe(401);
|
|
52
|
+
expect(auth.handler).toHaveBeenCalledOnce();
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
// ---------------------------------------------------------------------------
|
|
56
|
+
// POST /api/auth/sign-up/email
|
|
57
|
+
// ---------------------------------------------------------------------------
|
|
58
|
+
describe("POST /api/auth/sign-up/email", () => {
|
|
59
|
+
it("delegates sign-up request to auth.handler", async () => {
|
|
60
|
+
const created = { user: { id: "u-new", email: "new@test.com" } };
|
|
61
|
+
const auth = mockAuthWithHandler(new Response(JSON.stringify(created), {
|
|
62
|
+
status: 200,
|
|
63
|
+
headers: { "Content-Type": "application/json" },
|
|
64
|
+
}));
|
|
65
|
+
const app = createAuthApp(auth);
|
|
66
|
+
const res = await app.request("/api/auth/sign-up/email", {
|
|
67
|
+
method: "POST",
|
|
68
|
+
headers: { "Content-Type": "application/json" },
|
|
69
|
+
body: JSON.stringify({
|
|
70
|
+
name: "Test User",
|
|
71
|
+
email: "new@test.com",
|
|
72
|
+
password: "strongpassword123",
|
|
73
|
+
}),
|
|
74
|
+
});
|
|
75
|
+
expect(res.status).toBe(200);
|
|
76
|
+
expect(auth.handler).toHaveBeenCalledOnce();
|
|
77
|
+
// Verify the raw Request was forwarded
|
|
78
|
+
const forwardedReq = auth.handler.mock.calls[0][0];
|
|
79
|
+
expect(forwardedReq).toBeInstanceOf(Request);
|
|
80
|
+
expect(new URL(forwardedReq.url).pathname).toBe("/api/auth/sign-up/email");
|
|
81
|
+
expect(forwardedReq.method).toBe("POST");
|
|
82
|
+
});
|
|
83
|
+
it("returns error when handler rejects invalid fields", async () => {
|
|
84
|
+
const auth = mockAuthWithHandler(new Response(JSON.stringify({ error: "Email is required" }), {
|
|
85
|
+
status: 400,
|
|
86
|
+
headers: { "Content-Type": "application/json" },
|
|
87
|
+
}));
|
|
88
|
+
const app = createAuthApp(auth);
|
|
89
|
+
const res = await app.request("/api/auth/sign-up/email", {
|
|
90
|
+
method: "POST",
|
|
91
|
+
headers: { "Content-Type": "application/json" },
|
|
92
|
+
body: JSON.stringify({}),
|
|
93
|
+
});
|
|
94
|
+
expect(res.status).toBe(400);
|
|
95
|
+
expect(auth.handler).toHaveBeenCalledOnce();
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
// ---------------------------------------------------------------------------
|
|
99
|
+
// POST /api/auth/sign-in/email
|
|
100
|
+
// ---------------------------------------------------------------------------
|
|
101
|
+
describe("POST /api/auth/sign-in/email", () => {
|
|
102
|
+
it("delegates sign-in request to auth.handler", async () => {
|
|
103
|
+
const session = { user: { id: "u-1" }, session: { id: "s-1", token: "tok" } };
|
|
104
|
+
const auth = mockAuthWithHandler(new Response(JSON.stringify(session), {
|
|
105
|
+
status: 200,
|
|
106
|
+
headers: {
|
|
107
|
+
"Content-Type": "application/json",
|
|
108
|
+
"Set-Cookie": "better-auth.session_token=tok; Path=/; HttpOnly",
|
|
109
|
+
},
|
|
110
|
+
}));
|
|
111
|
+
const app = createAuthApp(auth);
|
|
112
|
+
const res = await app.request("/api/auth/sign-in/email", {
|
|
113
|
+
method: "POST",
|
|
114
|
+
headers: { "Content-Type": "application/json" },
|
|
115
|
+
body: JSON.stringify({ email: "a@b.com", password: "password123456" }),
|
|
116
|
+
});
|
|
117
|
+
expect(res.status).toBe(200);
|
|
118
|
+
expect(auth.handler).toHaveBeenCalledOnce();
|
|
119
|
+
const forwardedReq = auth.handler.mock.calls[0][0];
|
|
120
|
+
expect(new URL(forwardedReq.url).pathname).toBe("/api/auth/sign-in/email");
|
|
121
|
+
});
|
|
122
|
+
it("returns 401 for invalid credentials", async () => {
|
|
123
|
+
const auth = mockAuthWithHandler(new Response(JSON.stringify({ error: "Invalid credentials" }), {
|
|
124
|
+
status: 401,
|
|
125
|
+
headers: { "Content-Type": "application/json" },
|
|
126
|
+
}));
|
|
127
|
+
const app = createAuthApp(auth);
|
|
128
|
+
const res = await app.request("/api/auth/sign-in/email", {
|
|
129
|
+
method: "POST",
|
|
130
|
+
headers: { "Content-Type": "application/json" },
|
|
131
|
+
body: JSON.stringify({ email: "a@b.com", password: "wrong" }),
|
|
132
|
+
});
|
|
133
|
+
expect(res.status).toBe(401);
|
|
134
|
+
expect(auth.handler).toHaveBeenCalledOnce();
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
// ---------------------------------------------------------------------------
|
|
138
|
+
// POST /api/auth/sign-out
|
|
139
|
+
// ---------------------------------------------------------------------------
|
|
140
|
+
describe("POST /api/auth/sign-out", () => {
|
|
141
|
+
it("delegates sign-out request to auth.handler", async () => {
|
|
142
|
+
const auth = mockAuthWithHandler(new Response(JSON.stringify({ success: true }), {
|
|
143
|
+
status: 200,
|
|
144
|
+
headers: {
|
|
145
|
+
"Content-Type": "application/json",
|
|
146
|
+
"Set-Cookie": "better-auth.session_token=; Path=/; Expires=Thu, 01 Jan 1970 00:00:00 GMT",
|
|
147
|
+
},
|
|
148
|
+
}));
|
|
149
|
+
const app = createAuthApp(auth);
|
|
150
|
+
const res = await app.request("/api/auth/sign-out", {
|
|
151
|
+
method: "POST",
|
|
152
|
+
headers: {
|
|
153
|
+
Cookie: "better-auth.session_token=existing-tok",
|
|
154
|
+
},
|
|
155
|
+
});
|
|
156
|
+
expect(res.status).toBe(200);
|
|
157
|
+
expect(auth.handler).toHaveBeenCalledOnce();
|
|
158
|
+
const forwardedReq = auth.handler.mock.calls[0][0];
|
|
159
|
+
expect(new URL(forwardedReq.url).pathname).toBe("/api/auth/sign-out");
|
|
160
|
+
expect(forwardedReq.method).toBe("POST");
|
|
161
|
+
});
|
|
162
|
+
});
|
|
163
|
+
// ---------------------------------------------------------------------------
|
|
164
|
+
// Route matching
|
|
165
|
+
// ---------------------------------------------------------------------------
|
|
166
|
+
describe("auth route matching", () => {
|
|
167
|
+
it("does not match non-auth routes", async () => {
|
|
168
|
+
const auth = mockAuthWithHandler(new Response("ok"));
|
|
169
|
+
const app = createAuthApp(auth);
|
|
170
|
+
app.get("/api/other", (c) => c.json({ other: true }));
|
|
171
|
+
const res = await app.request("/api/other");
|
|
172
|
+
expect(res.status).toBe(200);
|
|
173
|
+
const body = await res.json();
|
|
174
|
+
expect(body.other).toBe(true);
|
|
175
|
+
expect(auth.handler).not.toHaveBeenCalled();
|
|
176
|
+
});
|
|
177
|
+
it("forwards the raw Request object to auth.handler", async () => {
|
|
178
|
+
const auth = mockAuthWithHandler(new Response(JSON.stringify({ ok: true }), {
|
|
179
|
+
status: 200,
|
|
180
|
+
headers: { "Content-Type": "application/json" },
|
|
181
|
+
}));
|
|
182
|
+
const app = createAuthApp(auth);
|
|
183
|
+
await app.request("/api/auth/get-session", {
|
|
184
|
+
headers: { Cookie: "better-auth.session_token=test-tok" },
|
|
185
|
+
});
|
|
186
|
+
expect(auth.handler).toHaveBeenCalledOnce();
|
|
187
|
+
const forwardedReq = auth.handler.mock.calls[0][0];
|
|
188
|
+
expect(forwardedReq).toBeInstanceOf(Request);
|
|
189
|
+
expect(forwardedReq.headers.get("Cookie")).toBe("better-auth.session_token=test-tok");
|
|
190
|
+
});
|
|
191
|
+
});
|
package/dist/index.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
export * from "./account/index.js";
|
|
1
2
|
export * from "./admin/index.js";
|
|
2
3
|
export * from "./auth/index.js";
|
|
3
4
|
export { type ChargeOpts, type ChargeResult, type CheckoutOpts, type CheckoutSession, DrizzleWebhookSeenRepository, type Invoice, type IPaymentProcessor, type IWebhookSeenRepository, noOpReplayGuard, PaymentMethodOwnershipError, type PortalOpts, type SavedPaymentMethod, type SetupResult, type WebhookResult, } from "./billing/index.js";
|
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { IMeterEventRepository } from "./meter-event-repository.js";
|
|
2
2
|
import type { MeterEvent, MeterEventRow } from "./types.js";
|
|
3
3
|
export interface IMeterEmitter {
|
|
4
|
-
emit(event: MeterEvent): void
|
|
4
|
+
emit(event: MeterEvent): void | Promise<void>;
|
|
5
5
|
flush(): Promise<number>;
|
|
6
6
|
readonly pending: number;
|
|
7
7
|
close(): void;
|
|
@@ -53,7 +53,7 @@ export declare class DrizzleMeterEmitter implements IMeterEmitter {
|
|
|
53
53
|
private reconstituteCreditFields;
|
|
54
54
|
private replayWALAsync;
|
|
55
55
|
/** Emit a meter event. Non-blocking -- buffers in memory after WAL write. */
|
|
56
|
-
emit(event: MeterEvent): void
|
|
56
|
+
emit(event: MeterEvent): Promise<void>;
|
|
57
57
|
/** Flush buffered events to the database with retry and DLQ logic. */
|
|
58
58
|
flush(): Promise<number>;
|
|
59
59
|
/** Number of events currently buffered. */
|
package/dist/metering/emitter.js
CHANGED
|
@@ -94,13 +94,13 @@ export class DrizzleMeterEmitter {
|
|
|
94
94
|
}
|
|
95
95
|
}
|
|
96
96
|
/** Emit a meter event. Non-blocking -- buffers in memory after WAL write. */
|
|
97
|
-
emit(event) {
|
|
97
|
+
async emit(event) {
|
|
98
98
|
if (this.closed)
|
|
99
99
|
return;
|
|
100
100
|
// FAIL-CLOSED: Write to WAL first, then buffer.
|
|
101
|
-
// append() is
|
|
102
|
-
//
|
|
103
|
-
const eventWithId = this.wal.append(event);
|
|
101
|
+
// append() is mutex-guarded to prevent TOCTOU races with remove().
|
|
102
|
+
// appendFileSync is still used inside the lock for crash safety.
|
|
103
|
+
const eventWithId = await this.wal.append(event);
|
|
104
104
|
this.buffer.push(eventWithId);
|
|
105
105
|
if (this.buffer.length >= this.batchSize) {
|
|
106
106
|
void this.flush();
|
|
@@ -57,7 +57,7 @@ describe("DrizzleMeterEmitter — happy path", () => {
|
|
|
57
57
|
}
|
|
58
58
|
});
|
|
59
59
|
it("writes a meter event to the database after flush", async () => {
|
|
60
|
-
emitter.emit(makeEvent({ tenant: "t1", capability: "voice" }));
|
|
60
|
+
await emitter.emit(makeEvent({ tenant: "t1", capability: "voice" }));
|
|
61
61
|
expect(emitter.pending).toBe(1);
|
|
62
62
|
const flushed = await emitter.flush();
|
|
63
63
|
expect(flushed).toBe(1);
|
|
@@ -70,9 +70,9 @@ describe("DrizzleMeterEmitter — happy path", () => {
|
|
|
70
70
|
expect(rows[0].charge).toBe(Credit.fromCents(2).toRaw());
|
|
71
71
|
});
|
|
72
72
|
it("persists multiple events in one flush", async () => {
|
|
73
|
-
emitter.emit(makeEvent({ tenant: "t1" }));
|
|
74
|
-
emitter.emit(makeEvent({ tenant: "t1" }));
|
|
75
|
-
emitter.emit(makeEvent({ tenant: "t2" }));
|
|
73
|
+
await emitter.emit(makeEvent({ tenant: "t1" }));
|
|
74
|
+
await emitter.emit(makeEvent({ tenant: "t1" }));
|
|
75
|
+
await emitter.emit(makeEvent({ tenant: "t2" }));
|
|
76
76
|
const flushed = await emitter.flush();
|
|
77
77
|
expect(flushed).toBe(3);
|
|
78
78
|
const t1Rows = await emitter.queryEvents("t1");
|
|
@@ -89,7 +89,7 @@ describe("DrizzleMeterEmitter — happy path", () => {
|
|
|
89
89
|
expect(emitter.pending).toBe(0);
|
|
90
90
|
});
|
|
91
91
|
it("persists sessionId, duration, usage, tier, and metadata", async () => {
|
|
92
|
-
emitter.emit(makeEvent({
|
|
92
|
+
await emitter.emit(makeEvent({
|
|
93
93
|
sessionId: "sess-1",
|
|
94
94
|
duration: 5000,
|
|
95
95
|
usage: { units: 100, unitType: "tokens" },
|
|
@@ -119,7 +119,7 @@ describe("DrizzleMeterEmitter — DLQ failure paths", () => {
|
|
|
119
119
|
maxRetries: 1,
|
|
120
120
|
});
|
|
121
121
|
await em.ready;
|
|
122
|
-
em.emit(makeEvent());
|
|
122
|
+
await em.emit(makeEvent());
|
|
123
123
|
// Drop the table so flush fails
|
|
124
124
|
await failPool.query("DROP TABLE meter_events CASCADE");
|
|
125
125
|
await em.flush();
|
|
@@ -148,7 +148,7 @@ describe("DrizzleMeterEmitter — DLQ failure paths", () => {
|
|
|
148
148
|
maxRetries: 2,
|
|
149
149
|
});
|
|
150
150
|
await em.ready;
|
|
151
|
-
em.emit(makeEvent());
|
|
151
|
+
await em.emit(makeEvent());
|
|
152
152
|
// First flush fails
|
|
153
153
|
await failPool.query("DROP TABLE meter_events CASCADE");
|
|
154
154
|
await em.flush();
|