@nextsparkjs/core 0.1.0-beta.196 → 0.1.0-beta.197

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 (136) hide show
  1. package/README.md +2 -0
  2. package/dist/components/auth/forms/SignupForm.d.ts.map +1 -1
  3. package/dist/components/auth/forms/SignupForm.js +55 -45
  4. package/dist/components/dashboard/block-editor/config-panel.d.ts.map +1 -1
  5. package/dist/components/dashboard/block-editor/config-panel.js +3 -2
  6. package/dist/components/dashboard/block-editor/entity-fields-sidebar.d.ts.map +1 -1
  7. package/dist/components/dashboard/block-editor/entity-fields-sidebar.js +3 -2
  8. package/dist/components/users/tables/UsersTable.d.ts.map +1 -1
  9. package/dist/components/users/tables/UsersTable.js +3 -4
  10. package/dist/hooks/useAuthReadiness.d.ts +1 -0
  11. package/dist/hooks/useAuthReadiness.d.ts.map +1 -1
  12. package/dist/hooks/useAuthReadiness.js +3 -1
  13. package/dist/lib/actions/entity.actions.js +2 -2
  14. package/dist/lib/actions/team.actions.js +2 -2
  15. package/dist/lib/actions/user.actions.js +4 -4
  16. package/dist/lib/api/auth/dual-auth.d.ts.map +1 -1
  17. package/dist/lib/api/auth/dual-auth.js +3 -2
  18. package/dist/lib/api/auth.d.ts.map +1 -1
  19. package/dist/lib/api/auth.js +7 -0
  20. package/dist/lib/api/entity/generic-handler.d.ts.map +1 -1
  21. package/dist/lib/api/entity/generic-handler.js +66 -12
  22. package/dist/lib/api/entity-handler.js +2 -2
  23. package/dist/lib/api/helpers.d.ts.map +1 -1
  24. package/dist/lib/api/helpers.js +4 -2
  25. package/dist/lib/api/post-categories.d.ts +29 -0
  26. package/dist/lib/api/post-categories.d.ts.map +1 -0
  27. package/dist/lib/api/post-categories.js +33 -0
  28. package/dist/lib/auth/auth-methods.d.ts +5 -2
  29. package/dist/lib/auth/auth-methods.d.ts.map +1 -1
  30. package/dist/lib/auth/auth-methods.js +4 -2
  31. package/dist/lib/auth/authorization-session.d.ts +14 -0
  32. package/dist/lib/auth/authorization-session.d.ts.map +1 -0
  33. package/dist/lib/auth/authorization-session.js +10 -0
  34. package/dist/lib/auth/request-session.d.ts +5 -0
  35. package/dist/lib/auth/request-session.d.ts.map +1 -1
  36. package/dist/lib/auth/request-session.js +3 -0
  37. package/dist/lib/auth/runtime-readiness.d.ts +7 -4
  38. package/dist/lib/auth/runtime-readiness.d.ts.map +1 -1
  39. package/dist/lib/auth/runtime-readiness.js +7 -6
  40. package/dist/lib/auth/sample-accounts.d.ts +2 -1
  41. package/dist/lib/auth/sample-accounts.d.ts.map +1 -1
  42. package/dist/lib/auth/sample-accounts.js +4 -1
  43. package/dist/lib/auth/suspension.d.ts +22 -0
  44. package/dist/lib/auth/suspension.d.ts.map +1 -0
  45. package/dist/lib/auth/suspension.js +43 -0
  46. package/dist/lib/auth.d.ts.map +1 -1
  47. package/dist/lib/auth.js +15 -15
  48. package/dist/lib/config/app.config.d.ts.map +1 -1
  49. package/dist/lib/config/app.config.js +6 -7
  50. package/dist/lib/config/types.d.ts +8 -6
  51. package/dist/lib/config/types.d.ts.map +1 -1
  52. package/dist/lib/db-pool.d.ts +31 -0
  53. package/dist/lib/db-pool.d.ts.map +1 -0
  54. package/dist/lib/db-pool.js +75 -0
  55. package/dist/lib/db.d.ts +1 -1
  56. package/dist/lib/db.d.ts.map +1 -1
  57. package/dist/lib/db.js +11 -14
  58. package/dist/lib/entities/api-generator.js +5 -5
  59. package/dist/lib/oauth/token-refresh.d.ts.map +1 -1
  60. package/dist/lib/oauth/token-refresh.js +3 -6
  61. package/dist/lib/permissions/check.d.ts +9 -0
  62. package/dist/lib/permissions/check.d.ts.map +1 -1
  63. package/dist/lib/permissions/check.js +11 -1
  64. package/dist/lib/services/generic-entity.service.d.ts.map +1 -1
  65. package/dist/lib/services/generic-entity.service.js +39 -7
  66. package/dist/lib/services/transactional-meta.service.d.ts.map +1 -1
  67. package/dist/lib/services/transactional-meta.service.js +7 -8
  68. package/dist/lib/validation.d.ts +5 -0
  69. package/dist/lib/validation.d.ts.map +1 -1
  70. package/dist/lib/validation.js +2 -0
  71. package/dist/migrations/030_taxonomies_team_writes.sql +29 -0
  72. package/dist/migrations/031_users_role_suspended.sql +41 -0
  73. package/dist/routes/(auth)/signup/page.cc.d.ts +3 -1
  74. package/dist/routes/(auth)/signup/page.cc.d.ts.map +1 -1
  75. package/dist/routes/(auth)/signup/page.cc.js +2 -2
  76. package/dist/routes/(auth)/signup/page.d.ts +3 -1
  77. package/dist/routes/(auth)/signup/page.d.ts.map +1 -1
  78. package/dist/routes/(auth)/signup/page.js +4 -3
  79. package/dist/routes/_internal/area-access.js +2 -2
  80. package/dist/routes/api/devtools/config/entities/route.js +2 -2
  81. package/dist/routes/api/devtools/config/theme/route.js +2 -2
  82. package/dist/routes/api/devtools/tests/[...path]/route.js +2 -2
  83. package/dist/routes/api/devtools/tests/route.js +2 -2
  84. package/dist/routes/api/superadmin/subscriptions/route.js +2 -2
  85. package/dist/routes/api/superadmin/teams/[teamId]/route.js +2 -2
  86. package/dist/routes/api/superadmin/teams/route.js +2 -2
  87. package/dist/routes/api/superadmin/users/[userId]/route.d.ts +5 -3
  88. package/dist/routes/api/superadmin/users/[userId]/route.d.ts.map +1 -1
  89. package/dist/routes/api/superadmin/users/[userId]/route.js +46 -23
  90. package/dist/routes/api/superadmin/users/route.js +2 -2
  91. package/dist/routes/api/user/delete-account/route.d.ts.map +1 -1
  92. package/dist/routes/api/user/delete-account/route.js +2 -3
  93. package/dist/routes/api/user/plan-flags/route.d.ts.map +1 -1
  94. package/dist/routes/api/user/plan-flags/route.js +5 -5
  95. package/dist/routes/api/user/profile/route.js +3 -3
  96. package/dist/routes/api/v1/auth/signup-with-invite/route.d.ts.map +1 -1
  97. package/dist/routes/api/v1/auth/signup-with-invite/route.js +80 -44
  98. package/dist/routes/api/v1/post-categories/[id]/route.d.ts.map +1 -1
  99. package/dist/routes/api/v1/post-categories/[id]/route.js +39 -34
  100. package/dist/routes/api/v1/post-categories/route.d.ts.map +1 -1
  101. package/dist/routes/api/v1/post-categories/route.js +18 -14
  102. package/dist/routes/api/v1/team-invitations/route.d.ts.map +1 -1
  103. package/dist/routes/api/v1/team-invitations/route.js +12 -5
  104. package/dist/routes/superadmin/users/page.js +2 -2
  105. package/dist/templates/next.config.mjs +11 -31
  106. package/dist/templates/projects/blog/package.json +1 -1
  107. package/dist/templates/projects/crm/package.json +1 -1
  108. package/dist/templates/projects/productivity/package.json +1 -1
  109. package/dist/templates/projects/starter/config/app.config.ts +8 -0
  110. package/dist/templates/proxy.ts +6 -3
  111. package/migrations/030_taxonomies_team_writes.sql +29 -0
  112. package/migrations/031_users_role_suspended.sql +41 -0
  113. package/package.json +3 -3
  114. package/scripts/build/registry/__tests__/build-output.test.mjs +1 -1
  115. package/scripts/build/registry/__tests__/project-mode-root-first.test.mjs +6 -6
  116. package/scripts/build/registry/__tests__/write-places.test.mjs +1 -1
  117. package/scripts/build/registry/contracts/__tests__/contracts-wire.test.mjs +1 -1
  118. package/scripts/build/registry/contracts/__tests__/contracts.test.mjs +1 -1
  119. package/scripts/build/registry/host/README.md +2 -2
  120. package/scripts/build/registry/host/__tests__/facade-emitter.test.mjs +14 -13
  121. package/scripts/build/registry/host/__tests__/host-mode.test.mjs +1 -1
  122. package/scripts/build/registry/host/__tests__/host-project-composition.test.mjs +1 -1
  123. package/scripts/build/registry/host/__tests__/host-project.test.mjs +2 -2
  124. package/scripts/build/registry/host/__tests__/host-proxy-areas.test.mjs +29 -2
  125. package/scripts/build/registry/host/__tests__/host-robustness.test.mjs +1 -1
  126. package/scripts/build/registry/host/facade-emitter.mjs +1 -1
  127. package/scripts/build/registry/host/next-route-exports.json +2 -2
  128. package/scripts/build/registry/host/proxy-areas.mjs +29 -2
  129. package/scripts/db/verify-theme-migrations.mjs +1 -1
  130. package/templates/next.config.mjs +11 -31
  131. package/templates/projects/blog/package.json +1 -1
  132. package/templates/projects/crm/package.json +1 -1
  133. package/templates/projects/productivity/package.json +1 -1
  134. package/templates/projects/starter/config/app.config.ts +8 -0
  135. package/templates/proxy.ts +6 -3
  136. package/tests/jest/__mocks__/@nextsparkjs/registries/permissions-registry.ts +5 -4
@@ -1,5 +1,5 @@
1
1
  import { NextResponse } from "next/server";
2
- import { authenticateRequest, createAuthFailureResponse } from "@nextsparkjs/core/lib/api/auth/dual-auth";
2
+ import { categoryReadTeam, categoryWriter } from "@nextsparkjs/core/lib/api/post-categories";
3
3
  import { query as dbQuery } from "@nextsparkjs/core/lib/db";
4
4
  import * as z from "zod";
5
5
  import { withRateLimitTier } from "@nextsparkjs/core/lib/api/rate-limit";
@@ -15,15 +15,33 @@ const updateCategorySchema = z.object({
15
15
  isDefault: z.boolean().optional(),
16
16
  isActive: z.boolean().optional()
17
17
  });
18
+ async function findCategory(id, writer) {
19
+ const found = await dbQuery(
20
+ `SELECT "teamId" FROM taxonomies WHERE id = $1 AND type = 'post_category' AND ("teamId" IS NULL OR "teamId" = $2)`,
21
+ [id, writer.teamId]
22
+ );
23
+ if (found.rows.length === 0) {
24
+ return { response: NextResponse.json({ success: false, error: "Category not found" }, { status: 404 }) };
25
+ }
26
+ if (found.rows[0].teamId === null && !writer.superadmin) {
27
+ return { response: NextResponse.json(
28
+ { success: false, error: "Only a superadmin can change a category shared by every team", code: "PERMISSION_DENIED" },
29
+ { status: 403 }
30
+ ) };
31
+ }
32
+ return found.rows[0];
33
+ }
18
34
  export const GET = withRateLimitTier(async (request, { params }) => {
19
35
  try {
20
36
  const { id } = await params;
37
+ const scope = await categoryReadTeam(request);
38
+ if ("response" in scope) return scope.response;
21
39
  const result = await dbQuery(
22
40
  `SELECT id, name, slug, description, icon, color, "parentId", "order", "isDefault", "isActive",
23
41
  "createdAt", "updatedAt"
24
42
  FROM taxonomies
25
- WHERE id = $1 AND type = 'post_category' AND "deletedAt" IS NULL`,
26
- [id]
43
+ WHERE id = $1 AND type = 'post_category' AND "deletedAt" IS NULL AND ("teamId" IS NULL OR "teamId" = $2)`,
44
+ [id, scope.teamId]
27
45
  );
28
46
  if (result.rows.length === 0) {
29
47
  return NextResponse.json({ success: false, error: "Category not found" }, { status: 404 });
@@ -39,24 +57,17 @@ export const GET = withRateLimitTier(async (request, { params }) => {
39
57
  }, "read");
40
58
  export const PUT = withRateLimitTier(async (request, { params }) => {
41
59
  try {
42
- const authResult = await authenticateRequest(request, { requiredScope: "posts:write" });
43
- if (!authResult.success || !authResult.user) {
44
- return createAuthFailureResponse(authResult);
45
- }
60
+ const writer = await categoryWriter(request, "update");
61
+ if ("response" in writer) return writer.response;
46
62
  const body = await request.json();
47
63
  const data = updateCategorySchema.parse(body);
48
64
  const { id } = await params;
49
- const typeCheck = await dbQuery(
50
- "SELECT id FROM taxonomies WHERE id = $1 AND type = 'post_category'",
51
- [id]
52
- );
53
- if (typeCheck.rows.length === 0) {
54
- return NextResponse.json({ success: false, error: "Category not found" }, { status: 404 });
55
- }
65
+ const row = await findCategory(id, writer);
66
+ if ("response" in row) return row.response;
56
67
  if (data.slug) {
57
68
  const slugCheck = await dbQuery(
58
- "SELECT id FROM taxonomies WHERE type = 'post_category' AND slug = $1 AND id != $2",
59
- [data.slug, id]
69
+ `SELECT id FROM taxonomies WHERE type = 'post_category' AND slug = $1 AND id != $2 AND "teamId" IS NOT DISTINCT FROM $3`,
70
+ [data.slug, id, row.teamId]
60
71
  );
61
72
  if (slugCheck.rows.length > 0) {
62
73
  return NextResponse.json(
@@ -67,8 +78,8 @@ export const PUT = withRateLimitTier(async (request, { params }) => {
67
78
  }
68
79
  if (data.parentId) {
69
80
  const parentCheck = await dbQuery(
70
- "SELECT id FROM taxonomies WHERE id = $1 AND type = 'post_category'",
71
- [data.parentId]
81
+ `SELECT id FROM taxonomies WHERE id = $1 AND type = 'post_category' AND ("teamId" IS NULL OR "teamId" IS NOT DISTINCT FROM $2)`,
82
+ [data.parentId, row.teamId]
72
83
  );
73
84
  if (parentCheck.rows.length === 0) {
74
85
  return NextResponse.json({
@@ -132,7 +143,8 @@ export const PUT = withRateLimitTier(async (request, { params }) => {
132
143
  return NextResponse.json({ success: true, data: current.rows[0] });
133
144
  }
134
145
  values.push(id);
135
- const query = `UPDATE taxonomies SET ${updates.join(", ")} WHERE id = $${paramIndex} RETURNING *`;
146
+ values.push(row.teamId);
147
+ const query = `UPDATE taxonomies SET ${updates.join(", ")} WHERE id = $${paramIndex} AND "teamId" IS NOT DISTINCT FROM $${paramIndex + 1} RETURNING *`;
136
148
  const result = await dbQuery(query, values);
137
149
  if (result.rows.length === 0) {
138
150
  return NextResponse.json({ success: false, error: "Category not found" }, { status: 404 });
@@ -152,18 +164,11 @@ export const PUT = withRateLimitTier(async (request, { params }) => {
152
164
  }, "write");
153
165
  export const DELETE = withRateLimitTier(async (request, { params }) => {
154
166
  try {
155
- const authResult = await authenticateRequest(request, { requiredScope: "posts:delete" });
156
- if (!authResult.success || !authResult.user) {
157
- return createAuthFailureResponse(authResult);
158
- }
167
+ const writer = await categoryWriter(request, "delete");
168
+ if ("response" in writer) return writer.response;
159
169
  const { id } = await params;
160
- const typeCheck = await dbQuery(
161
- "SELECT id FROM taxonomies WHERE id = $1 AND type = 'post_category'",
162
- [id]
163
- );
164
- if (typeCheck.rows.length === 0) {
165
- return NextResponse.json({ success: false, error: "Category not found" }, { status: 404 });
166
- }
170
+ const row = await findCategory(id, writer);
171
+ if ("response" in row) return row.response;
167
172
  let usageCount = 0;
168
173
  try {
169
174
  const usageCheck = await dbQuery(
@@ -177,8 +182,8 @@ export const DELETE = withRateLimitTier(async (request, { params }) => {
177
182
  }
178
183
  if (usageCount > 0) {
179
184
  await dbQuery(
180
- 'UPDATE taxonomies SET "deletedAt" = now() WHERE id = $1',
181
- [id]
185
+ 'UPDATE taxonomies SET "deletedAt" = now() WHERE id = $1 AND "teamId" IS NOT DISTINCT FROM $2',
186
+ [id, row.teamId]
182
187
  );
183
188
  return NextResponse.json({
184
189
  success: true,
@@ -186,8 +191,8 @@ export const DELETE = withRateLimitTier(async (request, { params }) => {
186
191
  });
187
192
  } else {
188
193
  const result = await dbQuery(
189
- "DELETE FROM taxonomies WHERE id = $1 RETURNING id",
190
- [id]
194
+ 'DELETE FROM taxonomies WHERE id = $1 AND "teamId" IS NOT DISTINCT FROM $2 RETURNING id',
195
+ [id, row.teamId]
191
196
  );
192
197
  if (result.rows.length === 0) {
193
198
  return NextResponse.json({ success: false, error: "Category not found" }, { status: 404 });
@@ -1 +1 @@
1
- {"version":3,"file":"route.d.ts","sourceRoot":"","sources":["../../../../../src/routes/api/v1/post-categories/route.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,aAAa,CAAA;AAKvD,OAAO,EAAE,aAAa,EAAE,MAAM,yCAAyC,CAAA;AAyBvE,eAAO,MAAM,GAAG,iDAkBN,CAAC;AAGX,eAAO,MAAM,IAAI,iDAsEN,CAAC;AAEZ,eAAO,MAAM,OAAO,sBAAgB,CAAA"}
1
+ {"version":3,"file":"route.d.ts","sourceRoot":"","sources":["../../../../../src/routes/api/v1/post-categories/route.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,aAAa,CAAA;AAKvD,OAAO,EAAE,aAAa,EAAE,MAAM,yCAAyC,CAAA;AA0BvE,eAAO,MAAM,GAAG,iDAuBN,CAAC;AAGX,eAAO,MAAM,IAAI,iDAqEN,CAAC;AAEZ,eAAO,MAAM,OAAO,sBAAgB,CAAA"}
@@ -1,5 +1,5 @@
1
1
  import { NextResponse } from "next/server";
2
- import { authenticateRequest, createAuthFailureResponse } from "@nextsparkjs/core/lib/api/auth/dual-auth";
2
+ import { categoryReadTeam, categoryWriter } from "@nextsparkjs/core/lib/api/post-categories";
3
3
  import { query as dbQuery } from "@nextsparkjs/core/lib/db";
4
4
  import * as z from "zod";
5
5
  import { withRateLimitTier } from "@nextsparkjs/core/lib/api/rate-limit";
@@ -17,14 +17,18 @@ const createCategorySchema = z.object({
17
17
  function generateSlug(name) {
18
18
  return name.toLowerCase().trim().replace(/[^a-z0-9\s-]/g, "").replace(/\s+/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
19
19
  }
20
- export const GET = withRateLimitTier(async () => {
20
+ export const GET = withRateLimitTier(async (request) => {
21
21
  try {
22
+ const scope = await categoryReadTeam(request);
23
+ if ("response" in scope) return scope.response;
22
24
  const result = await dbQuery(
23
25
  `SELECT id, name, slug, description, icon, color, "parentId", "order", "isDefault", "isActive",
24
26
  "createdAt", "updatedAt"
25
27
  FROM taxonomies
26
28
  WHERE type = 'post_category' AND "deletedAt" IS NULL AND "isActive" = true
27
- ORDER BY "order" ASC, name ASC`
29
+ AND ("teamId" IS NULL OR "teamId" = $1)
30
+ ORDER BY "order" ASC, name ASC`,
31
+ [scope.teamId]
28
32
  );
29
33
  return NextResponse.json({
30
34
  success: true,
@@ -37,16 +41,15 @@ export const GET = withRateLimitTier(async () => {
37
41
  }, "read");
38
42
  export const POST = withRateLimitTier(async (request) => {
39
43
  try {
40
- const authResult = await authenticateRequest(request, { requiredScope: "posts:write" });
41
- if (!authResult.success || !authResult.user) {
42
- return createAuthFailureResponse(authResult);
43
- }
44
+ const writer = await categoryWriter(request, "create");
45
+ if ("response" in writer) return writer.response;
46
+ const teamId = writer.teamId;
44
47
  const body = await request.json();
45
48
  const data = createCategorySchema.parse(body);
46
49
  const slug = data.slug || generateSlug(data.name);
47
50
  const existing = await dbQuery(
48
- "SELECT id FROM taxonomies WHERE type = 'post_category' AND slug = $1",
49
- [slug]
51
+ `SELECT id FROM taxonomies WHERE type = 'post_category' AND slug = $1 AND "teamId" = $2`,
52
+ [slug, teamId]
50
53
  );
51
54
  if (existing.rows.length > 0) {
52
55
  return NextResponse.json({
@@ -57,8 +60,8 @@ export const POST = withRateLimitTier(async (request) => {
57
60
  }
58
61
  if (data.parentId) {
59
62
  const parentCheck = await dbQuery(
60
- "SELECT id FROM taxonomies WHERE id = $1 AND type = 'post_category'",
61
- [data.parentId]
63
+ `SELECT id FROM taxonomies WHERE id = $1 AND type = 'post_category' AND ("teamId" IS NULL OR "teamId" = $2)`,
64
+ [data.parentId, teamId]
62
65
  );
63
66
  if (parentCheck.rows.length === 0) {
64
67
  return NextResponse.json({
@@ -69,8 +72,8 @@ export const POST = withRateLimitTier(async (request) => {
69
72
  }
70
73
  const result = await dbQuery(
71
74
  `INSERT INTO taxonomies (
72
- type, slug, name, description, icon, color, "parentId", "order", "isDefault", "userId"
73
- ) VALUES ('post_category', $1, $2, $3, $4, $5, $6, $7, $8, $9)
75
+ type, slug, name, description, icon, color, "parentId", "order", "isDefault", "userId", "teamId"
76
+ ) VALUES ('post_category', $1, $2, $3, $4, $5, $6, $7, $8, $9, $10)
74
77
  RETURNING *`,
75
78
  [
76
79
  slug,
@@ -81,7 +84,8 @@ export const POST = withRateLimitTier(async (request) => {
81
84
  data.parentId || null,
82
85
  data.order,
83
86
  data.isDefault,
84
- authResult.user.id
87
+ writer.userId,
88
+ teamId
85
89
  ]
86
90
  );
87
91
  return NextResponse.json({ success: true, data: result.rows[0] }, { status: 201 });
@@ -1 +1 @@
1
- {"version":3,"file":"route.d.ts","sourceRoot":"","sources":["../../../../../src/routes/api/v1/team-invitations/route.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,aAAa,CAAA;AAgBvD,wBAAsB,OAAO,CAAC,OAAO,EAAE,WAAW,kCAEjD;AAGD,eAAO,MAAM,GAAG,iDA2FL,CAAC"}
1
+ {"version":3,"file":"route.d.ts","sourceRoot":"","sources":["../../../../../src/routes/api/v1/team-invitations/route.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,aAAa,CAAA;AAgBvD,wBAAsB,OAAO,CAAC,OAAO,EAAE,WAAW,kCAEjD;AAGD,eAAO,MAAM,GAAG,iDAkGL,CAAC"}
@@ -25,11 +25,13 @@ export const GET = withRateLimitTier(withApiLogging(async (req) => {
25
25
  }
26
26
  const userEmail = authResult.user.email;
27
27
  const { searchParams } = new URL(req.url);
28
- const queryParams = {
29
- page: searchParams.get("page"),
30
- limit: searchParams.get("limit"),
31
- status: searchParams.get("status")
32
- };
28
+ const queryParams = Object.fromEntries(
29
+ Object.entries({
30
+ page: searchParams.get("page"),
31
+ limit: searchParams.get("limit"),
32
+ status: searchParams.get("status")
33
+ }).filter(([, value]) => value !== null && value !== "")
34
+ );
33
35
  const validatedQuery = invitationListQuerySchema.parse(queryParams);
34
36
  const { page, limit, status } = validatedQuery;
35
37
  const offset = (page - 1) * limit;
@@ -75,6 +77,11 @@ export const GET = withRateLimitTier(withApiLogging(async (req) => {
75
77
  const response = createApiResponse(invitations, paginationMeta);
76
78
  return addCorsHeaders(response, req);
77
79
  } catch (error) {
80
+ if (error instanceof Error && error.name === "ZodError") {
81
+ const zodError = error;
82
+ const response2 = createApiError("Validation error", 400, zodError.issues, "VALIDATION_ERROR");
83
+ return addCorsHeaders(response2, req);
84
+ }
78
85
  console.error("Error fetching invitations:", error);
79
86
  const response = createApiError("Internal server error", 500);
80
87
  return addCorsHeaders(response, req);
@@ -39,8 +39,8 @@ import { sel } from "@nextsparkjs/core/lib/selectors";
39
39
  import { withBasePath } from "@nextsparkjs/core/lib/base-path";
40
40
  const roleOptions = [
41
41
  { value: "member", label: "Member" },
42
- { value: "colaborator", label: "Colaborator" },
43
- { value: "admin", label: "Admin" }
42
+ { value: "developer", label: "Developer" },
43
+ { value: "suspended", label: "Suspended" }
44
44
  ];
45
45
  const statusOptions = [
46
46
  { value: "verified", label: "Verified" },
@@ -109,42 +109,21 @@ const nextConfig = {
109
109
  formats: ['image/avif', 'image/webp'],
110
110
  deviceSizes: [640, 750, 828, 1080, 1200, 1920],
111
111
  imageSizes: [16, 32, 48, 64, 96, 128, 256, 384],
112
+ // Hosts the server may fetch and resize through /_next/image.
113
+ // Keep this list to the exact hosts you use.
112
114
  remotePatterns: [
115
+ // Google profile pictures shown for accounts that sign in with Google.
113
116
  {
114
117
  protocol: 'https',
115
118
  hostname: 'lh3.googleusercontent.com',
116
119
  pathname: '/**',
117
120
  },
118
- {
119
- protocol: 'https',
120
- hostname: '*.public.blob.vercel-storage.com',
121
- pathname: '/**',
122
- },
123
- {
124
- protocol: 'https',
125
- hostname: 'images.unsplash.com',
126
- pathname: '/**',
127
- },
128
- {
129
- protocol: 'https',
130
- hostname: 'upload.wikimedia.org',
131
- pathname: '/**',
132
- },
133
- {
134
- protocol: 'https',
135
- hostname: 'i.pravatar.cc',
136
- pathname: '/**',
137
- },
138
- {
139
- protocol: 'https',
140
- hostname: '*.supabase.co',
141
- pathname: '/**',
142
- },
143
- {
144
- protocol: 'https',
145
- hostname: '*.cloudinary.com',
146
- pathname: '/**',
147
- },
121
+ // Examples: add the exact host of your own store, e.g. your Vercel Blob store
122
+ // (`<store-id>.public.blob.vercel-storage.com`) once uploads use it.
123
+ // { protocol: 'https', hostname: '<store-id>.public.blob.vercel-storage.com', pathname: '/**' },
124
+ // { protocol: 'https', hostname: '<project-ref>.supabase.co', pathname: '/**' },
125
+ // { protocol: 'https', hostname: 'res.cloudinary.com', pathname: '/<cloud-name>/**' },
126
+ // { protocol: 'https', hostname: 'images.unsplash.com', pathname: '/**' },
148
127
  ],
149
128
  },
150
129
  // Next.js sets TURBOPACK before it loads this file whenever Turbopack builds
@@ -158,7 +137,8 @@ const nextConfig = {
158
137
  async headers() {
159
138
  const isProduction = process.env.NODE_ENV === 'production';
160
139
 
161
- // Allowed image domains (must match remotePatterns above)
140
+ // Image hosts the browser may load directly (CSP img-src). The server only fetches the hosts in
141
+ // images.remotePatterns above; add a host there too when next/image should serve it.
162
142
  // NOTE: Wildcard patterns (*.public.blob.vercel-storage.com, *.supabase.co, *.cloudinary.com)
163
143
  // allow images from any account on these services for development flexibility.
164
144
  // For production with stricter security, consider restricting to specific account subdomains.
@@ -14,7 +14,7 @@
14
14
  "@nextsparkjs/core": ">=0.1.0-0",
15
15
  "@nextsparkjs/testing": ">=0.1.0-0",
16
16
  "lucide-react": "^0.539.0",
17
- "next": "~16.3.6",
17
+ "next": "~16.3.8",
18
18
  "next-intl": "^4.0.0",
19
19
  "react": "^19.2.0",
20
20
  "react-dom": "^19.2.0",
@@ -13,7 +13,7 @@
13
13
  "@nextsparkjs/core": ">=0.1.0-0",
14
14
  "@nextsparkjs/testing": ">=0.1.0-0",
15
15
  "lucide-react": "^0.539.0",
16
- "next": "~16.3.6",
16
+ "next": "~16.3.8",
17
17
  "next-intl": "^4.0.0",
18
18
  "react": "^19.2.0",
19
19
  "react-dom": "^19.2.0",
@@ -17,7 +17,7 @@
17
17
  "@nextsparkjs/core": ">=0.1.0-0",
18
18
  "@nextsparkjs/testing": ">=0.1.0-0",
19
19
  "lucide-react": "^0.539.0",
20
- "next": "~16.3.6",
20
+ "next": "~16.3.8",
21
21
  "react": "^19.2.0",
22
22
  "react-dom": "^19.2.0",
23
23
  "zod": "^4.0.0"
@@ -126,6 +126,14 @@ export const APP_CONFIG_OVERRIDES = {
126
126
  // methods: ['email-password', 'google'],
127
127
  // methods: ['email-otp', 'email-password', 'google'],
128
128
 
129
+ /**
130
+ * Password endpoints (sign-in/email, sign-up/email, reset). Left undeclared
131
+ * they follow `methods` in production (off under the passwordless preset) and
132
+ * stay on in development (dev keyring, `pnpm db:seed` users, Cypress).
133
+ * Uncomment to keep password logins in production (API clients, test users):
134
+ */
135
+ // emailAndPassword: { enabled: true },
136
+
129
137
  /**
130
138
  * Session duration / renewal in seconds (defaults: 7 days, renewed daily).
131
139
  * Long-lived sessions for an installed PWA, for example:
@@ -189,14 +189,17 @@ function redirectAccessDenied(request: NextRequest): NextResponse {
189
189
  * Nothing here leaves the process, so no forwarded scheme or host can point
190
190
  * the check anywhere. `disableRefresh`: this response cannot carry the renewed
191
191
  * session cookie, so the rolling renewal is left to the auth route
192
- * (lib/auth/session-refresh).
192
+ * (lib/auth/session-refresh). `disableCookieCache`: the session comes from the
193
+ * database, not the signed session_data cookie, so a sign-out, a revoked
194
+ * session or a role change counts at once (lib/auth/authorization-session).
195
+ * A suspended account's session is no session (lib/auth/suspension).
193
196
  */
194
197
  async function getSession(request: NextRequest): Promise<Session | null> {
195
198
  const session = await auth.api.getSession({
196
199
  headers: new Headers({ cookie: request.headers.get('cookie') || '' }),
197
- query: { disableRefresh: true },
200
+ query: { disableRefresh: true, disableCookieCache: true },
198
201
  })
199
- return session as Session | null
202
+ return session && (session.user as { role?: unknown }).role !== 'suspended' ? (session as Session) : null
200
203
  }
201
204
 
202
205
  /**
@@ -0,0 +1,29 @@
1
+ -- Migration: 030_taxonomies_team_writes.sql
2
+ -- Description: Taxonomy rows are written by members of their team; rows without a team only by a superadmin.
3
+ --
4
+ -- 021 let any authenticated user write a taxonomy row with "teamId" IS NULL. Post categories are now created in
5
+ -- the caller's team (lib/api/post-categories.ts), and the rows without a team that earlier releases created stay
6
+ -- readable by everyone but are changed only by a superadmin. This policy says the same on the RLS-enforced
7
+ -- connection: reads are as before, writes need the row's team (or is_superadmin()).
8
+ -- is_superadmin() also accepts a developer enrolled in the System Admin Team; the API (lib/api/post-categories.ts) is
9
+ -- stricter and lets only users.role = 'superadmin' change a row without a team.
10
+ -- Idempotent; the anon read policy from 006 is unchanged.
11
+
12
+ DROP POLICY IF EXISTS "taxonomies auth can do all" ON public.taxonomies;
13
+ DROP POLICY IF EXISTS "taxonomies auth can select" ON public.taxonomies;
14
+ DROP POLICY IF EXISTS "taxonomies team can write" ON public.taxonomies;
15
+
16
+ CREATE POLICY "taxonomies auth can select"
17
+ ON public.taxonomies
18
+ FOR SELECT TO authenticated
19
+ USING (
20
+ "teamId" IS NULL
21
+ OR "teamId" = ANY(public.get_user_team_ids())
22
+ OR public.is_superadmin()
23
+ );
24
+
25
+ CREATE POLICY "taxonomies team can write"
26
+ ON public.taxonomies
27
+ FOR ALL TO authenticated
28
+ USING ("teamId" = ANY(public.get_user_team_ids()) OR public.is_superadmin())
29
+ WITH CHECK ("teamId" = ANY(public.get_user_team_ids()) OR public.is_superadmin());
@@ -0,0 +1,41 @@
1
+ -- Migration: 031_users_role_suspended.sql
2
+ -- Description: users.role accepts 'suspended' (lib/auth/suspension.ts).
3
+ --
4
+ -- The superadmin "suspend" action sets users.role = 'suspended', which check_users_role (002: member, superadmin,
5
+ -- developer) refused. This adds 'suspended' to the roles the constraint already accepts and keeps every other one,
6
+ -- including roles a project added to it. Idempotent: a constraint that already accepts 'suspended' is left alone. A
7
+ -- constraint that is not a plain list of roles is left alone too, with a notice (add 'suspended' to it yourself).
8
+
9
+ DO $$
10
+ DECLARE
11
+ def TEXT;
12
+ roles TEXT[];
13
+ BEGIN
14
+ SELECT pg_get_constraintdef(c.oid) INTO def
15
+ FROM pg_constraint c
16
+ WHERE c.conrelid = 'public.users'::regclass AND c.conname = 'check_users_role';
17
+
18
+ IF def IS NULL THEN
19
+ ALTER TABLE public."users" ADD CONSTRAINT check_users_role
20
+ CHECK ("role" IN ('member', 'superadmin', 'developer', 'suspended'));
21
+ RETURN;
22
+ END IF;
23
+
24
+ IF def ~ '\msuspended\M' THEN
25
+ RETURN;
26
+ END IF;
27
+
28
+ -- "role" IN (...) is stored as: CHECK ((role = ANY (ARRAY['member'::text, ...])))
29
+ IF def !~ '^CHECK \(\(+"?role"?\)?(::text)? = ANY \(\(?ARRAY\[' THEN
30
+ RAISE NOTICE 'check_users_role is not a plain list of roles (%); add ''suspended'' to it to use the suspend action', def;
31
+ RETURN;
32
+ END IF;
33
+
34
+ SELECT array_agg(m[1]) INTO roles FROM regexp_matches(def, '''([^'']+)''', 'g') AS m;
35
+
36
+ ALTER TABLE public."users" DROP CONSTRAINT check_users_role;
37
+ EXECUTE format(
38
+ 'ALTER TABLE public."users" ADD CONSTRAINT check_users_role CHECK ("role" IN (%s))',
39
+ (SELECT string_agg(quote_literal(r), ', ') FROM unnest(array_append(roles, 'suspended')) AS r)
40
+ );
41
+ END $$;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nextsparkjs/core",
3
- "version": "0.1.0-beta.196",
3
+ "version": "0.1.0-beta.197",
4
4
  "description": "NextSpark - The complete SaaS framework for Next.js",
5
5
  "license": "MIT",
6
6
  "author": "NextSpark <hello@nextspark.dev>",
@@ -390,7 +390,7 @@
390
390
  "scripts"
391
391
  ],
392
392
  "peerDependencies": {
393
- "next": "~16.3.6",
393
+ "next": "~16.3.8",
394
394
  "react": "^19.2.0",
395
395
  "react-dom": "^19.2.0"
396
396
  },
@@ -496,7 +496,7 @@
496
496
  "use-intl": "^4.9.1",
497
497
  "uuid": "^13.0.0",
498
498
  "zod": "^4.3.6",
499
- "@nextsparkjs/ui": "0.1.0-beta.196"
499
+ "@nextsparkjs/ui": "0.1.0-beta.197"
500
500
  },
501
501
  "engines": {
502
502
  "node": ">=22.14.0"
@@ -29,7 +29,7 @@ const RAW_CONTROL = /[\u0000-\u0009\u000b-\u001f\u007f-\u009f\u061c\u200e\u200f\
29
29
  async function createProject(parent = tmpdir(), name = 'nextspark-build-output-test-') {
30
30
  const root = await mkdtemp(join(parent, name))
31
31
  await writeFile(join(root, 'nextspark.config.ts'), 'export default {}\n')
32
- await writeFile(join(root, 'package.json'), '{"dependencies":{"next":"16.3.6"}}')
32
+ await writeFile(join(root, 'package.json'), '{"dependencies":{"next":"16.3.8"}}')
33
33
  await mkdir(join(root, 'src', 'app'), { recursive: true })
34
34
  return root
35
35
  }
@@ -17,7 +17,7 @@ async function project(files = {}) {
17
17
 
18
18
  test('the nearest nextspark.config.ts owns every project source path', async () => {
19
19
  const root = await project({
20
- 'package.json': JSON.stringify({ dependencies: { next: '16.3.6' } }),
20
+ 'package.json': JSON.stringify({ dependencies: { next: '16.3.8' } }),
21
21
  'nextspark.config.ts': 'export default {}\n',
22
22
  })
23
23
  const nested = join(root, 'entities', 'tasks')
@@ -40,7 +40,7 @@ test('the nearest nextspark.config.ts owns every project source path', async ()
40
40
 
41
41
  test('a next.config file cannot select a project without nextspark.config.ts', async () => {
42
42
  const root = await project({
43
- 'package.json': JSON.stringify({ dependencies: { next: '16.3.6' } }),
43
+ 'package.json': JSON.stringify({ dependencies: { next: '16.3.8' } }),
44
44
  'next.config.mjs': 'export default {}\n',
45
45
  })
46
46
  try {
@@ -66,7 +66,7 @@ test('an enabled packaged plugin resolves through the project dependency tree',
66
66
  const root = await project({
67
67
  'package.json': JSON.stringify({
68
68
  dependencies: {
69
- next: '16.3.6',
69
+ next: '16.3.8',
70
70
  '@example/plugin-search': '1.0.0',
71
71
  },
72
72
  }),
@@ -98,7 +98,7 @@ test('an enabled packaged plugin that is not installed fails with an actionable
98
98
  const root = await project({
99
99
  'package.json': JSON.stringify({
100
100
  dependencies: {
101
- next: '16.3.6',
101
+ next: '16.3.8',
102
102
  '@example/plugin-search': '1.0.0',
103
103
  },
104
104
  }),
@@ -119,7 +119,7 @@ test('a packaged plugin cannot use a path segment as its logical name', async ()
119
119
  const root = await project({
120
120
  'package.json': JSON.stringify({
121
121
  dependencies: {
122
- next: '16.3.6',
122
+ next: '16.3.8',
123
123
  '@example/plugin-search': '1.0.0',
124
124
  },
125
125
  }),
@@ -146,7 +146,7 @@ test('a local plugin shadows an installed packaged plugin with the same NextSpar
146
146
  const root = await project({
147
147
  'package.json': JSON.stringify({
148
148
  dependencies: {
149
- next: '16.3.6',
149
+ next: '16.3.8',
150
150
  '@example/plugin-search': '1.0.0',
151
151
  },
152
152
  }),
@@ -269,7 +269,7 @@ async function snapshot(root) {
269
269
  async function buildableProject() {
270
270
  const project = await directory()
271
271
  await writeIn(project.root, 'nextspark.config.ts', 'export default {}\n')
272
- await writeIn(project.root, 'package.json', '{"dependencies":{"next":"16.3.6"}}')
272
+ await writeIn(project.root, 'package.json', '{"dependencies":{"next":"16.3.8"}}')
273
273
  await writeIn(project.root, 'templates/pricing/page.tsx', 'export default function Pricing() { return null }\n')
274
274
  return project
275
275
  }
@@ -22,7 +22,7 @@ async function planFor(files) {
22
22
  const root = temp()
23
23
  try {
24
24
  write(root, 'nextspark.config.ts', 'export default { plugins: [] }\n')
25
- write(root, 'package.json', JSON.stringify({ name: 'wire', dependencies: { next: '16.3.6' } }))
25
+ write(root, 'package.json', JSON.stringify({ name: 'wire', dependencies: { next: '16.3.8' } }))
26
26
  write(root, 'config/theme.config.ts', "export const wireThemeConfig = { name: 'wire' }\n")
27
27
  for (const [path, content] of Object.entries(files)) write(root, path, content)
28
28
  const entities = await discoverAllEntities(getConfig(root), { includeCore: false })
@@ -73,7 +73,7 @@ const NOTES_CONFIG = `export const noteEntityConfig = {
73
73
  function project(extra = {}) {
74
74
  const root = temp()
75
75
  write(root, 'nextspark.config.ts', 'export default { plugins: [] }\n')
76
- write(root, 'package.json', JSON.stringify({ name: 'contracts-project', dependencies: { next: '16.3.6' } }))
76
+ write(root, 'package.json', JSON.stringify({ name: 'contracts-project', dependencies: { next: '16.3.8' } }))
77
77
  write(root, 'config/theme.config.ts', "export const contractsThemeConfig = { name: 'contracts' }\n")
78
78
  write(root, 'entities/tasks/tasks.config.ts', TASKS_CONFIG)
79
79
  write(root, 'entities/tasks/tasks.fields.ts', FIELDS)
@@ -28,7 +28,7 @@ const { target, content } = await emitFacade({
28
28
  Every problem is a `FacadeEmitError` carrying all diagnostics (`code`, `file`, `line`, `exportName`,
29
29
  `message`); nothing is dropped silently.
30
30
 
31
- ## Export table (Next.js 16.3.6)
31
+ ## Export table (Next.js 16.3.8)
32
32
 
33
33
  Source of truth: `next-route-exports.json`. The tests fail when the installed Next.js version differs,
34
34
  when Next's `AppSegmentConfigSchemaKeys` or `HTTP_METHODS` change, and when this rendering is stale.
@@ -71,7 +71,7 @@ export { listSlugs as generateStaticParams }
71
71
  Metadata image kinds accept Next's one-digit variants (`icon1`, `apple-icon9`, `opengraph-image2`,
72
72
  `twitter-image3`), mapped to the base kind.
73
73
 
74
- The emitter resolves `next/package.json` once (project first, then core) and accepts only `~16.3.6`: the table's
74
+ The emitter resolves `next/package.json` once (project first, then core) and accepts only `~16.3.8`: the table's
75
75
  `next` is the verified baseline, and any stable 16.3.x patch at or above it is accepted, while other minors/majors,
76
76
  lower patches and prereleases are refused (`NS_HOST_UNSUPPORTED_NEXT_VERSION`). It loads Next's segment config
77
77
  schema from that same package root.