@podosoft/podokit 1.2.0 → 1.3.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 (61) hide show
  1. package/dist/add.d.ts +3 -1
  2. package/dist/add.js +116 -14
  3. package/dist/index.js +8 -0
  4. package/dist/lib.d.ts +2 -2
  5. package/dist/lib.js +3 -2
  6. package/dist/lockfile.d.ts +3 -1
  7. package/dist/lockfile.js +17 -6
  8. package/dist/templates/fullstack/apps/api/scripts/build.mjs +1 -0
  9. package/dist/templates/fullstack/apps/api/src/core/api-contract.ts +1 -0
  10. package/dist/templates/fullstack/apps/api/src/database/data-source.spec.ts +15 -0
  11. package/dist/templates/fullstack/apps/api/src/database/data-source.ts +3 -0
  12. package/dist/templates/fullstack/apps/api/src/database/migrate.ts +1 -1
  13. package/dist/templates/fullstack/apps/web/package.json +1 -1
  14. package/dist/templates/fullstack/dot-claude/skills/podokit-sveltekit-route/SKILL.md +8 -1
  15. package/dist/templates/fullstack/tests/playwright.extensions.ts +2 -2
  16. package/dist/templates/modules/admin-dashboard/files/apps/api/scripts/bootstrap-admin.mjs +21 -26
  17. package/dist/templates/modules/admin-dashboard/files/apps/api/src/admin-sessions/admin-sessions.migration.spec.ts +74 -0
  18. package/dist/templates/modules/admin-dashboard/files/apps/api/src/admin-users/admin-users.routes.spec.ts +76 -0
  19. package/dist/templates/modules/admin-dashboard/files/apps/api/src/admin-users/admin-users.routes.ts +41 -0
  20. package/dist/templates/modules/admin-dashboard/files/apps/api/src/admin-users/admin-users.service.spec.ts +124 -0
  21. package/dist/templates/modules/admin-dashboard/files/apps/api/src/admin-users/admin-users.service.ts +153 -0
  22. package/dist/templates/modules/admin-dashboard/files/apps/api/src/admin.module.ts +5 -0
  23. package/dist/templates/modules/admin-dashboard/files/apps/api/src/bootstrap-admin-runtime.ts +60 -0
  24. package/dist/templates/modules/admin-dashboard/files/apps/api/src/migrations/1792060000000-IndexSessionCreatedAt.ts +35 -0
  25. package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/admin/users.test.ts +59 -32
  26. package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/admin/users.ts +81 -11
  27. package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/server/guards.test.ts +23 -0
  28. package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/server/guards.ts +2 -0
  29. package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/server/public-paths.ts +9 -0
  30. package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/(admin)/admin/users/+page.svelte +13 -19
  31. package/dist/templates/modules/admin-dashboard/files/tests/api/admin-users.api.spec.ts +86 -0
  32. package/dist/templates/modules/admin-dashboard/files/tests/api/auth-config.api.spec.ts +1 -0
  33. package/dist/templates/modules/admin-dashboard/files/tests/api/auth.api.spec.ts +1 -1
  34. package/dist/templates/modules/admin-dashboard/files/tests/helpers/disposable-users.ts +5 -2
  35. package/dist/templates/modules/admin-dashboard/files/tests/ui/data-table.ui.spec.ts +2 -2
  36. package/dist/templates/modules/admin-dashboard/files/tests/ui/email-admin.ui.spec.ts +0 -1
  37. package/dist/templates/modules/admin-dashboard/files/tests/ui/email.ui.spec.ts +1 -3
  38. package/dist/templates/modules/admin-dashboard/files/tests/ui/oidc.ui.spec.ts +3 -1
  39. package/dist/templates/modules/admin-dashboard/files/tests/ui/request-budget.ui.spec.ts +14 -15
  40. package/dist/templates/modules/admin-dashboard/files/tests/ui/table-toolbar.ui.spec.ts +13 -8
  41. package/dist/templates/modules/admin-dashboard/files/tests/ui/users-crud.ui.spec.ts +1 -1
  42. package/dist/templates/modules/admin-dashboard/files/tests/ui/users.ui.spec.ts +14 -3
  43. package/dist/templates/modules/admin-dashboard/module.manifest.json +3 -2
  44. package/dist/templates/modules/audit-log/files/apps/api/src/audit/audit.migration.spec.ts +53 -11
  45. package/dist/templates/modules/audit-log/files/apps/api/src/migrations/1792000000000-IndexAuditLogFilters.ts +26 -4
  46. package/dist/templates/modules/auth/files/apps/api/src/auth-config/auth-config.rows.spec.ts +53 -0
  47. package/dist/templates/modules/auth/files/apps/api/src/auth-config/auth-config.rows.ts +65 -0
  48. package/dist/templates/modules/auth/files/apps/api/src/auth-config/auth-config.service.ts +16 -37
  49. package/dist/templates/modules/auth/files/apps/api/src/auth-config/session-expiry.spec.ts +33 -0
  50. package/dist/templates/modules/auth/files/apps/api/src/auth-config/session-expiry.ts +29 -0
  51. package/dist/templates/modules/auth/files/dot-claude/skills/podokit-configure-auth/references/bootstrap-admin.md +2 -0
  52. package/dist/templates/modules/rate-limit/files/apps/api/src/rate-limit/rate-limit.module.ts +2 -2
  53. package/dist/templates/modules/sse/files/apps/api/src/events/events.module.spec.ts +48 -1
  54. package/dist/templates/modules/sse/files/apps/api/src/events/events.module.ts +15 -2
  55. package/dist/templates/modules/sse/files/apps/api/src/events/events.service.spec.ts +35 -0
  56. package/dist/templates/modules/sse/files/apps/api/src/events/events.service.ts +18 -1
  57. package/dist/templates/modules/sse/files/tests/api/events.api.spec.ts +46 -0
  58. package/dist/templates/modules/sse/module.manifest.json +3 -1
  59. package/dist/update.d.ts +17 -0
  60. package/dist/update.js +33 -8
  61. package/package.json +1 -1
@@ -119,6 +119,52 @@ test("sse: streams require a session and a valid topic list", async ({ playwrigh
119
119
  }
120
120
  });
121
121
 
122
+ function openStream(cookie: string, controller: AbortController): Promise<Response> {
123
+ return fetch(`${base}/api/events/stream?topics=${uniqueTopic()}`, {
124
+ headers: { ...origin, cookie },
125
+ signal: controller.signal,
126
+ });
127
+ }
128
+
129
+ test("sse: a user's open streams are capped per replica", async ({ playwright }) => {
130
+ const member = await newContext(playwright);
131
+ const other = await newContext(playwright);
132
+ const controllers: AbortController[] = [];
133
+ try {
134
+ test.skip(!(await signUp(member)), "requires the auth module");
135
+ expect(await signUp(other)).toBeDefined();
136
+ const cookie = await cookieHeader(member);
137
+ // The limit is SSE_MAX_STREAMS_PER_USER on the server, so open until refused.
138
+ let refused: Response | undefined;
139
+ for (let attempt = 0; attempt < 64 && !refused; attempt += 1) {
140
+ const controller = new AbortController();
141
+ const response = await openStream(cookie, controller);
142
+ if (response.status === 200) controllers.push(controller);
143
+ else refused = response;
144
+ }
145
+ expect(controllers.length).toBeGreaterThan(0);
146
+ expect(refused?.status).toBe(429);
147
+ expect((await refused?.json()).error.code).toBe("EVENTS_STREAM_LIMIT");
148
+
149
+ // Another user is unaffected, and closing a stream frees its slot.
150
+ const otherController = new AbortController();
151
+ controllers.push(otherController);
152
+ expect((await openStream(await cookieHeader(other), otherController)).status).toBe(200);
153
+ controllers.shift()?.abort();
154
+ await expect.poll(async () => {
155
+ const controller = new AbortController();
156
+ const response = await openStream(cookie, controller);
157
+ if (response.status === 200) controllers.push(controller);
158
+ else controller.abort();
159
+ return response.status;
160
+ }, { timeout: 8_000 }).toBe(200);
161
+ } finally {
162
+ for (const controller of controllers) controller.abort();
163
+ await member.dispose();
164
+ await other.dispose();
165
+ }
166
+ });
167
+
122
168
  test("sse: only administrators may publish", async ({ playwright }) => {
123
169
  const member = await newContext(playwright);
124
170
  try {
@@ -17,7 +17,9 @@
17
17
  "targetApp": "api",
18
18
  "env": [
19
19
  "# Maximum encoded Server-Sent Event payload",
20
- "SSE_MAX_EVENT_BYTES=65536"
20
+ "SSE_MAX_EVENT_BYTES=65536",
21
+ "# Open event streams per user on each API replica",
22
+ "SSE_MAX_STREAMS_PER_USER=8"
21
23
  ],
22
24
  "inject": [
23
25
  { "file": "apps/api/src/app.ts", "marker": "// podokit:end:imports", "text": "import { eventsModule } from \"./events/events.module\";" },
package/dist/update.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import { type TemplateVars } from "@podosoft/podokit-template-engine";
2
2
  import { type Tier } from "./lockfile";
3
+ import type { ProviderCapability } from "@podosoft/podokit-runtime";
3
4
  import type { Toolchain } from "./toolchain";
4
5
  /**
5
6
  * `podo update` planner. For now it produces a dry-run plan only: it assembles
@@ -17,11 +18,23 @@ export interface FileChange {
17
18
  /** Why the action was chosen (for the report). */
18
19
  note: string;
19
20
  }
21
+ /** A module the target version needs that the project does not record yet. */
22
+ export interface AddedModule {
23
+ name: string;
24
+ /** Installed or added module that needs it. */
25
+ requiredBy: string;
26
+ /** Provider capability it implements, when added for a capability requirement. */
27
+ capability?: ProviderCapability;
28
+ /** Selected provider it implements. */
29
+ provider?: string;
30
+ }
20
31
  export interface UpdatePlan {
21
32
  fromVersion: string;
22
33
  toVersion: string;
23
34
  template: string;
24
35
  modules: string[];
36
+ /** Modules the update adds, ordered ahead of the modules that need them. */
37
+ addedModules: AddedModule[];
25
38
  changes: FileChange[];
26
39
  }
27
40
  /**
@@ -51,6 +64,8 @@ export interface ApplyOptions {
51
64
  abortOnConflict?: boolean;
52
65
  }
53
66
  export interface ApplyResult {
67
+ /** Modules added because the target version requires them. */
68
+ addedModules: string[];
54
69
  written: string[];
55
70
  /** Files relocated by module update migrations. */
56
71
  moved: {
@@ -71,5 +86,7 @@ export interface ApplyResult {
71
86
  * stamps the new version.
72
87
  */
73
88
  export declare function applyUpdate(projectRoot: string, templatesDir: string, options?: ApplyOptions): ApplyResult;
89
+ /** Why an update adds a module, for the plan report. */
90
+ export declare function addedModuleReason(module: AddedModule): string;
74
91
  /** Count actions for a one-line summary. */
75
92
  export declare function summarize(plan: UpdatePlan): Record<Action, number>;
package/dist/update.js CHANGED
@@ -2,6 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.planUpdate = planUpdate;
4
4
  exports.applyUpdate = applyUpdate;
5
+ exports.addedModuleReason = addedModuleReason;
5
6
  exports.summarize = summarize;
6
7
  const node_child_process_1 = require("node:child_process");
7
8
  const node_fs_1 = require("node:fs");
@@ -25,17 +26,21 @@ function diskContent(projectRoot, path) {
25
26
  }
26
27
  /**
27
28
  * Expand the recorded module list against the target manifests. A module may
28
- * gain a new requirement after it was first added to a project; updates must
29
- * apply that requirement before rebuilding the dependent module or the target
30
- * tree can contain imports and wiring for files that were never installed.
29
+ * gain a new requirement after it was first added to a project, either another
30
+ * module or a provider capability. Updates must apply that requirement before
31
+ * the dependent module: modules configure in this order, and the target tree
32
+ * would otherwise wire files that were never installed. A capability resolves
33
+ * to the module implementing the project's selected provider, as `podo add`
34
+ * would choose it.
31
35
  */
32
- function targetModules(projectRoot, templatesDir, modules) {
36
+ function targetModules(projectRoot, templatesDir, modules, providers) {
33
37
  const modulesDir = (0, node_path_1.join)(templatesDir, "modules");
34
38
  const recorded = new Map(modules.map((module) => [module.name, module]));
35
39
  const ordered = [];
40
+ const added = [];
36
41
  const visiting = new Set();
37
42
  const visited = new Set();
38
- const visit = (name, requiredBy) => {
43
+ const visit = (name, requiredBy, capability) => {
39
44
  if (visited.has(name))
40
45
  return;
41
46
  if (visiting.has(name)) {
@@ -52,12 +57,24 @@ function targetModules(projectRoot, templatesDir, modules) {
52
57
  const moduleManifest = (0, add_1.readModuleManifest)(resolved.dir);
53
58
  for (const required of moduleManifest.requires ?? [])
54
59
  visit(required, name);
60
+ for (const required of moduleManifest.requiresCapabilities ?? []) {
61
+ visit((0, add_1.selectedProviderModule)(modulesDir, projectRoot, providers, required), name, required);
62
+ }
55
63
  visiting.delete(name);
56
64
  visited.add(name);
65
+ if (!existing && requiredBy) {
66
+ added.push({
67
+ name: resolved.name,
68
+ requiredBy,
69
+ ...(capability ? { capability, provider: providers[capability] } : {}),
70
+ });
71
+ }
57
72
  ordered.push({
58
73
  ...(existing ?? {
59
74
  name: resolved.name,
60
75
  addedWith: (0, lockfile_1.podokitVersion)(),
76
+ // A freshly added module ships its current layout, as `podo add` records it.
77
+ appliedMigrations: (moduleManifest.migrations ?? []).map((migration) => migration.id),
61
78
  }),
62
79
  order: ordered.length,
63
80
  ...(resolved.packageName
@@ -71,7 +88,7 @@ function targetModules(projectRoot, templatesDir, modules) {
71
88
  for (const module of [...modules].sort((a, b) => a.order - b.order)) {
72
89
  visit(module.name);
73
90
  }
74
- return ordered;
91
+ return { modules: ordered, added };
75
92
  }
76
93
  function targetManagedOverrides(projectRoot, templatesDir, modules, current = []) {
77
94
  const overrides = new Set(current);
@@ -102,7 +119,7 @@ function planUpdate(projectRoot, templatesDir, options = {}) {
102
119
  const lock = (0, lockfile_1.readFilesLock)(projectRoot);
103
120
  if (!manifest || !lock)
104
121
  throw new inspect_1.NotAProjectError();
105
- const modules = targetModules(projectRoot, templatesDir, manifest.modules);
122
+ const { modules, added } = targetModules(projectRoot, templatesDir, manifest.modules, manifest.providers);
106
123
  const newTree = (0, assemble_1.assembleProject)({
107
124
  templatesDir,
108
125
  template: manifest.template,
@@ -210,6 +227,7 @@ function planUpdate(projectRoot, templatesDir, options = {}) {
210
227
  toVersion: (0, lockfile_1.podokitVersion)(),
211
228
  template: manifest.template,
212
229
  modules: modules.map((module) => module.name),
230
+ addedModules: added,
213
231
  changes,
214
232
  };
215
233
  }
@@ -333,7 +351,7 @@ function applyUpdate(projectRoot, templatesDir, options = {}) {
333
351
  forceManagedPaths: options.forceManagedPaths,
334
352
  onlyPaths: options.onlyPaths,
335
353
  });
336
- const modules = targetModules(projectRoot, templatesDir, manifest.modules);
354
+ const { modules } = targetModules(projectRoot, templatesDir, manifest.modules, manifest.providers);
337
355
  const newTree = (0, assemble_1.assembleProject)({
338
356
  templatesDir,
339
357
  template: manifest.template,
@@ -396,6 +414,7 @@ function applyUpdate(projectRoot, templatesDir, options = {}) {
396
414
  }
397
415
  }
398
416
  const result = {
417
+ addedModules: plan.addedModules.map((module) => module.name),
399
418
  written: [],
400
419
  moved: (0, module_migrations_1.applyModuleMigrations)(projectRoot, migrationPlan),
401
420
  removed: [],
@@ -469,6 +488,12 @@ function applyUpdate(projectRoot, templatesDir, options = {}) {
469
488
  });
470
489
  return result;
471
490
  }
491
+ /** Why an update adds a module, for the plan report. */
492
+ function addedModuleReason(module) {
493
+ return module.capability
494
+ ? `${module.capability} provider "${module.provider}" required by ${module.requiredBy}`
495
+ : `required by ${module.requiredBy}`;
496
+ }
472
497
  /** Count actions for a one-line summary. */
473
498
  function summarize(plan) {
474
499
  const counts = {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@podosoft/podokit",
3
- "version": "1.2.0",
3
+ "version": "1.3.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": {