@podosoft/podokit 1.1.6 → 1.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.
- package/dist/deploy-render.js +5 -0
- package/dist/templates/fullstack/apps/api/package.json +2 -2
- package/dist/templates/fullstack/apps/api/src/app.spec.ts +81 -0
- package/dist/templates/fullstack/apps/api/src/app.ts +53 -5
- package/dist/templates/fullstack/apps/api/src/config/env.validation.ts +6 -0
- package/dist/templates/fullstack/apps/api/src/core/api-contract.ts +9 -1
- package/dist/templates/fullstack/apps/api/src/core/services.spec.ts +30 -0
- package/dist/templates/fullstack/apps/api/src/core/services.ts +19 -3
- package/dist/templates/fullstack/apps/api/src/database/database.spec.ts +149 -4
- package/dist/templates/fullstack/apps/api/src/database/database.ts +66 -13
- package/dist/templates/fullstack/apps/api/src/health/health.plugin.spec.ts +86 -0
- package/dist/templates/fullstack/apps/api/src/health/health.plugin.ts +4 -9
- package/dist/templates/fullstack/apps/api/src/health/readiness.service.spec.ts +141 -1
- package/dist/templates/fullstack/apps/api/src/health/readiness.service.ts +86 -12
- package/dist/templates/fullstack/apps/web/src/lib/server/backend-proxy.test.ts +188 -1
- package/dist/templates/fullstack/apps/web/src/lib/server/backend-proxy.ts +30 -0
- package/dist/templates/fullstack/apps/web/src/routes/api/[...path]/+server.ts +3 -3
- package/dist/templates/fullstack/apps/web/src/routes/api/auth/[...all]/+server.ts +3 -3
- package/dist/templates/fullstack/compose.dev.yaml +21 -0
- package/dist/templates/fullstack/dot-env.docker +3 -0
- package/dist/templates/fullstack/dot-env.example +14 -0
- package/dist/templates/fullstack/infra/k3s/api-deployment.yaml +2 -0
- package/dist/templates/modules/admin-dashboard/files/apps/api/src/admin-sessions/admin-sessions.revoker.spec.ts +25 -0
- package/dist/templates/modules/admin-dashboard/files/apps/api/src/admin-sessions/admin-sessions.revoker.ts +26 -0
- package/dist/templates/modules/admin-dashboard/files/apps/api/src/admin-sessions/admin-sessions.routes.spec.ts +122 -0
- package/dist/templates/modules/admin-dashboard/files/apps/api/src/admin-sessions/admin-sessions.routes.ts +46 -0
- package/dist/templates/modules/admin-dashboard/files/apps/api/src/admin-sessions/admin-sessions.service.spec.ts +187 -0
- package/dist/templates/modules/admin-dashboard/files/apps/api/src/admin-sessions/admin-sessions.service.ts +160 -0
- package/dist/templates/modules/admin-dashboard/files/apps/api/src/admin.module.ts +17 -28
- package/dist/templates/modules/admin-dashboard/files/apps/api/src/profile-image/profile-image.service.spec.ts +3 -5
- package/dist/templates/modules/admin-dashboard/files/apps/api/src/profile-image/profile-image.service.ts +6 -17
- package/dist/templates/modules/admin-dashboard/files/apps/api/src/site-assets/site-assets.routes.spec.ts +167 -0
- package/dist/templates/modules/admin-dashboard/files/apps/api/src/site-assets/site-assets.routes.ts +50 -0
- package/dist/templates/modules/admin-dashboard/files/apps/api/src/site-settings/site-settings.service.spec.ts +158 -0
- package/dist/templates/modules/admin-dashboard/files/apps/api/src/site-settings/site-settings.service.ts +78 -28
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/app.d.ts +3 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/hooks.server.test.ts +124 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/hooks.server.ts +19 -11
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/account-data.server.ts +6 -32
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/admin/sessions.test.ts +74 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/admin/sessions.ts +73 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/admin/users.test.ts +75 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/admin/users.ts +71 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/audit-log.test.ts +68 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/audit-log.ts +108 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/account-page.svelte +14 -8
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/session-idle-timeout.svelte +74 -49
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/i18n/catalogs/admin-dashboard/en.json +6 -1
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/i18n/catalogs/admin-dashboard/ko.json +6 -1
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/i18n/messages.test.ts +29 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/i18n/messages.ts +23 -6
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/oauth-query.test.ts +21 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/oauth-query.ts +15 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/server/capabilities.test.ts +98 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/server/capabilities.ts +54 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/server/protected-layout.ts +7 -50
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/(admin)/admin/audit/+page.svelte +88 -45
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/(admin)/admin/sessions/+page.svelte +76 -75
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/(admin)/admin/settings/+page.server.ts +15 -14
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/(admin)/admin/settings/+page.svelte +2 -2
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/(admin)/admin/users/+page.svelte +86 -38
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/+layout.server.ts +10 -17
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/+layout.ts +10 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/oauth2/consent/+page.svelte +4 -3
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/setup-2fa/+page.svelte +14 -8
- package/dist/templates/modules/admin-dashboard/files/dot-claude/skills/podokit-data-table/SKILL.md +2 -1
- package/dist/templates/modules/admin-dashboard/files/tests/api/admin-sessions.api.spec.ts +124 -0
- package/dist/templates/modules/admin-dashboard/files/tests/api/audit.api.spec.ts +75 -12
- package/dist/templates/modules/admin-dashboard/files/tests/api/auth.api.spec.ts +34 -0
- package/dist/templates/modules/admin-dashboard/files/tests/api/profile-image.api.spec.ts +7 -0
- package/dist/templates/modules/admin-dashboard/files/tests/api/site-settings.api.spec.ts +72 -0
- package/dist/templates/modules/admin-dashboard/files/tests/api/users-admin.api.spec.ts +36 -2
- package/dist/templates/modules/admin-dashboard/files/tests/ui/audit.ui.spec.ts +41 -0
- package/dist/templates/modules/admin-dashboard/files/tests/ui/data-table.ui.spec.ts +32 -13
- package/dist/templates/modules/admin-dashboard/files/tests/ui/oidc.ui.spec.ts +13 -4
- package/dist/templates/modules/admin-dashboard/files/tests/ui/request-budget.ui.spec.ts +46 -0
- package/dist/templates/modules/admin-dashboard/files/tests/ui/sessions.ui.spec.ts +64 -1
- package/dist/templates/modules/admin-dashboard/files/tests/ui/table-toolbar.ui.spec.ts +24 -0
- package/dist/templates/modules/admin-dashboard/files/tests/ui/users-crud.ui.spec.ts +8 -1
- package/dist/templates/modules/admin-dashboard/files/tests/ui/users.ui.spec.ts +20 -0
- package/dist/templates/modules/audit-log/files/apps/api/src/audit/README.md +46 -5
- package/dist/templates/modules/audit-log/files/apps/api/src/audit/audit-events.ts +19 -7
- package/dist/templates/modules/audit-log/files/apps/api/src/audit/audit-hook.ts +2 -0
- package/dist/templates/modules/audit-log/files/apps/api/src/audit/audit.migration.spec.ts +81 -0
- package/dist/templates/modules/audit-log/files/apps/api/src/audit/audit.module.ts +13 -11
- package/dist/templates/modules/audit-log/files/apps/api/src/audit/audit.routes.spec.ts +115 -0
- package/dist/templates/modules/audit-log/files/apps/api/src/audit/audit.routes.ts +44 -0
- package/dist/templates/modules/audit-log/files/apps/api/src/audit/audit.service.spec.ts +421 -43
- package/dist/templates/modules/audit-log/files/apps/api/src/audit/audit.service.ts +382 -35
- package/dist/templates/modules/audit-log/files/apps/api/src/migrations/1792000000000-IndexAuditLogFilters.ts +35 -0
- package/dist/templates/modules/audit-log/module.manifest.json +7 -4
- package/dist/templates/modules/auth/files/apps/api/src/auth/auth-config-store.ts +20 -5
- package/dist/templates/modules/auth/files/apps/api/src/auth/auth-provider.ts +24 -29
- package/dist/templates/modules/auth/files/apps/api/src/auth/auth.module.ts +28 -13
- package/dist/templates/modules/auth/files/apps/api/src/auth/auth.openapi.spec.ts +56 -2
- package/dist/templates/modules/auth/files/apps/api/src/auth/auth.openapi.ts +22 -0
- package/dist/templates/modules/auth/files/apps/api/src/auth/auth.service.spec.ts +81 -5
- package/dist/templates/modules/auth/files/apps/api/src/auth/auth.service.ts +26 -2
- package/dist/templates/modules/auth/files/apps/api/src/auth/auth.ts +15 -9
- package/dist/templates/modules/auth/files/apps/api/src/auth/config-events.spec.ts +38 -0
- package/dist/templates/modules/auth/files/apps/api/src/auth/config-events.ts +43 -0
- package/dist/templates/modules/auth/files/apps/api/src/auth/db.spec.ts +24 -0
- package/dist/templates/modules/auth/files/apps/api/src/auth/db.ts +31 -2
- package/dist/templates/modules/auth/files/apps/api/src/auth/feature-gate.spec.ts +58 -0
- package/dist/templates/modules/auth/files/apps/api/src/auth/feature-gate.ts +46 -57
- package/dist/templates/modules/auth/files/apps/api/src/auth-config/auth-config.service.ts +7 -2
- package/dist/templates/modules/auth/files/apps/api/src/auth-config/config-param.spec.ts +23 -0
- package/dist/templates/modules/auth/files/apps/api/src/auth-config/config-param.ts +10 -0
- package/dist/templates/modules/auth/files/apps/api/src/settings/flag-defaults.ts +10 -0
- package/dist/templates/modules/auth/files/apps/api/src/settings/settings.service.spec.ts +81 -0
- package/dist/templates/modules/auth/files/apps/api/src/settings/settings.service.ts +37 -34
- package/dist/templates/modules/auth/module.manifest.json +2 -2
- package/dist/templates/modules/file-upload/files/apps/api/src/files/files.module.spec.ts +136 -0
- package/dist/templates/modules/file-upload/files/apps/api/src/files/files.module.ts +34 -13
- package/dist/templates/modules/file-upload/files/apps/api/src/files/upload-keys.spec.ts +60 -0
- package/dist/templates/modules/file-upload/files/apps/api/src/files/upload-keys.ts +51 -0
- package/dist/templates/modules/file-upload/files/tests/api/files.api.spec.ts +70 -9
- package/dist/templates/modules/file-upload/module.manifest.json +9 -3
- package/dist/templates/modules/job-progress/files/apps/api/src/progress/job-progress.module.ts +26 -32
- package/dist/templates/modules/job-progress/files/apps/api/src/progress/progress.bridge.spec.ts +16 -1
- package/dist/templates/modules/job-progress/files/apps/api/src/progress/progress.processor.ts +31 -4
- package/dist/templates/modules/job-progress/files/apps/api/src/progress/progress.worker.spec.ts +145 -0
- package/dist/templates/modules/job-progress/files/apps/api/src/progress/progress.worker.ts +37 -0
- package/dist/templates/modules/job-progress/files/tests/api/progress.api.spec.ts +94 -4
- package/dist/templates/modules/job-progress/module.manifest.json +3 -3
- package/dist/templates/modules/mailer/files/apps/api/src/mail/mailer.spec.ts +54 -0
- package/dist/templates/modules/mailer/files/apps/api/src/mail/mailer.ts +47 -25
- package/dist/templates/modules/mailer/module.manifest.json +1 -1
- package/dist/templates/modules/object-storage-local/files/apps/api/src/storage-local/download-url.spec.ts +86 -0
- package/dist/templates/modules/object-storage-local/files/apps/api/src/storage-local/download-url.ts +159 -0
- package/dist/templates/modules/object-storage-local/files/apps/api/src/storage-local/storage-demo.ts +38 -0
- package/dist/templates/modules/object-storage-local/files/apps/api/src/storage-local/storage-local.module.spec.ts +172 -4
- package/dist/templates/modules/object-storage-local/files/apps/api/src/storage-local/storage-local.module.ts +110 -33
- package/dist/templates/modules/object-storage-local/files/tests/api/storage-local.api.spec.ts +88 -0
- package/dist/templates/modules/object-storage-local/module.manifest.json +6 -1
- package/dist/templates/modules/object-storage-s3/files/apps/api/src/storage/s3-object.store.spec.ts +80 -0
- package/dist/templates/modules/object-storage-s3/files/apps/api/src/storage/s3-object.store.ts +13 -7
- package/dist/templates/modules/object-storage-s3/files/apps/api/src/storage/storage-demo.spec.ts +156 -0
- package/dist/templates/modules/object-storage-s3/files/apps/api/src/storage/storage-demo.ts +38 -0
- package/dist/templates/modules/object-storage-s3/files/apps/api/src/storage/storage.module.ts +39 -21
- package/dist/templates/modules/object-storage-s3/files/apps/api/src/storage/storage.service.ts +101 -2
- package/dist/templates/modules/object-storage-s3/files/tests/api/storage.api.spec.ts +84 -9
- package/dist/templates/modules/object-storage-s3/module.manifest.json +8 -3
- package/dist/templates/modules/rate-limit/files/apps/api/src/rate-limit/rate-limit.config.spec.ts +9 -0
- package/dist/templates/modules/rate-limit/files/apps/api/src/rate-limit/rate-limit.config.ts +7 -0
- package/dist/templates/modules/rate-limit/files/apps/api/src/rate-limit/rate-limit.identity.spec.ts +139 -21
- package/dist/templates/modules/rate-limit/files/apps/api/src/rate-limit/rate-limit.identity.ts +80 -11
- package/dist/templates/modules/rate-limit/files/apps/api/src/rate-limit/rate-limit.module.spec.ts +12 -6
- package/dist/templates/modules/rate-limit/files/apps/api/src/rate-limit/rate-limit.module.ts +15 -2
- package/dist/templates/modules/rate-limit/files/tests/api/rate-limit.api.spec.ts +46 -0
- package/dist/templates/modules/rate-limit/module.manifest.json +4 -3
- package/dist/templates/modules/redis/files/apps/api/src/redis/redis-cache.store.spec.ts +125 -0
- package/dist/templates/modules/redis/files/apps/api/src/redis/redis-cache.store.ts +27 -13
- package/dist/templates/modules/sse/files/apps/api/src/events/event-envelope.ts +123 -0
- package/dist/templates/modules/sse/files/apps/api/src/events/event-stream.ts +191 -0
- package/dist/templates/modules/sse/files/apps/api/src/events/event-topics.ts +64 -0
- package/dist/templates/modules/sse/files/apps/api/src/events/events.module.spec.ts +203 -0
- package/dist/templates/modules/sse/files/apps/api/src/events/events.module.ts +107 -14
- package/dist/templates/modules/sse/files/apps/api/src/events/events.service.spec.ts +372 -18
- package/dist/templates/modules/sse/files/apps/api/src/events/events.service.ts +136 -40
- package/dist/templates/modules/sse/files/apps/web/src/lib/events.test.ts +216 -0
- package/dist/templates/modules/sse/files/apps/web/src/lib/events.ts +207 -0
- package/dist/templates/modules/sse/files/tests/api/events.api.spec.ts +181 -70
- package/dist/templates/modules/sse/module.manifest.json +17 -4
- package/package.json +2 -2
|
@@ -1,87 +1,198 @@
|
|
|
1
|
-
import { expect, test } from "@playwright/test";
|
|
1
|
+
import { expect, test, type APIRequestContext, type PlaywrightWorkerArgs } from "@playwright/test";
|
|
2
2
|
|
|
3
3
|
const base = process.env.E2E_BASE_URL ?? "http://localhost:5001";
|
|
4
4
|
const secondaryApi = process.env.E2E_SECONDARY_API_URL;
|
|
5
5
|
const origin = { origin: base };
|
|
6
|
+
// The administrator seeded by the admin-dashboard Playwright setup. Inlined so this
|
|
7
|
+
// spec also compiles in apps that do not install that module.
|
|
8
|
+
const admin = {
|
|
9
|
+
email: "admin@example.com",
|
|
10
|
+
password: process.env.E2E_ADMIN_PASSWORD || "Podokit3e-Str0ng!pw",
|
|
11
|
+
};
|
|
6
12
|
|
|
7
|
-
|
|
8
|
-
|
|
13
|
+
type Playwright = PlaywrightWorkerArgs["playwright"];
|
|
14
|
+
|
|
15
|
+
interface SessionUser {
|
|
16
|
+
id: string;
|
|
17
|
+
roles: string[];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function newContext(playwright: Playwright): Promise<APIRequestContext> {
|
|
21
|
+
return playwright.request.newContext({ baseURL: base, extraHTTPHeaders: origin });
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async function sessionUser(ctx: APIRequestContext): Promise<SessionUser | undefined> {
|
|
25
|
+
const response = await ctx.get("/api/auth/get-session");
|
|
26
|
+
if (!response.ok()) return undefined;
|
|
27
|
+
const session = await response.json() as { user?: { id?: string; role?: string | null } } | null;
|
|
28
|
+
if (!session?.user?.id) return undefined;
|
|
29
|
+
const roles = (session.user.role ?? "").split(",").map((role) => role.trim()).filter(Boolean);
|
|
30
|
+
return { id: session.user.id, roles };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Resolves undefined when the auth module is missing or sign-up does not start a session. */
|
|
34
|
+
async function signUp(ctx: APIRequestContext): Promise<SessionUser | undefined> {
|
|
9
35
|
const email = `sse-${Date.now()}-${Math.random().toString(36).slice(2, 8)}@example.com`;
|
|
10
|
-
await ctx.post("/api/auth/sign-up/email", { data: { email, password: "Podokit3e-Str0ng!pw", name: "E" } })
|
|
11
|
-
return ctx;
|
|
36
|
+
await ctx.post("/api/auth/sign-up/email", { data: { email, password: "Podokit3e-Str0ng!pw", name: "E" } });
|
|
37
|
+
return sessionUser(ctx);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Resolves false when no administrator session is available. */
|
|
41
|
+
async function signInAdmin(ctx: APIRequestContext): Promise<boolean> {
|
|
42
|
+
const response = await ctx.post("/api/auth/sign-in/email", { data: admin });
|
|
43
|
+
return response.ok() && ((await sessionUser(ctx))?.roles ?? []).includes("admin");
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async function cookieHeader(ctx: APIRequestContext): Promise<string> {
|
|
47
|
+
return (await ctx.storageState()).cookies.map((cookie) => `${cookie.name}=${cookie.value}`).join("; ");
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function uniqueTopic(): string {
|
|
51
|
+
return `spec-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
class EventStreamReader {
|
|
55
|
+
private readonly decoder = new TextDecoder();
|
|
56
|
+
private buffered = "";
|
|
57
|
+
|
|
58
|
+
private constructor(
|
|
59
|
+
private readonly reader: ReadableStreamDefaultReader<Uint8Array>,
|
|
60
|
+
private readonly controller: AbortController,
|
|
61
|
+
) {}
|
|
62
|
+
|
|
63
|
+
/** Opens a stream and waits for its retry hint, which proves the subscription is live. */
|
|
64
|
+
static async open(ctx: APIRequestContext, topics: string): Promise<EventStreamReader> {
|
|
65
|
+
const controller = new AbortController();
|
|
66
|
+
const response = await fetch(`${base}/api/events/stream?topics=${encodeURIComponent(topics)}`, {
|
|
67
|
+
headers: { ...origin, cookie: await cookieHeader(ctx) },
|
|
68
|
+
signal: controller.signal,
|
|
69
|
+
});
|
|
70
|
+
expect(response.status).toBe(200);
|
|
71
|
+
expect(response.headers.get("content-type")).toContain("text/event-stream");
|
|
72
|
+
const reader = response.body?.getReader();
|
|
73
|
+
if (!reader) throw new Error("event stream has no body");
|
|
74
|
+
const stream = new EventStreamReader(reader, controller);
|
|
75
|
+
await stream.until("retry:");
|
|
76
|
+
return stream;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** Returns everything received up to and including `text`. */
|
|
80
|
+
async until(text: string, timeoutMs = 8_000): Promise<string> {
|
|
81
|
+
const deadline = Date.now() + timeoutMs;
|
|
82
|
+
while (!this.buffered.includes(text)) {
|
|
83
|
+
const remaining = deadline - Date.now();
|
|
84
|
+
if (remaining <= 0) throw new Error(`timed out waiting for ${text}`);
|
|
85
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
86
|
+
const timeout = new Promise<"timeout">((resolve) => {
|
|
87
|
+
timer = setTimeout(() => resolve("timeout"), remaining);
|
|
88
|
+
});
|
|
89
|
+
const result = await Promise.race([this.reader.read(), timeout]);
|
|
90
|
+
clearTimeout(timer);
|
|
91
|
+
if (result === "timeout") continue;
|
|
92
|
+
if (result.done) throw new Error(`stream ended before ${text}`);
|
|
93
|
+
this.buffered += this.decoder.decode(result.value, { stream: true });
|
|
94
|
+
}
|
|
95
|
+
const end = this.buffered.indexOf(text) + text.length;
|
|
96
|
+
const seen = this.buffered.slice(0, end);
|
|
97
|
+
this.buffered = this.buffered.slice(end);
|
|
98
|
+
return seen;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
close(): void {
|
|
102
|
+
this.controller.abort();
|
|
103
|
+
}
|
|
12
104
|
}
|
|
13
105
|
|
|
14
|
-
test("sse:
|
|
15
|
-
const
|
|
16
|
-
const
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
106
|
+
test("sse: streams require a session and a valid topic list", async ({ playwright }) => {
|
|
107
|
+
const anonymous = await newContext(playwright);
|
|
108
|
+
const member = await newContext(playwright);
|
|
109
|
+
try {
|
|
110
|
+
test.skip(!(await signUp(member)), "requires the auth module");
|
|
111
|
+
const unauthenticated = await fetch(`${base}/api/events/stream?topics=message`, { headers: origin });
|
|
112
|
+
expect(unauthenticated.status).toBe(401);
|
|
113
|
+
const invalid = await member.get("/api/events/stream?topics=bad%20topic");
|
|
114
|
+
expect(invalid.status()).toBe(400);
|
|
115
|
+
expect((await invalid.json()).error.code).toBe("EVENTS_TOPICS_INVALID");
|
|
116
|
+
} finally {
|
|
117
|
+
await anonymous.dispose();
|
|
118
|
+
await member.dispose();
|
|
119
|
+
}
|
|
20
120
|
});
|
|
21
121
|
|
|
22
|
-
test("sse:
|
|
23
|
-
const
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
const
|
|
36
|
-
const
|
|
37
|
-
|
|
38
|
-
const
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
122
|
+
test("sse: only administrators may publish", async ({ playwright }) => {
|
|
123
|
+
const member = await newContext(playwright);
|
|
124
|
+
try {
|
|
125
|
+
test.skip(!(await signUp(member)), "requires the auth module");
|
|
126
|
+
const published = await member.post("/api/events", { data: { message: "hello" } });
|
|
127
|
+
expect(published.status()).toBe(403);
|
|
128
|
+
expect((await published.json()).error.code).toBe("ADMIN_REQUIRED");
|
|
129
|
+
} finally {
|
|
130
|
+
await member.dispose();
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
test("sse: an admin publish reaches only the addressed user @smoke", async ({ playwright }) => {
|
|
135
|
+
const adminCtx = await newContext(playwright);
|
|
136
|
+
const aliceCtx = await newContext(playwright);
|
|
137
|
+
const bobCtx = await newContext(playwright);
|
|
138
|
+
const streams: EventStreamReader[] = [];
|
|
139
|
+
try {
|
|
140
|
+
test.skip(!(await signInAdmin(adminCtx)), "requires the auth module and the seeded administrator");
|
|
141
|
+
const alice = await signUp(aliceCtx);
|
|
142
|
+
expect(alice).toBeDefined();
|
|
143
|
+
expect(await signUp(bobCtx)).toBeDefined();
|
|
144
|
+
const topic = uniqueTopic();
|
|
145
|
+
const aliceStream = await EventStreamReader.open(aliceCtx, topic);
|
|
146
|
+
const bobStream = await EventStreamReader.open(bobCtx, topic);
|
|
147
|
+
streams.push(aliceStream, bobStream);
|
|
148
|
+
|
|
149
|
+
const secret = `secret-${Date.now()}`;
|
|
150
|
+
const done = `done-${Date.now()}`;
|
|
151
|
+
const targeted = await adminCtx.post("/api/events", {
|
|
152
|
+
data: { topic, audience: { userIds: [alice?.id] }, message: secret },
|
|
153
|
+
});
|
|
154
|
+
expect(targeted.status()).toBe(201);
|
|
155
|
+
expect(await targeted.json()).toEqual({ ok: true });
|
|
156
|
+
expect((await adminCtx.post("/api/events", { data: { topic, message: done } })).status()).toBe(201);
|
|
157
|
+
|
|
158
|
+
const aliceSaw = await aliceStream.until(done);
|
|
159
|
+
expect(aliceSaw).toContain(JSON.stringify({ topic, data: { message: secret } }));
|
|
160
|
+
expect(await bobStream.until(done)).not.toContain(secret);
|
|
161
|
+
} finally {
|
|
162
|
+
for (const stream of streams) stream.close();
|
|
163
|
+
await adminCtx.dispose();
|
|
164
|
+
await aliceCtx.dispose();
|
|
165
|
+
await bobCtx.dispose();
|
|
43
166
|
}
|
|
44
|
-
ac.abort();
|
|
45
|
-
expect(seen).toBe(true);
|
|
46
|
-
await ctx.dispose();
|
|
47
167
|
});
|
|
48
168
|
|
|
49
169
|
test("sse: a second API replica delivers through Redis", async ({ playwright }) => {
|
|
50
170
|
test.skip(!secondaryApi, "a secondary API replica is not configured");
|
|
51
|
-
const
|
|
52
|
-
const
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
const deadline = Date.now() + 8000;
|
|
78
|
-
while (Date.now() < deadline && seen === 0) {
|
|
79
|
-
const { value, done } = await reader.read();
|
|
80
|
-
if (done) break;
|
|
81
|
-
buffered += decoder.decode(value, { stream: true });
|
|
82
|
-
seen = buffered.split(msg).length - 1;
|
|
171
|
+
const adminCtx = await newContext(playwright);
|
|
172
|
+
const memberCtx = await newContext(playwright);
|
|
173
|
+
let stream: EventStreamReader | undefined;
|
|
174
|
+
try {
|
|
175
|
+
test.skip(!(await signInAdmin(adminCtx)), "requires the auth module and the seeded administrator");
|
|
176
|
+
const member = await signUp(memberCtx);
|
|
177
|
+
expect(member).toBeDefined();
|
|
178
|
+
const topic = uniqueTopic();
|
|
179
|
+
stream = await EventStreamReader.open(memberCtx, topic);
|
|
180
|
+
|
|
181
|
+
const message = `replica-${Date.now()}`;
|
|
182
|
+
const published = await fetch(`${secondaryApi}/events`, {
|
|
183
|
+
method: "POST",
|
|
184
|
+
headers: { ...origin, cookie: await cookieHeader(adminCtx), "content-type": "application/json" },
|
|
185
|
+
body: JSON.stringify({ topic, audience: { userIds: [member?.id] }, message }),
|
|
186
|
+
});
|
|
187
|
+
expect(published.status).toBe(201);
|
|
188
|
+
|
|
189
|
+
const done = `done-${Date.now()}`;
|
|
190
|
+
expect((await adminCtx.post("/api/events", { data: { topic, message: done } })).status()).toBe(201);
|
|
191
|
+
const seen = await stream.until(done);
|
|
192
|
+
expect(seen.split(message).length - 1).toBe(1);
|
|
193
|
+
} finally {
|
|
194
|
+
stream?.close();
|
|
195
|
+
await adminCtx.dispose();
|
|
196
|
+
await memberCtx.dispose();
|
|
83
197
|
}
|
|
84
|
-
ac.abort();
|
|
85
|
-
expect(seen).toBe(1);
|
|
86
|
-
await ctx.dispose();
|
|
87
198
|
});
|
|
@@ -6,6 +6,14 @@
|
|
|
6
6
|
"requiredProjectFiles": [
|
|
7
7
|
"apps/api/src/health/readiness.service.ts"
|
|
8
8
|
],
|
|
9
|
+
"requiredProjectContents": {
|
|
10
|
+
"apps/api/src/core/services.ts": [
|
|
11
|
+
"isAdmin(request: Request): Promise<boolean>;",
|
|
12
|
+
"roles(request: Request): Promise<string[]>;",
|
|
13
|
+
"activeOrganizationId(request: Request): Promise<string | undefined>;",
|
|
14
|
+
"tryResolve<T>(key: ServiceKey<T>): T | undefined {"
|
|
15
|
+
]
|
|
16
|
+
},
|
|
9
17
|
"targetApp": "api",
|
|
10
18
|
"env": [
|
|
11
19
|
"# Maximum encoded Server-Sent Event payload",
|
|
@@ -16,9 +24,14 @@
|
|
|
16
24
|
{ "file": "apps/api/src/app.ts", "marker": "// podokit:end:modules", "text": "eventsModule," }
|
|
17
25
|
],
|
|
18
26
|
"instructions": [
|
|
19
|
-
"Stream
|
|
20
|
-
"Publish
|
|
21
|
-
"
|
|
22
|
-
"
|
|
27
|
+
"Stream as a signed-in user: curl -N -b cookies.txt 'localhost:5002/events/stream?topics=message'",
|
|
28
|
+
"Publish as an administrator (in another terminal): curl -b admin-cookies.txt -XPOST localhost:5002/events -H 'content-type: application/json' -d '{\"message\":\"hello\"}'",
|
|
29
|
+
"Each data event is a default SSE message whose data is {\"topic\",\"data\"}. A resync event means events were dropped, so reload state.",
|
|
30
|
+
"Publish from a module with EventsService.publish({ topic, audience, data }). An audience of userIds, roles, or organizationId limits who receives it.",
|
|
31
|
+
"Use publishAsync for confirmed delivery, publish for best-effort delivery, or publishLocal for events already fanned out to every replica.",
|
|
32
|
+
"Restrict who may subscribe to a topic with events.topics.register(topic, authorizer).",
|
|
33
|
+
"Streams take the subscriber's roles and active organization from the request identity. Register EVENT_IDENTITY in app.extensions.ts to derive them differently.",
|
|
34
|
+
"In the browser, subscribe(topic, handler) from $lib/events shares one EventSource per tab.",
|
|
35
|
+
"Select events=redis for multiple replicas or events=memory for a single process."
|
|
23
36
|
]
|
|
24
37
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@podosoft/podokit",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "An opinionated Bun and Elysia full-stack TypeScript starter toolkit and CLI with SvelteKit, TailwindCSS, shadcn-svelte, Docker, and k3s.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"repository": {
|
|
@@ -45,7 +45,7 @@
|
|
|
45
45
|
"test": "vitest run"
|
|
46
46
|
},
|
|
47
47
|
"dependencies": {
|
|
48
|
-
"@podosoft/podokit-runtime": "^0.
|
|
48
|
+
"@podosoft/podokit-runtime": "^0.3.0",
|
|
49
49
|
"@podosoft/podokit-template-engine": "^0.4.2"
|
|
50
50
|
}
|
|
51
51
|
}
|