@podosoft/podokit 1.4.2 → 1.5.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 (66) hide show
  1. package/dist/deploy-compose-profile.js +2 -2
  2. package/dist/deploy-compose-render.js +1 -0
  3. package/dist/deploy-profile.js +2 -2
  4. package/dist/deploy-render.js +2 -0
  5. package/dist/deploy-schema.d.ts +7 -0
  6. package/dist/deploy-schema.js +40 -0
  7. package/dist/lockfile.js +3 -1
  8. package/dist/provider.d.ts +1 -0
  9. package/dist/provider.js +5 -2
  10. package/dist/templates/fullstack/AGENTS.md +18 -5
  11. package/dist/templates/fullstack/apps/api/package.json +1 -1
  12. package/dist/templates/fullstack/apps/api/src/app.extensions.ts +7 -4
  13. package/dist/templates/fullstack/apps/api/src/app.ts +6 -3
  14. package/dist/templates/fullstack/apps/api/src/config/env.validation.ts +28 -0
  15. package/dist/templates/fullstack/apps/api/src/config/providers.ts +4 -2
  16. package/dist/templates/fullstack/apps/api/src/config/server-options.spec.ts +17 -6
  17. package/dist/templates/fullstack/apps/api/src/config/server-options.ts +5 -8
  18. package/dist/templates/fullstack/apps/api/src/core/client-address.spec.ts +82 -0
  19. package/dist/templates/fullstack/apps/api/src/core/client-address.ts +68 -0
  20. package/dist/templates/fullstack/apps/api/src/core/services.ts +30 -0
  21. package/dist/templates/fullstack/apps/web/Dockerfile +4 -0
  22. package/dist/templates/fullstack/apps/web/src/lib/server/backend-proxy.test.ts +291 -36
  23. package/dist/templates/fullstack/apps/web/src/lib/server/backend-proxy.ts +196 -41
  24. package/dist/templates/fullstack/apps/web/src/lib/server/body-limits.ts +5 -2
  25. package/dist/templates/fullstack/apps/web/src/lib/server/proxy-timeouts.ts +22 -0
  26. package/dist/templates/fullstack/dot-env.docker.example +2 -0
  27. package/dist/templates/fullstack/dot-env.example +17 -1
  28. package/dist/templates/fullstack/infra/k3s/configmap.yaml +3 -0
  29. package/dist/templates/modules/admin-dashboard/files/apps/api/src/account-sessions/account-sessions.routes.spec.ts +152 -0
  30. package/dist/templates/modules/admin-dashboard/files/apps/api/src/account-sessions/account-sessions.routes.ts +112 -0
  31. package/dist/templates/modules/admin-dashboard/files/apps/api/src/admin-sessions/admin-sessions.revoker.ts +24 -0
  32. package/dist/templates/modules/admin-dashboard/files/apps/api/src/admin-sessions/admin-sessions.service.spec.ts +18 -0
  33. package/dist/templates/modules/admin-dashboard/files/apps/api/src/admin-sessions/admin-sessions.service.ts +36 -13
  34. package/dist/templates/modules/admin-dashboard/files/apps/api/src/admin.module.ts +13 -1
  35. package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/account-sessions.test.ts +33 -0
  36. package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/account-sessions.ts +34 -0
  37. package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/account-page.svelte +62 -38
  38. package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/table-toolbar.svelte +3 -1
  39. package/dist/templates/modules/admin-dashboard/files/tests/api/account-sessions.api.spec.ts +55 -0
  40. package/dist/templates/modules/admin-dashboard/files/tests/api/users-admin.api.spec.ts +26 -0
  41. package/dist/templates/modules/admin-dashboard/files/tests/helpers/devices.ts +25 -0
  42. package/dist/templates/modules/admin-dashboard/files/tests/seed.setup.ts +3 -10
  43. package/dist/templates/modules/admin-dashboard/files/tests/ui/account-sessions.ui.spec.ts +46 -0
  44. package/dist/templates/modules/admin-dashboard/files/tests/ui/account.ui.spec.ts +7 -4
  45. package/dist/templates/modules/admin-dashboard/files/tests/ui/table-toolbar.ui.spec.ts +25 -0
  46. package/dist/templates/modules/audit-log/files/apps/api/src/audit/audit-hook.ts +2 -1
  47. package/dist/templates/modules/audit-log/files/apps/api/src/audit/audit.service.spec.ts +16 -0
  48. package/dist/templates/modules/audit-log/files/apps/api/src/audit/audit.service.ts +2 -1
  49. package/dist/templates/modules/auth/files/apps/api/src/auth/auth.module.ts +4 -1
  50. package/dist/templates/modules/auth/files/apps/api/src/auth/auth.service.spec.ts +54 -6
  51. package/dist/templates/modules/auth/files/apps/api/src/auth/auth.service.ts +26 -4
  52. package/dist/templates/modules/auth/files/apps/api/src/auth/auth.ts +8 -3
  53. package/dist/templates/modules/auth/files/apps/api/src/auth/revoke-other-sessions.spec.ts +56 -0
  54. package/dist/templates/modules/auth/files/apps/api/src/auth/revoke-other-sessions.ts +45 -0
  55. package/dist/templates/modules/rate-limit/files/apps/api/src/rate-limit/rate-limit.config.spec.ts +2 -59
  56. package/dist/templates/modules/rate-limit/files/apps/api/src/rate-limit/rate-limit.config.ts +0 -65
  57. package/dist/templates/modules/rate-limit/files/apps/api/src/rate-limit/rate-limit.identity.spec.ts +82 -21
  58. package/dist/templates/modules/rate-limit/files/apps/api/src/rate-limit/rate-limit.identity.ts +54 -15
  59. package/dist/templates/modules/rate-limit/files/apps/api/src/rate-limit/rate-limit.module.spec.ts +43 -5
  60. package/dist/templates/modules/rate-limit/files/apps/api/src/rate-limit/rate-limit.module.ts +10 -2
  61. package/dist/templates/modules/rate-limit/files/apps/api/src/rate-limit/rate-limit.routes.spec.ts +61 -3
  62. package/dist/templates/modules/rate-limit/files/apps/api/src/rate-limit/rate-limit.routes.ts +48 -15
  63. package/dist/templates/modules/rate-limit/files/tests/api/rate-limit.api.spec.ts +20 -9
  64. package/dist/templates/modules/rate-limit/module.manifest.json +6 -9
  65. package/dist/templates/todo/AGENTS.md +41 -5
  66. package/package.json +2 -2
@@ -70,7 +70,7 @@ test("rate limit: health probes stay available while ordinary routes return 429
70
70
  await ctx.dispose();
71
71
  });
72
72
 
73
- test("rate limit: each signed session has its own counter and forged sessions do not", async ({
73
+ test("rate limit: a user's sessions share one counter and forged sessions do not get one", async ({
74
74
  playwright,
75
75
  }) => {
76
76
  const email = `rate-limit-${Date.now()}-${Math.random().toString(36).slice(2, 8)}@example.com`;
@@ -81,21 +81,31 @@ test("rate limit: each signed session has its own counter and forged sessions do
81
81
  test.skip(probe.status() >= 500, "throttler storage (redis) not reachable");
82
82
  const remaining = (response: APIResponse): number => Number(response.headers()["ratelimit-remaining"]);
83
83
  const ceiling = Number(probe.headers()["ratelimit-limit"]);
84
- expect(remaining(probe)).toBe(ceiling - 1);
85
84
 
86
- // The signed bearer form of the same session shares its counter.
85
+ // A second sign-in of the same user, and the signed bearer form of the first
86
+ // session. The probe requires a session, so each request below resolves it; a
87
+ // session counts under its user from the request after the API first resolves
88
+ // it (or from that request when authentication runs before the limiter).
89
+ const second = await playwright.request.newContext({ baseURL: base, extraHTTPHeaders: origin });
90
+ expect((await second.post("/api/auth/sign-in/email", { data: { email, password } })).ok()).toBeTruthy();
87
91
  const cookie = (await first.storageState()).cookies.find((c) => c.name.endsWith("session_token"));
88
92
  expect(cookie, "sign-up set a session cookie").toBeTruthy();
89
93
  const bearer = await playwright.request.newContext({
90
94
  baseURL: base,
91
95
  extraHTTPHeaders: { ...origin, authorization: `Bearer ${cookie?.value ?? ""}` },
92
96
  });
93
- expect(remaining(await bearer.get("/api/cache/rate-limit-probe"))).toBe(ceiling - 2);
97
+ await second.get("/api/cache/rate-limit-probe");
94
98
 
95
- // Counters follow the session, not the user: signing in again starts a new one.
96
- const second = await playwright.request.newContext({ baseURL: base, extraHTTPHeaders: origin });
97
- expect((await second.post("/api/auth/sign-in/email", { data: { email, password } })).ok()).toBeTruthy();
98
- expect(remaining(await second.get("/api/cache/rate-limit-probe"))).toBe(ceiling - 1);
99
+ // Both sessions now draw from one budget: each request lowers what the next
100
+ // one, from either session, has left.
101
+ const fromFirst = remaining(await first.get("/api/cache/rate-limit-probe"));
102
+ expect(remaining(await second.get("/api/cache/rate-limit-probe"))).toBe(fromFirst - 1);
103
+ expect(remaining(await bearer.get("/api/cache/rate-limit-probe"))).toBe(fromFirst - 2);
104
+ expect(remaining(await first.get("/api/cache/rate-limit-probe"))).toBe(fromFirst - 3);
105
+
106
+ // Another user has a budget of their own.
107
+ const other = await session(playwright);
108
+ expect(remaining(await other.get("/api/cache/rate-limit-probe"))).toBe(ceiling - 1);
99
109
 
100
110
  // A session cookie without a valid signature is counted with anonymous traffic
101
111
  // from the same client address instead of receiving a fresh counter. The cache
@@ -113,12 +123,13 @@ test("rate limit: each signed session has its own counter and forged sessions do
113
123
  expect(remaining(after)).toBeLessThan(remaining(before));
114
124
  }).toPass({ timeout: 10_000 });
115
125
 
116
- for (const context of [first, bearer, second, forged, anonymous]) await context.dispose();
126
+ for (const context of [first, second, bearer, other, forged, anonymous]) await context.dispose();
117
127
  });
118
128
 
119
129
  test("rate limit: session reads do not consume the strict authentication budget", async ({
120
130
  playwright,
121
131
  }) => {
132
+ test.skip(authLimit > 1000, "the authentication budget is too large to exhaust here");
122
133
  const ctx = await session(playwright);
123
134
  for (let i = 0; i < authLimit + 5; i++) {
124
135
  const response = await ctx.get("/api/auth/get-session");
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "rate-limit",
3
3
  "manifestVersion": 3,
4
- "description": "Provider-neutral Elysia request limiting by validated API key, signed-in session, or trusted-proxy client address.",
4
+ "description": "Provider-neutral Elysia request limiting by validated API key, signed-in user, or trusted-proxy client address.",
5
5
  "requires": ["api-key-auth"],
6
6
  "requiresCapabilities": ["cache"],
7
7
  "targetApp": "api",
@@ -19,9 +19,6 @@
19
19
  "RATE_LIMIT_AUTH_MAX=20",
20
20
  "# higher ceiling for SSR runtime settings and session reads",
21
21
  "RATE_LIMIT_RUNTIME_MAX=1000",
22
- "# trust no proxy hop by default; set the exact production proxy depth",
23
- "RATE_LIMIT_TRUSTED_PROXY_HOPS=0",
24
- "RATE_LIMIT_PROXY_HEADER=x-forwarded-for",
25
22
  "RATE_LIMIT_STORAGE_TIMEOUT_MS=1000",
26
23
  "RATE_LIMIT_UNAVAILABLE_RETRY_AFTER=1"
27
24
  ],
@@ -40,20 +37,20 @@
40
37
  "file": "AGENTS.md",
41
38
  "marker": "<!-- podokit:end:agents-modules -->",
42
39
  "optional": true,
43
- "text": "### rate-limit\n- Every request counts against a per-caller budget (general, `/api/auth`, or runtime reads); exceeded limits return 429 `RATE_LIMIT_EXCEEDED` with `Retry-After`.\n- Give routes that legitimately send many requests, such as upload chunks, their own budget instead of raising `RATE_LIMIT_MAX`: `services.tryResolve(RATE_LIMIT_ROUTES)?.register({ method: \"PUT\", pathPrefix: \"/uploads\", profile: { name: \"uploads\", limit: 3000 } })` (`RATE_LIMIT_ROUTES` from `@podosoft/podokit-runtime`). Register from a module's `plugin` factory or `configureServices()` in `app.extensions.ts`; the most specific prefix wins, and `profile: \"exempt\"` skips limiting.\n- Prefixes are API paths (`/uploads`, not the web proxy's `/api/uploads`); Better Auth routes keep `/api/auth`.\n"
40
+ "text": "### rate-limit\n- Every request counts against a per-caller budget (general, `/api/auth`, or runtime reads), keyed by API key, then signed-in user, then client address; exceeded limits return 429 `RATE_LIMIT_EXCEEDED` with `Retry-After`. A user's sessions share one budget once the API has resolved them for a request.\n- Give routes that legitimately send many requests, such as upload chunks, their own budget instead of raising `RATE_LIMIT_MAX`: `services.tryResolve(RATE_LIMIT_ROUTES)?.register({ method: \"PUT\", pathPrefix: \"/uploads\", profile: { name: \"uploads\", limit: 3000 } })` (`RATE_LIMIT_ROUTES` from `@podosoft/podokit-runtime`). Register from a module's `plugin` factory or `configureServices()` in `app.extensions.ts`; the most specific prefix wins (more segments, then a literal segment over a `:name` parameter), and `profile: \"exempt\"` skips limiting.\n- Prefixes are API paths (`/uploads`, not the web proxy's `/api/uploads`); Better Auth routes keep `/api/auth`. A `:name` segment matches one path segment, as in `/hosts/:hostId/file-transfers`.\n"
44
41
  }
45
42
  ],
46
43
  "instructions": [
47
44
  "Configure the selected cache provider before enabling request limits.",
48
45
  "Tune the general, authentication, and runtime limits in .env.",
49
46
  "Give each deployment that shares Redis a distinct RATE_LIMIT_KEY_PREFIX.",
50
- "Set RATE_LIMIT_TRUSTED_PROXY_HOPS to the exact number of trusted reverse proxies; keep 0 for direct API access.",
51
- "Validated X-API-Key clients and signed-in sessions receive independent opaque counters; other traffic uses the trusted client address.",
52
- "Sessions are recognized by verifying the Better Auth cookie or bearer signature with BETTER_AUTH_SECRET, without a database read, so limits apply per session rather than per user.",
47
+ "Requests without a validated API key or session count by the client address the API resolves from TRUSTED_PROXY_HOPS and TRUSTED_PROXY_HEADER, which audit entries and Better Auth sessions use too.",
48
+ "Validated X-API-Key clients and signed-in users receive independent opaque counters; other traffic uses the resolved client address.",
49
+ "Sessions are recognized by verifying the Better Auth cookie or bearer signature with BETTER_AUTH_SECRET, without a database read, and count under their user once the API has resolved them for a request, so limits apply per user.",
53
50
  "Exceeded limits return RATE_LIMIT_EXCEEDED with HTTP 429 and Retry-After. Cache failures return RATE_LIMIT_UNAVAILABLE with HTTP 503.",
54
51
  "Register a RateLimitIdentityExtension under RATE_LIMIT_IDENTITY_EXTENSION in app.extensions.ts for another validated machine-key source.",
55
52
  "GET /health and GET /health/ready bypass request quotas so orchestrator probes remain reliable.",
56
- "Give a route its own budget or exempt it with services.tryResolve(RATE_LIMIT_ROUTES)?.register({ method, pathPrefix, profile }) from a module's plugin factory or configureServices() in app.extensions.ts; the most specific prefix wins.",
53
+ "Give a route its own budget or exempt it with services.tryResolve(RATE_LIMIT_ROUTES)?.register({ method, pathPrefix, profile }) from a module's plugin factory or configureServices() in app.extensions.ts; a :name segment matches one path segment, and the most specific prefix wins.",
57
54
  "Redis counters are shared across replicas; memory counters require one API replica."
58
55
  ]
59
56
  }
@@ -10,8 +10,9 @@ A Bun 1.4 full-stack TypeScript monorepo ({{packageManager}} workspaces):
10
10
 
11
11
  - `apps/api` — **Elysia** backend (`{{projectName}}-api`), Bun.SQL + `{{databaseProvider}}`, merged OpenAPI at `/api-docs`.
12
12
  - `apps/web` — **SvelteKit** frontend (`{{projectName}}-web`), Tailwind v4 + shadcn-svelte + typesafe-i18n.
13
- - `infra/` — Docker Compose references for external services and example k3s manifests.
14
- - `.podokit/` — generation metadata plus application-owned development and deployment profiles.
13
+ - `infra/` — provider-aware Docker Compose and example k3s manifests.
14
+ - `.podokit/` — PodoKit generation metadata plus application-owned development and
15
+ deployment profiles (see "PodoKit tooling" below).
15
16
  - **Todo example**: `apps/api/src/todos/` (Elysia plugin, Bun.SQL repository, and migration) and the todo UI in `apps/web/src/routes/` show the end-to-end patterns to follow.
16
17
 
17
18
  ## Setup & commands
@@ -49,7 +50,32 @@ Merge added module settings from `.env.example` into an existing `.env`.
49
50
  managed lists. Cap a route's request body in the app-owned
50
51
  `apps/web/src/lib/server/body-limits.ts` (`APP_BODY_LIMITS`, 413
51
52
  `PAYLOAD_TOO_LARGE`). `BODY_SIZE_LIMIT` is the ceiling for every web route, so
52
- uploads larger than it must use a direct API path (`apiPaths`) instead.
53
+ uploads larger than it must use a direct API path (`apiPaths`) instead. Give a
54
+ route a longer, shorter, or no response deadline in the app-owned
55
+ `apps/web/src/lib/server/proxy-timeouts.ts` (`APP_PROXY_TIMEOUTS`, `null` for
56
+ none); never edit the managed deadline. Both lists take path prefixes with
57
+ `:name` parameters, such as `/api/hosts/:hostId/file-transfers`, and the most
58
+ specific prefix wins.
59
+ - Server-side API calls (`serverApiClient`) give up after `BACKEND_TIMEOUT_MS`
60
+ (2000), and the proxy answers 504 `BACKEND_TIMEOUT` when the API sends no
61
+ response headers within `BACKEND_PROXY_TIMEOUT_MS` (30000). Once the API's
62
+ `GET /health` stops answering too, calls throw `BackendUnavailableError` and the
63
+ proxy answers 503 `BACKEND_UNAVAILABLE` at once until it answers again. Handle
64
+ any non-`ApiError` failure in a load function as a temporary outage. The API
65
+ (`API_IDLE_TIMEOUT`) and the built web server (`CONNECTION_IDLE_TIMEOUT`) close
66
+ a connection that sends nothing for that many seconds (35 each): raise both
67
+ with any longer `BACKEND_PROXY_TIMEOUT_MS` or `APP_PROXY_TIMEOUTS` entry, more
68
+ than 4 above it, or set `0` when a route has no deadline. The proxy refuses to
69
+ load when `CONNECTION_IDLE_TIMEOUT` cannot cover a deadline.
70
+ - Read a caller's address in the API with `clientAddressOf(request.headers)` from
71
+ `apps/api/src/core/client-address.ts`, never from `X-Forwarded-For` directly. The
72
+ API resolves it once per request from `TRUSTED_PROXY_HOPS` (1: the web proxy) and
73
+ `TRUSTED_PROXY_HEADER`; Better Auth sessions, audit entries, and rate limits use
74
+ the same address.
75
+ - `GET /healthz` (`apps/web/src/routes/healthz/+server.ts`) is the web server's own
76
+ health route for the Kubernetes probes, the Compose healthcheck, and the image
77
+ `HEALTHCHECK`. Keep it free of API calls, so an API outage does not take the web
78
+ out of service.
53
79
  - When the root layout or a server hook fails, such as with 503 while the API is
54
80
  down, SvelteKit serves the app-owned `apps/web/src/error.html`. It is plain HTML
55
81
  with inline styles: no components, app CSS, scripts, or assets, and only the
@@ -96,15 +122,25 @@ changes need an e2e/unit test.
96
122
  This project is managed by the `podo` CLI. Files have an ownership tier recorded
97
123
  in `.podokit/files.lock` — respect it:
98
124
 
99
- - **owned** (your code: `apps/web/src/routes/**`, your components/services, `apps/api/src/app.extensions.ts`, and this `AGENTS.md`) — edit freely. Add application-owned services or module descriptors through `app.extensions.ts` instead of changing toolkit registrations.
125
+ - **owned** (your code: `apps/web/src/routes/**`, your components/services, `apps/api/src/app.extensions.ts`, and this `AGENTS.md`) — edit freely. Add application-owned services or module descriptors through `app.extensions.ts` instead of changing toolkit registrations. Set Bun server options (`maxRequestBodySize`, `idleTimeout`) with its `apiServerOptions` export rather than editing `main.ts`; `API_MAX_REQUEST_BODY_SIZE` and `API_IDLE_TIMEOUT` apply when the export leaves them out.
100
126
  - **assembled** (e.g. `apps/api/src/app.ts`, `auth.ts`) — contain `// podokit:begin:… / …:end` fenced regions that the toolkit recomputes. Edit outside the fences only; **never hand-edit inside the fences**.
101
127
  - **managed** (bootstrap/config like `main.ts`) — the toolkit may update these; avoid gratuitous edits.
102
128
 
103
129
  Useful commands:
104
130
 
105
- - `podo provider list` / `podo provider set <capability> <provider> [--apply]` — inspect or switch database, cache, storage, events, and jobs; set previews by default.
131
+ - `podo dev watch|up|ps|logs|exec|down` — run container development through the
132
+ shared loopback gateway. `.podokit/dev.json` owns the local/public hosts and the
133
+ exact API WebSocket allowlist; never use a prefix or wildcard for that allowlist.
134
+ `podo dev up` rebuilds the images from the current source so a recreated
135
+ container never runs code older than the files Compose Watch synced.
136
+ `podo dev down` also stops this project's running `podo dev watch`; do not
137
+ kill Compose processes by hand.
138
+ The containers read the untracked `.env.docker`; `podo dev` creates it from the
139
+ committed `.env.docker.example` with generated secrets when it is missing. Never
140
+ commit `.env.docker` or put secrets in the example.
106
141
  - `podo add <module>` — add a feature (auth, admin-dashboard, redis, bullmq, …). It wires itself into the app; run `podo add` with no argument to list modules.
107
142
  - `podo remove <module>` — un-apply a module (inverse of add; refuses if another module needs it, keeps files you edited).
143
+ - `podo provider list` / `podo provider set <capability> <provider> [--apply]` — inspect or switch database, cache, object storage, events, and jobs. Set is dry-run by default and never migrates or deletes provider data. Local providers require one API process.
108
144
  - `podo status` / `podo diff` — see your local edits vs. what PodoKit generated.
109
145
  - `podo update` — pull in template/module improvements (3-way merges your edits; never touches owned files).
110
146
  - `podo deploy` — plan, apply, verify, or roll back an immutable release on Kubernetes or one Docker host. Production API WebSocket paths belong in the deployment profile's exact `exposure.webSocketPaths` allowlist, and API routes that take bodies above the web server's `BODY_SIZE_LIMIT` (file uploads) in `exposure.apiPaths` (and `.podokit/dev.json` `apiPaths` for `podo dev`; `podo add` and `podo deploy init` fill in the paths modules declare), which reach the API without the web proxy, so the API must check their size, session, and `Origin` itself; Compose trusts forwarding headers only from `exposure.trustedProxyCidrs`. Tag `vX.Y.Z` to build and push the images (`.github/workflows/release.yml`; check its runner — GitHub-hosted minutes are free for public repositories only). Use `.agents/skills/podokit-deploy`.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@podosoft/podokit",
3
- "version": "1.4.2",
3
+ "version": "1.5.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.4.0",
48
+ "@podosoft/podokit-runtime": "^0.4.1",
49
49
  "@podosoft/podokit-template-engine": "^0.4.2"
50
50
  }
51
51
  }