@nextsparkjs/ai-workflow 0.1.0-beta.166 → 0.1.0-beta.168
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/claude/skills/README.md +72 -47
- package/claude/skills/anthropics-mcp-builder/LICENSE.txt +202 -0
- package/claude/skills/anthropics-mcp-builder/SKILL.md +236 -0
- package/claude/skills/anthropics-mcp-builder/reference/evaluation.md +602 -0
- package/claude/skills/anthropics-mcp-builder/reference/mcp_best_practices.md +249 -0
- package/claude/skills/anthropics-mcp-builder/reference/node_mcp_server.md +970 -0
- package/claude/skills/anthropics-mcp-builder/reference/python_mcp_server.md +719 -0
- package/claude/skills/anthropics-mcp-builder/scripts/connections.py +151 -0
- package/claude/skills/anthropics-mcp-builder/scripts/evaluation.py +373 -0
- package/claude/skills/anthropics-mcp-builder/scripts/example_evaluation.xml +22 -0
- package/claude/skills/anthropics-mcp-builder/scripts/requirements.txt +2 -0
- package/claude/skills/anthropics-skill-creator/LICENSE.txt +202 -0
- package/claude/skills/anthropics-skill-creator/SKILL.md +485 -0
- package/claude/skills/anthropics-skill-creator/agents/analyzer.md +274 -0
- package/claude/skills/anthropics-skill-creator/agents/comparator.md +202 -0
- package/claude/skills/anthropics-skill-creator/agents/grader.md +223 -0
- package/claude/skills/anthropics-skill-creator/assets/eval_review.html +146 -0
- package/claude/skills/anthropics-skill-creator/eval-viewer/generate_review.py +471 -0
- package/claude/skills/anthropics-skill-creator/eval-viewer/viewer.html +1325 -0
- package/claude/skills/anthropics-skill-creator/references/schemas.md +430 -0
- package/claude/skills/anthropics-skill-creator/scripts/__init__.py +0 -0
- package/claude/skills/anthropics-skill-creator/scripts/aggregate_benchmark.py +401 -0
- package/claude/skills/anthropics-skill-creator/scripts/generate_report.py +326 -0
- package/claude/skills/anthropics-skill-creator/scripts/improve_description.py +247 -0
- package/claude/skills/anthropics-skill-creator/scripts/package_skill.py +136 -0
- package/claude/skills/anthropics-skill-creator/scripts/quick_validate.py +103 -0
- package/claude/skills/anthropics-skill-creator/scripts/run_eval.py +310 -0
- package/claude/skills/anthropics-skill-creator/scripts/run_loop.py +328 -0
- package/claude/skills/anthropics-skill-creator/scripts/utils.py +47 -0
- package/claude/skills/api-bypass-layers/SKILL.md +20 -7
- package/claude/skills/database-migrations/SKILL.md +23 -8
- package/claude/skills/entity-system/SKILL.md +2 -2
- package/claude/skills/entity-system/scripts/generate-migration.py +64 -54
- package/claude/skills/nextjs-api-development/SKILL.md +6 -1
- package/claude/skills/permissions-system/SKILL.md +11 -2
- package/claude/skills/rls-enforcement/SKILL.md +192 -0
- package/claude/skills/rls-enforcement/evals/RESULTS.md +24 -0
- package/claude/skills/rls-enforcement/evals/evals.json +56 -0
- package/claude/skills/service-layer/SKILL.md +13 -2
- package/package.json +1 -1
- package/LICENSE +0 -21
|
@@ -56,10 +56,11 @@ FIELD_TYPE_MAP = {
|
|
|
56
56
|
'select': 'VARCHAR(100)',
|
|
57
57
|
'multiselect': 'TEXT[]',
|
|
58
58
|
'tags': 'TEXT[]',
|
|
59
|
-
|
|
60
|
-
'relation
|
|
61
|
-
'
|
|
62
|
-
'
|
|
59
|
+
# FKs are TEXT: NextSpark ids are TEXT (Better Auth + gen_random_uuid()::text), not UUID
|
|
60
|
+
'relation': 'TEXT',
|
|
61
|
+
'relation-multi': 'TEXT[]',
|
|
62
|
+
'reference': 'TEXT',
|
|
63
|
+
'user': 'TEXT',
|
|
63
64
|
'phone': 'VARCHAR(50)',
|
|
64
65
|
'rating': 'INTEGER',
|
|
65
66
|
'range': 'DECIMAL',
|
|
@@ -124,16 +125,19 @@ def parse_fields_file(fields_path: Path) -> List[Dict]:
|
|
|
124
125
|
|
|
125
126
|
def generate_column_sql(field: Dict) -> str:
|
|
126
127
|
"""Generate SQL column definition from field."""
|
|
127
|
-
|
|
128
|
+
# NextSpark column names are camelCase (e.g. "firstName"), even though table
|
|
129
|
+
# names are snake_case (e.g. "team_members"). Field names from config may be
|
|
130
|
+
# camelCase already (no-op) or kebab-case (converted).
|
|
131
|
+
name = to_camel_case(field['name'])
|
|
128
132
|
field_type = field['type']
|
|
129
133
|
required = field['required']
|
|
130
134
|
|
|
131
135
|
sql_type = FIELD_TYPE_MAP.get(field_type, 'TEXT')
|
|
132
136
|
|
|
133
|
-
# Handle select fields with CHECK constraint
|
|
137
|
+
# Handle select fields with CHECK constraint (quote the camelCase column)
|
|
134
138
|
if field_type == 'select' and 'options' in field:
|
|
135
139
|
options = ', '.join(f"'{v}'" for v in field['options'])
|
|
136
|
-
sql_type = f
|
|
140
|
+
sql_type = f'VARCHAR(100) CHECK ("{name}" IN ({options}))'
|
|
137
141
|
|
|
138
142
|
nullable = '' if required else ''
|
|
139
143
|
not_null = ' NOT NULL' if required else ''
|
|
@@ -146,10 +150,10 @@ def generate_table_migration(entity_slug: str, fields: List[Dict], options: Dict
|
|
|
146
150
|
table_name = to_snake_case(entity_slug)
|
|
147
151
|
timestamp = datetime.now().isoformat()
|
|
148
152
|
|
|
149
|
-
# Build column definitions
|
|
153
|
+
# Build column definitions (NextSpark: TEXT ids, camelCase columns, "users" table)
|
|
150
154
|
columns = [
|
|
151
|
-
' "id"
|
|
152
|
-
' "
|
|
155
|
+
' "id" TEXT PRIMARY KEY DEFAULT gen_random_uuid()::text',
|
|
156
|
+
' "userId" TEXT NOT NULL REFERENCES "users"(id) ON DELETE CASCADE',
|
|
153
157
|
]
|
|
154
158
|
|
|
155
159
|
for field in fields:
|
|
@@ -157,15 +161,15 @@ def generate_table_migration(entity_slug: str, fields: List[Dict], options: Dict
|
|
|
157
161
|
|
|
158
162
|
# Timestamps
|
|
159
163
|
columns.extend([
|
|
160
|
-
' "
|
|
161
|
-
' "
|
|
164
|
+
' "createdAt" TIMESTAMPTZ NOT NULL DEFAULT now()',
|
|
165
|
+
' "updatedAt" TIMESTAMPTZ NOT NULL DEFAULT now()',
|
|
162
166
|
])
|
|
163
167
|
|
|
164
168
|
# Soft delete
|
|
165
169
|
if options.get('soft_delete'):
|
|
166
170
|
columns.extend([
|
|
167
|
-
' "
|
|
168
|
-
' "
|
|
171
|
+
' "deletedAt" TIMESTAMPTZ',
|
|
172
|
+
' "deletedBy" TEXT REFERENCES "users"(id)',
|
|
169
173
|
])
|
|
170
174
|
|
|
171
175
|
# Build SQL
|
|
@@ -186,19 +190,19 @@ CREATE TABLE "{table_name}" (
|
|
|
186
190
|
if options.get('with_indexes', True):
|
|
187
191
|
sql += f'''
|
|
188
192
|
-- Indexes
|
|
189
|
-
CREATE INDEX "idx_{table_name}
|
|
190
|
-
CREATE INDEX "idx_{table_name}
|
|
193
|
+
CREATE INDEX "idx_{table_name}_userId" ON "{table_name}" ("userId");
|
|
194
|
+
CREATE INDEX "idx_{table_name}_createdAt" ON "{table_name}" ("createdAt" DESC);
|
|
191
195
|
'''
|
|
192
|
-
# Add indexes for searchable/sortable fields
|
|
196
|
+
# Add indexes for searchable/sortable fields (camelCase column names)
|
|
193
197
|
for field in fields:
|
|
194
|
-
name =
|
|
198
|
+
name = to_camel_case(field['name'])
|
|
195
199
|
if field['type'] in ['text', 'textarea']:
|
|
196
|
-
sql += f'CREATE INDEX "idx_{table_name}_{name}_search" ON "{table_name}" USING gin(to_tsvector(\'english\', {name}));\n'
|
|
200
|
+
sql += f'CREATE INDEX "idx_{table_name}_{name}_search" ON "{table_name}" USING gin(to_tsvector(\'english\', "{name}"));\n'
|
|
197
201
|
elif field['type'] == 'select':
|
|
198
202
|
sql += f'CREATE INDEX "idx_{table_name}_{name}" ON "{table_name}" ("{name}");\n'
|
|
199
203
|
|
|
200
204
|
if options.get('soft_delete'):
|
|
201
|
-
sql += f'CREATE INDEX "idx_{table_name}
|
|
205
|
+
sql += f'CREATE INDEX "idx_{table_name}_deletedAt" ON "{table_name}" ("deletedAt");\n'
|
|
202
206
|
|
|
203
207
|
# RLS
|
|
204
208
|
if options.get('with_rls', True):
|
|
@@ -206,22 +210,21 @@ CREATE INDEX "idx_{table_name}_created_at" ON "{table_name}" ("created_at" DESC)
|
|
|
206
210
|
-- Enable Row Level Security
|
|
207
211
|
ALTER TABLE "{table_name}" ENABLE ROW LEVEL SECURITY;
|
|
208
212
|
|
|
209
|
-
-- RLS Policies
|
|
213
|
+
-- RLS Policies (NextSpark model: app.user_id GUC via public.get_auth_user_id();
|
|
214
|
+
-- bypass tier via public.can_bypass_rls(). System ops run on the SERVICE connection
|
|
215
|
+
-- which bypasses RLS by credential — no `TO service_role` policy is needed.)
|
|
210
216
|
CREATE POLICY "{table_name}_owner_access" ON "{table_name}"
|
|
211
|
-
FOR ALL
|
|
212
|
-
USING (
|
|
213
|
-
|
|
214
|
-
CREATE POLICY "{table_name}_service_role_access" ON "{table_name}"
|
|
215
|
-
FOR ALL TO service_role
|
|
216
|
-
USING (TRUE);
|
|
217
|
+
FOR ALL TO authenticated
|
|
218
|
+
USING (public.can_bypass_rls() OR "userId" = public.get_auth_user_id())
|
|
219
|
+
WITH CHECK (public.can_bypass_rls() OR "userId" = public.get_auth_user_id());
|
|
217
220
|
'''
|
|
218
221
|
|
|
219
222
|
sql += f'''
|
|
220
|
-
-- Trigger for
|
|
221
|
-
CREATE TRIGGER
|
|
223
|
+
-- Trigger for updatedAt (uses the core helper public.set_updated_at from migration 001)
|
|
224
|
+
CREATE TRIGGER {table_name}_set_updated_at
|
|
222
225
|
BEFORE UPDATE ON "{table_name}"
|
|
223
226
|
FOR EACH ROW
|
|
224
|
-
EXECUTE FUNCTION
|
|
227
|
+
EXECUTE FUNCTION public.set_updated_at();
|
|
225
228
|
|
|
226
229
|
COMMIT;
|
|
227
230
|
|
|
@@ -246,45 +249,52 @@ def generate_metas_migration(entity_slug: str) -> str:
|
|
|
246
249
|
-- Up Migration
|
|
247
250
|
BEGIN;
|
|
248
251
|
|
|
249
|
-
-- Create {table_name}_metas table
|
|
252
|
+
-- Create {table_name}_metas table (NextSpark meta convention: TEXT ids, camelCase,
|
|
253
|
+
-- entityId / metaKey / metaValue / isPublic)
|
|
250
254
|
CREATE TABLE "{table_name}_metas" (
|
|
251
|
-
"id"
|
|
252
|
-
"
|
|
253
|
-
"
|
|
254
|
-
"
|
|
255
|
-
"
|
|
256
|
-
"
|
|
257
|
-
|
|
255
|
+
"id" TEXT PRIMARY KEY DEFAULT gen_random_uuid()::text,
|
|
256
|
+
"entityId" TEXT NOT NULL REFERENCES "{table_name}"(id) ON DELETE CASCADE,
|
|
257
|
+
"metaKey" VARCHAR(255) NOT NULL,
|
|
258
|
+
"metaValue" JSONB,
|
|
259
|
+
"isPublic" BOOLEAN NOT NULL DEFAULT false,
|
|
260
|
+
"createdAt" TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
261
|
+
"updatedAt" TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
262
|
+
UNIQUE("entityId", "metaKey")
|
|
258
263
|
);
|
|
259
264
|
|
|
260
265
|
-- Indexes
|
|
261
|
-
CREATE INDEX "idx_{table_name}
|
|
262
|
-
CREATE INDEX "idx_{table_name}
|
|
263
|
-
CREATE INDEX "idx_{table_name}
|
|
266
|
+
CREATE INDEX "idx_{table_name}_metas_entityId" ON "{table_name}_metas" ("entityId");
|
|
267
|
+
CREATE INDEX "idx_{table_name}_metas_metaKey" ON "{table_name}_metas" ("metaKey");
|
|
268
|
+
CREATE INDEX "idx_{table_name}_metas_metaValue" ON "{table_name}_metas" USING gin("metaValue");
|
|
264
269
|
|
|
265
270
|
-- Enable Row Level Security
|
|
266
271
|
ALTER TABLE "{table_name}_metas" ENABLE ROW LEVEL SECURITY;
|
|
267
272
|
|
|
268
|
-
-- RLS Policies
|
|
273
|
+
-- RLS Policies (access inherited from the parent row's ownership; public metas readable)
|
|
269
274
|
CREATE POLICY "{table_name}_metas_owner_access" ON "{table_name}_metas"
|
|
270
|
-
FOR ALL
|
|
275
|
+
FOR ALL TO authenticated
|
|
271
276
|
USING (
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
277
|
+
public.can_bypass_rls()
|
|
278
|
+
OR EXISTS (
|
|
279
|
+
SELECT 1 FROM "{table_name}" p
|
|
280
|
+
WHERE p.id = "{table_name}_metas"."entityId"
|
|
281
|
+
AND p."userId" = public.get_auth_user_id()
|
|
282
|
+
)
|
|
283
|
+
)
|
|
284
|
+
WITH CHECK (
|
|
285
|
+
public.can_bypass_rls()
|
|
286
|
+
OR EXISTS (
|
|
287
|
+
SELECT 1 FROM "{table_name}" p
|
|
288
|
+
WHERE p.id = "{table_name}_metas"."entityId"
|
|
289
|
+
AND p."userId" = public.get_auth_user_id()
|
|
276
290
|
)
|
|
277
291
|
);
|
|
278
292
|
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
USING (TRUE);
|
|
282
|
-
|
|
283
|
-
-- Trigger for updated_at
|
|
284
|
-
CREATE TRIGGER update_{table_name}_metas_updated_at
|
|
293
|
+
-- Trigger for updatedAt (uses the core helper public.set_updated_at from migration 001)
|
|
294
|
+
CREATE TRIGGER {table_name}_metas_set_updated_at
|
|
285
295
|
BEFORE UPDATE ON "{table_name}_metas"
|
|
286
296
|
FOR EACH ROW
|
|
287
|
-
EXECUTE FUNCTION
|
|
297
|
+
EXECUTE FUNCTION public.set_updated_at();
|
|
288
298
|
|
|
289
299
|
COMMIT;
|
|
290
300
|
|
|
@@ -5,7 +5,7 @@ description: |
|
|
|
5
5
|
Dual authentication (API Key + Session), dynamic entities, metadata system.
|
|
6
6
|
Use this skill to create endpoints, validate APIs, or understand API patterns.
|
|
7
7
|
allowed-tools: Read, Glob, Grep, Bash(python:*)
|
|
8
|
-
version: 1.
|
|
8
|
+
version: 1.2.0
|
|
9
9
|
---
|
|
10
10
|
|
|
11
11
|
# Next.js API Development Skill
|
|
@@ -311,6 +311,11 @@ describe('Products API - CRUD Operations', {
|
|
|
311
311
|
|
|
312
312
|
### SQL Injection Prevention
|
|
313
313
|
|
|
314
|
+
> **RLS routing (beta.167):** `queryWithRLS(..., userId)` runs on the app pool (RLS evaluated).
|
|
315
|
+
> The bare `query()`/`queryOne()` family routes to the **service pool (bypass)** — never use it
|
|
316
|
+
> for a user-scoped read. See the **rls-enforcement** skill for the service-pool model and the
|
|
317
|
+
> LIST/READ fail-closed enforcement now applied in the generic handler.
|
|
318
|
+
|
|
314
319
|
```typescript
|
|
315
320
|
// ✅ CORRECT - Parameterized queries with queryWithRLS
|
|
316
321
|
const result = await queryWithRLS(
|
|
@@ -2,10 +2,12 @@
|
|
|
2
2
|
name: permissions-system
|
|
3
3
|
description: |
|
|
4
4
|
Three-layer permission system (Team Roles + Plans + Quotas) for this Next.js application.
|
|
5
|
-
Covers user roles, team roles, theme extensions, permission checking,
|
|
5
|
+
Covers user roles, team roles (stored as TEXT), theme role extensions, permission checking,
|
|
6
|
+
and LIST/READ fail-closed enforcement at the entity handler.
|
|
6
7
|
Use this skill when implementing or modifying access control features.
|
|
8
|
+
For the runtime RLS model (service pool, nextspark_app cutover) see rls-enforcement.
|
|
7
9
|
allowed-tools: Read, Glob, Grep
|
|
8
|
-
version: 1.
|
|
10
|
+
version: 1.1.0
|
|
9
11
|
---
|
|
10
12
|
|
|
11
13
|
# Permissions System Skill
|
|
@@ -553,6 +555,13 @@ const customers = await queryWithRLS(
|
|
|
553
555
|
)
|
|
554
556
|
```
|
|
555
557
|
|
|
558
|
+
**Enforcement at the entity handler (beta.167):** the generic handler now checks the session
|
|
559
|
+
permission on **LIST and READ** too (previously only create/update/delete) — a member without
|
|
560
|
+
`entity.list`/`entity.read` gets **403**. A permission check that **throws** returns
|
|
561
|
+
`500 PERMISSION_CHECK_FAILED` (**fail-closed**, never "allow"). Themes must declare `list`/`read`
|
|
562
|
+
per entity (`default`/`starter` already do). Note `team_role` is stored as **TEXT** (themes add
|
|
563
|
+
roles via config, not `ALTER TYPE`). For the runtime RLS model see the **rls-enforcement** skill.
|
|
564
|
+
|
|
556
565
|
## Build-Time Registry
|
|
557
566
|
|
|
558
567
|
```typescript
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: rls-enforcement
|
|
3
|
+
description: |
|
|
4
|
+
Operating Row-Level Security at RUNTIME: the two-pool service-context model
|
|
5
|
+
(DATABASE_SERVICE_URL bypass vs the app pool), the non-owner runtime role (nextspark_app)
|
|
6
|
+
and cutover, MIGRATE_DATABASE_URL, fail-closed LIST/READ enforcement, and ownershipFilter.
|
|
7
|
+
Use this WHENEVER a DB call mysteriously returns 0 rows (or too many) for some users, a
|
|
8
|
+
query/insert started failing after switching DATABASE_URL, a webhook / scheduled-action /
|
|
9
|
+
login path hits a Postgres permission error, you're choosing between the app pool and the
|
|
10
|
+
service (bypass) pool, or composing a transaction that mixes user-scoped and system writes —
|
|
11
|
+
even if the words "RLS" or "cutover" never appear. Also for forcing the service pool
|
|
12
|
+
(team/subscription bootstrap), doing/debugging the cutover, and extending core RLS policies
|
|
13
|
+
from a theme. For policy SQL see database-migrations; for queryWithRLS usage see
|
|
14
|
+
service-layer; for can_bypass_rls/superadmin see api-bypass-layers.
|
|
15
|
+
allowed-tools: Read, Glob, Grep
|
|
16
|
+
version: 1.1.0
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
# RLS Enforcement Layer Skill
|
|
20
|
+
|
|
21
|
+
**Canonical reference (read for SQL + cutover runbook):**
|
|
22
|
+
`packages/core/docs/10-backend/03-rls-policies.md` → "Enforcement Layer (beta.167+)".
|
|
23
|
+
This skill is the agent-facing companion: the operational rules and gotchas, not a
|
|
24
|
+
re-paste of the SQL.
|
|
25
|
+
|
|
26
|
+
## When to use this skill
|
|
27
|
+
|
|
28
|
+
- Adding/auditing a DB call and deciding **app pool (RLS) vs service pool (bypass)**.
|
|
29
|
+
- Anything that writes the **first** team membership/subscription (bootstrap).
|
|
30
|
+
- Doing or debugging the **RLS cutover** (`nextspark_app`, `DATABASE_SERVICE_URL`, `MIGRATE_DATABASE_URL`).
|
|
31
|
+
- A theme that needs to **override a core RLS policy** (e.g. widen a tier on `users`/`subscriptions`).
|
|
32
|
+
- Touching `generic-handler.ts` permission checks or `ownershipFilter`.
|
|
33
|
+
|
|
34
|
+
## 1. Two pools, routed by presence of `userId` (`src/lib/db.ts`)
|
|
35
|
+
|
|
36
|
+
| Helper | With `userId` | Without `userId` |
|
|
37
|
+
|---|---|---|
|
|
38
|
+
| `queryWithRLS` / `queryOneWithRLS` / `mutateWithRLS` / `getTransactionClient` | **app pool** (`SET LOCAL app.user_id`, RLS evaluated) | **service pool** (bypass) |
|
|
39
|
+
|
|
40
|
+
- **`query` / `queryOne` / `queryRows` ALWAYS go to the service pool (bypass).** They are
|
|
41
|
+
system operations by definition. **Never** use them for a user-scoped read that must be
|
|
42
|
+
RLS-filtered — use `queryWithRLS(sql, params, userId)`.
|
|
43
|
+
- **Force service even with a `userId`:** `mutateWithRLS(sql, params, userId, { service: true })`
|
|
44
|
+
or `getServiceTransactionClient()`.
|
|
45
|
+
- `DATABASE_SERVICE_URL` falls back to `DATABASE_URL` when unset → pre-cutover behavior is
|
|
46
|
+
unchanged (the app still connects as owner; nothing breaks).
|
|
47
|
+
- **A transaction is bound to ONE pool + ONE GUC at acquisition.** `getTransactionClient(userId)`
|
|
48
|
+
/ `getServiceTransactionClient()` acquire a single client up front; every `tx.query/.mutate`
|
|
49
|
+
runs on that pool. You **cannot** do a user-scoped RLS read and a service-pool bootstrap write
|
|
50
|
+
in the *same* `tx`. If a flow needs both, run the bootstrap as a service transaction and do
|
|
51
|
+
the RLS reads/writes as separate `queryWithRLS(..., userId)` calls (or sequence them: commit
|
|
52
|
+
the membership first, then the user-scoped write can satisfy RLS).
|
|
53
|
+
- **Atomicity:** `mutateWithRLS` wraps **each call** in its own BEGIN/COMMIT — N calls = N
|
|
54
|
+
transactions. For "insert order + 3 items atomically", use `getTransactionClient(userId)`
|
|
55
|
+
(one GUC, one commit/rollback), not N `mutateWithRLS` calls.
|
|
56
|
+
|
|
57
|
+
## 2. When you MUST force the service pool
|
|
58
|
+
|
|
59
|
+
Bootstrap writes that create the **first** membership/subscription of a team can't pass the
|
|
60
|
+
membership-based RLS insert policies under the new member's own GUC (they aren't a member
|
|
61
|
+
yet). These run on the **service pool**, authorized at the API/action layer (not RLS):
|
|
62
|
+
|
|
63
|
+
- `TeamService.create`, `teams/actions.ts:createTeam`, `addUserToGlobalTeam`,
|
|
64
|
+
`TeamMemberService.add` (via `{ service: true }`).
|
|
65
|
+
|
|
66
|
+
If you add a new "first-membership" writer, route it the same way or it will break under RLS.
|
|
67
|
+
|
|
68
|
+
**Other legitimate service-pool actors** (no request user → service pool by routing, since they
|
|
69
|
+
call with no `userId`): Better Auth login/verification, the **scheduled-actions processor**,
|
|
70
|
+
**payment-provider webhooks**, the superadmin/developer bypass check. Inside such a system path,
|
|
71
|
+
any *user-scoped* sub-operation must re-introduce the resolved userId via
|
|
72
|
+
`queryWithRLS(..., userId)` so RLS applies for that slice.
|
|
73
|
+
|
|
74
|
+
> ⚠️ **The inverse is a security hole.** Force the service pool **only** for work that
|
|
75
|
+
> *structurally* cannot satisfy RLS (first-membership/subscription, pre-login auth, webhooks/
|
|
76
|
+
> scheduler with no user). **Never** add `{ service: true }` to a normal user-scoped read/write
|
|
77
|
+
> to "fix" a 0-rows or permission-denied bug — that silently disables tenant isolation and leaks
|
|
78
|
+
> cross-tenant data. A 0-rows bug means a missing policy / missing `userId` / missing
|
|
79
|
+
> `entity.list` permission — fix *that*, don't bypass RLS.
|
|
80
|
+
|
|
81
|
+
## 3. Runtime role + cutover gotcha
|
|
82
|
+
|
|
83
|
+
- Migration `022_rls_runtime_roles.sql` creates `nextspark_app` (non-owner, member of
|
|
84
|
+
`authenticated`, **no** `BYPASSRLS`) + grants + `anon` lockdown.
|
|
85
|
+
- **Migrations run as the OWNER** via `MIGRATE_DATABASE_URL` (falls back to `DATABASE_URL`);
|
|
86
|
+
the runtime runs as `nextspark_app`.
|
|
87
|
+
- **GOTCHA:** post-cutover `DATABASE_URL` is a **non-owner** role — any op that assumed owner
|
|
88
|
+
(raw DDL, seeds, `query()` expecting full access) behaves differently. Keep owner work on
|
|
89
|
+
the migrate/service connection.
|
|
90
|
+
- Cutover is **opt-in**: leave `DATABASE_SERVICE_URL` unset and nothing changes.
|
|
91
|
+
|
|
92
|
+
## 4. Fail-closed permission enforcement (`generic-handler.ts`)
|
|
93
|
+
|
|
94
|
+
- LIST and READ now check the session entity permission (was create/update/delete only).
|
|
95
|
+
A member without `entity.list`/`entity.read` → **403**. Admin-bypass and api-key (scope)
|
|
96
|
+
paths are skipped.
|
|
97
|
+
- A **thrown error** in the permission check returns **`500 PERMISSION_CHECK_FAILED`**
|
|
98
|
+
(fail-closed) — never "allow". Themes must declare `list`/`read` per entity
|
|
99
|
+
(`default`/`starter` already do).
|
|
100
|
+
- **api-key vs session — two different layers, two different behaviors:**
|
|
101
|
+
- The **entity-permission** check is session-only (api-key uses scopes), so it's *skipped* for api-key.
|
|
102
|
+
- But **DB RLS still applies to api-key** requests — api-key auth carries a `userId`, so reads
|
|
103
|
+
route to the app pool and policies evaluate (team isolation holds).
|
|
104
|
+
- **`ownershipFilter` (role-based row scoping) is OFF for api-key and admin-bypass**
|
|
105
|
+
(`resolveOwnershipFilter` returns `{applies:false}` for those). So an api-key caller is *not*
|
|
106
|
+
narrowed to "their own" rows by `ownershipFilter`; they see everything their team-RLS policy
|
|
107
|
+
allows. To restrict api-key callers, scope the key narrowly or enforce it in a DB RLS policy
|
|
108
|
+
(which fires for any app-pool request), not via `ownershipFilter`.
|
|
109
|
+
|
|
110
|
+
## 5. `ownershipFilter` (entity `access` config, `entities/types.ts`)
|
|
111
|
+
|
|
112
|
+
- `linkedBy` is **optional** → omit for **direct-field** ownership (entity row carries the
|
|
113
|
+
owner id; filtered against the current user).
|
|
114
|
+
- `linkedBy.softDelete` makes the `deletedAt IS NULL` clause conditional — set `false` for
|
|
115
|
+
tables without a `deletedAt` column (avoids a 42703 undefined-column error).
|
|
116
|
+
|
|
117
|
+
## Debugging & validating
|
|
118
|
+
|
|
119
|
+
**"Returned 0 rows after the cutover" (worked before, member gets `[]`, superadmin still sees all).**
|
|
120
|
+
RLS is now live. Triage in order:
|
|
121
|
+
1. **403 vs `[]`?** A `403` = fail-closed permission (member lacks `entity.list`/`read`, or theme
|
|
122
|
+
didn't declare it). An empty `[]` (200) = permission passed but RLS filtered all rows. They
|
|
123
|
+
are different bugs.
|
|
124
|
+
2. **Is a `userId` actually passed?** A missing `userId` routes to the service pool → would
|
|
125
|
+
*over*-return, not under-return. So `[]` means a `userId` *is* present and RLS is filtering.
|
|
126
|
+
3. **Is `DATABASE_SERVICE_URL` set to a real bypass/owner role?** If unset, the "service" pool is
|
|
127
|
+
*also* `nextspark_app` (no bypass) → even system reads get filtered. Most common cutover
|
|
128
|
+
mistake. Set it.
|
|
129
|
+
4. **Does a policy exist?** RLS-enabled **with no policy** blocks ALL rows. Confirm a policy
|
|
130
|
+
`TO authenticated` exists and `nextspark_app` is a member of `authenticated` (it is, per 022).
|
|
131
|
+
5. **Reproduce as the runtime role** (below). If empty there too, it's the policy / membership /
|
|
132
|
+
`ownershipFilter`, not the app wiring.
|
|
133
|
+
|
|
134
|
+
**Validate/test RLS as the real runtime role (not the owner — owner bypasses RLS, proving nothing).**
|
|
135
|
+
`nextspark_app` is NOLOGIN, but `022` runs `GRANT nextspark_app TO current_user`, so the
|
|
136
|
+
test/migrate user can assume it:
|
|
137
|
+
```sql
|
|
138
|
+
BEGIN;
|
|
139
|
+
SET LOCAL ROLE nextspark_app;
|
|
140
|
+
SET LOCAL app.user_id = '<teamA_member_id>';
|
|
141
|
+
SELECT count(*) FROM "invoices" WHERE "teamId" = '<teamB_id>'; -- expect 0 (cross-tenant denied)
|
|
142
|
+
COMMIT;
|
|
143
|
+
```
|
|
144
|
+
Always include a positive control (same query as a team-B member → > 0), or a `0` is meaningless.
|
|
145
|
+
Seed fixtures via the service pool / owner (first-membership inserts can't satisfy RLS).
|
|
146
|
+
|
|
147
|
+
## Non-obvious rules (do not relearn the hard way)
|
|
148
|
+
|
|
149
|
+
- **RLS primitives live in migration `001`** (`can_bypass_rls`, `auth_user_can_see_user`,
|
|
150
|
+
`is_superadmin`) — because the hardened policies in `002` use them, and `002` runs before
|
|
151
|
+
the tables/functions a later migration would define. A policy with a forward dependency on
|
|
152
|
+
a later table fails at `CREATE POLICY` time; wrap such logic in a `SECURITY DEFINER` helper
|
|
153
|
+
defined in `001` (table refs resolve at call time). Cite `001`, not the stale `002:102`
|
|
154
|
+
comment that still says "010".
|
|
155
|
+
- **`team_role` is `TEXT`** (not an ENUM). Themes add roles via config
|
|
156
|
+
(`availableTeamRoles` + `permissions.config.ts`) — **never** `ALTER TYPE team_role`.
|
|
157
|
+
- **Theme override of a core RLS policy** runs in **phase 2** (after core). It must `DROP`
|
|
158
|
+
the core policy by name and recreate its own — dropping **both** the legacy AND the current
|
|
159
|
+
core policy names, or the policies **coexist** (RLS combines them with OR, loosening the
|
|
160
|
+
restriction). See campus `9035/9036` for the pattern.
|
|
161
|
+
- **Don't break the metadata self-read policy** (`003_user_metas`): login reads the user's own
|
|
162
|
+
flags under RLS via `MetaService.getEntityMetas(..., userId)`.
|
|
163
|
+
- **`anon` is not the app's public-read path.** Public reads enter with `userId=null` →
|
|
164
|
+
service pool (bypass), filtered in SQL by the handler. `TO anon` policies + the `022`
|
|
165
|
+
`anon` revoke are defense for the **Supabase Data API / PostgREST**, not app traffic.
|
|
166
|
+
|
|
167
|
+
## Anti-patterns
|
|
168
|
+
|
|
169
|
+
- ❌ Using bare `query()`/`queryOne()` for a user-scoped read that must be RLS-filtered (it
|
|
170
|
+
bypasses RLS via the service pool).
|
|
171
|
+
- ❌ Adding a hardened policy that references `team_members`/`can_bypass_rls` to an **early**
|
|
172
|
+
migration (e.g. 002) without a helper in 001 → forward-dependency failure.
|
|
173
|
+
- ❌ `ALTER TYPE team_role ADD VALUE ...` in a theme → the type no longer exists.
|
|
174
|
+
- ❌ A theme override that drops only the legacy core policy names → coexisting policies.
|
|
175
|
+
- ❌ Returning "allow" when a permission check throws → must be fail-closed.
|
|
176
|
+
|
|
177
|
+
## Checklist (the cross-cutting ones the body's rules don't already make obvious)
|
|
178
|
+
|
|
179
|
+
- [ ] DB call routing chosen deliberately: user-scoped → `queryWithRLS(..., userId)`; system → `query*`/no-userId; bootstrap-only → `{ service: true }`. **Not** `{ service: true }` to silence a 0-rows bug.
|
|
180
|
+
- [ ] No transaction tries to mix a user-scoped (app-pool) op and a service-pool (bootstrap) op on the same `tx`.
|
|
181
|
+
- [ ] Multi-row atomic write uses `getTransactionClient(userId)`, not N `mutateWithRLS` calls.
|
|
182
|
+
- [ ] New core-table policy: primitive deps already defined in `001` before the migration that uses them.
|
|
183
|
+
- [ ] New entity exposes `list`/`read` permissions (else 403 under enforcement); api-key callers restricted by scope or a DB policy (not `ownershipFilter`).
|
|
184
|
+
- [ ] Theme RLS override drops legacy AND current core policy names.
|
|
185
|
+
|
|
186
|
+
## Related skills
|
|
187
|
+
|
|
188
|
+
- `database-migrations` — RLS policy SQL patterns, migration structure.
|
|
189
|
+
- `service-layer` — `queryWithRLS`/`mutateWithRLS` usage and service classes.
|
|
190
|
+
- `api-bypass-layers` — `can_bypass_rls()`, superadmin/developer bypass.
|
|
191
|
+
- `permissions-system` — team roles, tiers, permission checks.
|
|
192
|
+
- `scheduled-actions` — background/webhook handlers (service-pool actors with no request user).
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# rls-enforcement — eval results
|
|
2
|
+
|
|
3
|
+
Methodology: anthropics-skill-creator (autonomous loop; subagents with-skill vs baseline,
|
|
4
|
+
graded against the per-case assertions in evals.json).
|
|
5
|
+
|
|
6
|
+
## Iteration 1 — core cases (evals.json #0-4)
|
|
7
|
+
With-skill completed runs: 4/4 PASS (3/3 assertions each). Baseline (no skill, repo access)
|
|
8
|
+
also strong (the code + sibling skills are well-documented), so easy cases SATURATE — no
|
|
9
|
+
headroom. Conclusion: measure the skill on HARDER composite/debugging scenarios.
|
|
10
|
+
|
|
11
|
+
## Iteration 1b — harder gap cases (current skill, pre-improvement)
|
|
12
|
+
gap1 tx-pool-binding: not covered by skill. gap2 0-rows: PASS (agent derived). gap3 api-key:
|
|
13
|
+
PARTIAL 2/3 — agent explicitly hedged ("skill doesn't specify api-key routing"). gap4
|
|
14
|
+
don't-over-service: PASS. gap8 test-as-nextspark_app: PASS (derived).
|
|
15
|
+
|
|
16
|
+
## Improvements applied (deltas A-F + description)
|
|
17
|
+
A tx bound to one pool+GUC / atomicity; B api-key vs session asymmetry; C "0 rows after cutover"
|
|
18
|
+
triage + test-as-nextspark_app mechanics; D inverse footgun (don't over-service); E lean checklist;
|
|
19
|
+
F webhooks/scheduler actors + related skill; sharper symptom-based description.
|
|
20
|
+
|
|
21
|
+
## Iteration 2 — same gap cases (improved skill)
|
|
22
|
+
5/5 PASS (15/15 assertions). gap1 now a precise PASS; gap3 PASS 3/3 with NO hedge (the
|
|
23
|
+
skill-attributable win). Pass-rate on completed runs: ~87.5% -> 100%. Agents now cite the
|
|
24
|
+
skill's encoded sections instead of deriving (lower reasoning burden / higher reliability).
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
{
|
|
2
|
+
"skill_name": "rls-enforcement",
|
|
3
|
+
"notes": "Autonomous eval. Each case is a realistic NextSpark developer task whose CORRECT answer depends on knowledge in the rls-enforcement skill. We compare with-skill vs no-skill (baseline), grade against objective assertions, then improve and re-run to measure pass-rate delta.",
|
|
4
|
+
"evals": [
|
|
5
|
+
{
|
|
6
|
+
"id": 0,
|
|
7
|
+
"name": "bootstrap-service-pool",
|
|
8
|
+
"prompt": "In @nextsparkjs/core I'm adding a server action `createOrganization(userId, name)` that, in one DB transaction, inserts a new row in `teams` (ownerId = userId), inserts the creator's `team_members` row with role 'owner', and inserts a default `subscriptions` row for the team. The app will run with RLS enabled (DATABASE_URL connects as the non-owner role). Write the data-access code (which db.ts helper do you use and why).",
|
|
9
|
+
"assertions": [
|
|
10
|
+
{"text": "Uses the SERVICE pool (getServiceTransactionClient or { service: true } or a non-userId service path), NOT getTransactionClient(userId)/app pool"},
|
|
11
|
+
{"text": "Explains the reason: it creates the FIRST team_members/subscription so it cannot satisfy the membership-based RLS insert policy under the user's own GUC"},
|
|
12
|
+
{"text": "Notes that authorization is enforced at the app/action layer, not by RLS, for this bootstrap"}
|
|
13
|
+
]
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
"id": 1,
|
|
17
|
+
"name": "bare-query-bypass",
|
|
18
|
+
"prompt": "A teammate wrote this helper to list the current user's own invoices in @nextsparkjs/core:\n\nimport { query } from '@/core/lib/db'\nexport async function listMyInvoices(userId: string) {\n return (await query('SELECT * FROM \"invoices\" WHERE \"userId\" = $1', [userId])).rows\n}\n\nUnder the RLS runtime, is this correct? If not, fix it and explain.",
|
|
19
|
+
"assertions": [
|
|
20
|
+
{"text": "Identifies that the bare query()/queryOne()/queryRows() family routes to the SERVICE pool and BYPASSES RLS"},
|
|
21
|
+
{"text": "Recommends queryWithRLS(sql, params, userId) so the query runs on the app pool with the GUC / RLS evaluated"},
|
|
22
|
+
{"text": "Frames it as a real risk (RLS not enforced) rather than a style nit"}
|
|
23
|
+
]
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
"id": 2,
|
|
27
|
+
"name": "team-role-text-extension",
|
|
28
|
+
"prompt": "Our theme needs a new team role called 'reviewer'. How do I make it real so it can be stored in team_members.role and recognized by permission checks? Do I need a migration?",
|
|
29
|
+
"assertions": [
|
|
30
|
+
{"text": "States that team_role is TEXT (not a Postgres ENUM), so no ALTER TYPE / no DB migration is needed to add the value"},
|
|
31
|
+
{"text": "Says to declare the role via config (availableTeamRoles in app.config + permissions.config.ts)"},
|
|
32
|
+
{"text": "Explicitly warns against ALTER TYPE team_role ADD VALUE"}
|
|
33
|
+
]
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
"id": 3,
|
|
37
|
+
"name": "policy-primitive-ordering",
|
|
38
|
+
"prompt": "I'm hardening the RLS policy on the `account` table by editing migration 002_auth_tables.sql to use `public.can_bypass_rls()`. On a fresh db:reset it fails with: ERROR: function public.can_bypass_rls() does not exist. Why, and how do I fix it without an incremental migration?",
|
|
39
|
+
"assertions": [
|
|
40
|
+
{"text": "Explains the cause: can_bypass_rls (and friends) must exist BEFORE migration 002 runs; defining them later (e.g. 010) is a forward dependency that fails at CREATE POLICY time"},
|
|
41
|
+
{"text": "Fix: define the primitive in migration 001 (the foundational functions) so 002 can use it"},
|
|
42
|
+
{"text": "Notes that a SECURITY DEFINER function's table refs resolve at call time, so it can be defined before those tables exist"}
|
|
43
|
+
]
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
"id": 4,
|
|
47
|
+
"name": "theme-override-coexistence",
|
|
48
|
+
"prompt": "My theme ships a phase-2 migration that overrides the core `subscriptions` SELECT policy with a stricter campus-specific one. But after db:reset, a plain team member can still read the team's subscriptions as if the core policy were still active. What's wrong and how do I fix the override?",
|
|
49
|
+
"assertions": [
|
|
50
|
+
{"text": "Explains that RLS combines multiple permissive policies with OR, so the core policy and the theme policy COEXIST (the looser core one still grants access)"},
|
|
51
|
+
{"text": "Fix: the theme override must DROP the core policy by name — and drop BOTH the legacy AND the current (beta.167) core policy names — before creating its own"},
|
|
52
|
+
{"text": "Mentions the theme override runs in phase 2 (after core) which is why it must drop the core's policies"}
|
|
53
|
+
]
|
|
54
|
+
}
|
|
55
|
+
]
|
|
56
|
+
}
|
|
@@ -2,10 +2,11 @@
|
|
|
2
2
|
name: service-layer
|
|
3
3
|
description: |
|
|
4
4
|
Service layer patterns for this Next.js application.
|
|
5
|
-
Covers static class pattern, RLS
|
|
5
|
+
Covers static class pattern, passing the RLS userId context (queryWithRLS), standard method signatures, and BaseEntityService.
|
|
6
6
|
Use this skill when implementing business logic or data access services.
|
|
7
|
+
For the runtime RLS model (service pool vs app pool, when to force service) see rls-enforcement.
|
|
7
8
|
allowed-tools: Read, Glob, Grep
|
|
8
|
-
version: 1.
|
|
9
|
+
version: 1.1.0
|
|
9
10
|
---
|
|
10
11
|
|
|
11
12
|
# Service Layer Skill
|
|
@@ -203,6 +204,15 @@ export async function mutateWithRLS<T>(
|
|
|
203
204
|
- RLS policies use this to filter/restrict data access
|
|
204
205
|
- Always pass `userId` parameter for proper isolation
|
|
205
206
|
|
|
207
|
+
**Dual-pool routing (beta.167) — see the `rls-enforcement` skill:**
|
|
208
|
+
- `*WithRLS` **with** a `userId` → app pool (RLS evaluated). **Without** a `userId` → **service
|
|
209
|
+
pool (bypass)**. The bare `query`/`queryOne`/`queryRows` family is **always** service/bypass.
|
|
210
|
+
- **System/bootstrap writes** that create the first team membership/subscription must force the
|
|
211
|
+
service pool: `mutateWithRLS(sql, params, userId, { service: true })` or
|
|
212
|
+
`getServiceTransactionClient()` (they can't satisfy membership-based insert policies under the
|
|
213
|
+
new member's own GUC). This is why `TeamService.create` / `TeamMemberService.add` use the
|
|
214
|
+
service path.
|
|
215
|
+
|
|
206
216
|
### Service Implementation with RLS
|
|
207
217
|
|
|
208
218
|
```typescript
|
|
@@ -585,3 +595,4 @@ Before finalizing service implementation:
|
|
|
585
595
|
- `entity-api` - API endpoint patterns
|
|
586
596
|
- `permissions-system` - Permission checking
|
|
587
597
|
- `database-migrations` - Migration patterns
|
|
598
|
+
- `rls-enforcement` - Runtime RLS: service pool vs app pool, when to force service, cutover
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nextsparkjs/ai-workflow",
|
|
3
|
-
"version": "0.1.0-beta.
|
|
3
|
+
"version": "0.1.0-beta.168",
|
|
4
4
|
"description": "AI workflow templates for NextSpark - Claude Code agents, commands, skills, and multi-editor support",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "NextSpark <hello@nextspark.dev>",
|
package/LICENSE
DELETED
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
MIT License
|
|
2
|
-
|
|
3
|
-
Copyright (c) 2025 NextSpark
|
|
4
|
-
|
|
5
|
-
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
-
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
-
in the Software without restriction, including without limitation the rights
|
|
8
|
-
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
-
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
-
furnished to do so, subject to the following conditions:
|
|
11
|
-
|
|
12
|
-
The above copyright notice and this permission notice shall be included in all
|
|
13
|
-
copies or substantial portions of the Software.
|
|
14
|
-
|
|
15
|
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
-
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
-
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
-
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
-
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
-
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
-
SOFTWARE.
|