create-monolayer-app 0.4.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/template/.agents/skills/monolayer-application-permissions/SKILL.md +51 -0
- package/template/.agents/skills/monolayer-application-permissions/agents/openai.yaml +4 -0
- package/template/.agents/skills/monolayer-application-permissions/references/development-and-verification.md +51 -0
- package/template/.agents/skills/monolayer-application-permissions/references/interface-examples.md +60 -0
- package/template/.agents/skills/monolayer-application-permissions/references/server-examples.md +81 -0
- package/template/.claude/CLAUDE.md +11 -0
- package/template/AGENTS.md +11 -0
- package/template/app/root.tsx +4 -2
- package/template/lib/db/user-context.server.ts +35 -0
package/package.json
CHANGED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: monolayer-application-permissions
|
|
3
|
+
description: >
|
|
4
|
+
Apply viewer, editor, and owner permissions in monolayer React Router apps.
|
|
5
|
+
Use for application content access, editing interactions, or changes to
|
|
6
|
+
authentication and user context, including demos with sample or temporary data.
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# Monolayer Application Permissions
|
|
10
|
+
|
|
11
|
+
Build application features using the existing user context. The application is the sharing boundary: authorized users see the same content, subject to explicit product rules.
|
|
12
|
+
|
|
13
|
+
Application content interactions follow these roles even in demos backed by sample or temporary data. Keep the requested storage and integration behavior.
|
|
14
|
+
|
|
15
|
+
## Access model
|
|
16
|
+
|
|
17
|
+
| Operation | `viewer` | `editor` | `owner` |
|
|
18
|
+
| --- | --- | --- | --- |
|
|
19
|
+
| Read, search, and navigate shared content | Allow | Allow | Allow |
|
|
20
|
+
| Download content the user can read | Allow by default | Allow | Allow |
|
|
21
|
+
| Create, edit, delete, upload, or import shared content | Deny | Allow | Allow |
|
|
22
|
+
| Manage platform invitations or access grants | Platform sharing policy | Platform sharing policy | Platform sharing policy |
|
|
23
|
+
|
|
24
|
+
Scope shared records to the trusted application, not the current user's email. Email is display or attribution data, not an immutable person ID or application identifier. Keep personal preferences separate where the product requires them.
|
|
25
|
+
|
|
26
|
+
Invitations, grants, application deletion, and ownership transfer use the platform's separate authorization. `canWrite` grants no administrative authority; use the existing platform sharing integration instead of local grant rows. Application roles do not supply live collaboration, conflict resolution, or per-document permissions.
|
|
27
|
+
|
|
28
|
+
## Existing context
|
|
29
|
+
|
|
30
|
+
[lib/user-context.server.ts](../../../lib/user-context.server.ts) exports `userContext`, `userSchema`, and `userContextMiddleware`; the context contains `email` and `role`. [app/root.tsx](../../../app/root.tsx) registers the middleware. Reuse existing context and access helpers. Inspect registration when adding a protected route or investigating missing context.
|
|
31
|
+
|
|
32
|
+
Production context depends on the platform authenticating and authorizing requests before they reach the application. Local schema validation does not establish header authenticity. See the development reference when changing that integration.
|
|
33
|
+
|
|
34
|
+
## Authorization constraints
|
|
35
|
+
|
|
36
|
+
- Preserve existing authentication, middleware registration, and development behavior unless an authentication change is part of the user's request. A demo or sample-only request does not imply such a change.
|
|
37
|
+
- Read authority from `context.get(userContext)` on the server. Do not reparse identity headers in features or accept authority from client input. Pass request context or a trusted actor explicitly to server helpers; never store the current user in a module variable.
|
|
38
|
+
- Authorize each write using the current request's role before database changes, file changes, external writes, or issuing write capabilities. A viewer's write returns `403`. Missing context is a configuration fault, never an owner fallback.
|
|
39
|
+
- Keep reads free of shared-content mutations, including default records created on page visits.
|
|
40
|
+
- Send only the identity fields and capabilities the interface needs. UI controls reflect permissions; every server write still checks permission when earlier loader data says editing is allowed.
|
|
41
|
+
- Jobs, webhooks, sockets, and React Server Functions need their own trusted authorization. Request context does not persist into later work, and a saved email or role does not establish continuing authority.
|
|
42
|
+
|
|
43
|
+
## Task-specific references
|
|
44
|
+
|
|
45
|
+
Read the relevant sections when needed:
|
|
46
|
+
|
|
47
|
+
- [Server examples](references/server-examples.md): write guards, application scope, protected downloads, and upload capabilities.
|
|
48
|
+
- [Interface examples](references/interface-examples.md): capability data, read-only controls, sample-only demos, and denied saves.
|
|
49
|
+
- [Development and verification](references/development-and-verification.md): local identity, isolated role fixtures, platform trust prerequisites, and checks selected by the changed behavior.
|
|
50
|
+
|
|
51
|
+
Complete the requested feature and select checks for the affected behavior. For server writes, verify viewer denial without side effects and writer success; check application scope when data access changes. For local demos, check role-dependent interactions and the requested reset behavior. Report relevant verification gaps; isolated context fixtures do not verify deployed authentication.
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# Development and verification
|
|
2
|
+
|
|
3
|
+
Use the sections needed for local role checks, authorization tests, or changes to the authentication integration.
|
|
4
|
+
|
|
5
|
+
## Local identity
|
|
6
|
+
|
|
7
|
+
[lib/user-context.server.ts](../../../../lib/user-context.server.ts) supplies `{ email: "developer@example.com", role: "owner" }` when `import.meta.env.DEV` is true. That branch ignores incoming identity headers; there is no runtime role selector in the current middleware.
|
|
8
|
+
|
|
9
|
+
Use isolated contexts to exercise other roles without changing production authentication. If browser verification needs a role fixture, use an existing test harness or add a test-only mechanism within the requested scope. A deployed stage named `dev` is not grounds for enabling mock users.
|
|
10
|
+
|
|
11
|
+
## Isolated request contexts
|
|
12
|
+
|
|
13
|
+
This helper belongs only in test code. It represents context after authentication; it does not verify middleware registration or upstream header trust.
|
|
14
|
+
|
|
15
|
+
```ts
|
|
16
|
+
// tests/application-context.ts
|
|
17
|
+
import { RouterContextProvider } from "react-router";
|
|
18
|
+
import type { z } from "zod";
|
|
19
|
+
import { userContext, userSchema } from "../lib/user-context.server";
|
|
20
|
+
|
|
21
|
+
export function applicationContextForTest(user: z.input<typeof userSchema>) {
|
|
22
|
+
const context = new RouterContextProvider();
|
|
23
|
+
context.set(userContext, userSchema.parse(user));
|
|
24
|
+
return context;
|
|
25
|
+
}
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Create a context for each simulated request. Use `{ email: "reader@example.com", role: "viewer" }` for a viewer and different emails when checking shared-content behavior. To exercise missing-context handling, leave `userContext` unset.
|
|
29
|
+
|
|
30
|
+
## Select checks by the changed behavior
|
|
31
|
+
|
|
32
|
+
Use applicable cases; this table is not a checklist for every edit.
|
|
33
|
+
|
|
34
|
+
| Changed behavior | Evidence to check |
|
|
35
|
+
| --- | --- |
|
|
36
|
+
| Shared reads | Different authorized users, including viewers, see the same application content. Reads do not create shared records. |
|
|
37
|
+
| Mutations | Direct viewer requests return `403` with no database, file, or external writes. Valid editor and owner requests succeed. Client identity fields cannot override the trusted actor or attribution. |
|
|
38
|
+
| Application scope | Reads and writes cannot expose or change another application's records. For shared storage, check the actual query or adapter boundary. |
|
|
39
|
+
| Input validation | Invalid content fails before a write occurs. |
|
|
40
|
+
| Downloads | Viewers receive only permitted application content. |
|
|
41
|
+
| Uploads | Viewer requests cannot obtain upload capabilities or publish files through a completion operation. |
|
|
42
|
+
| Access UI | Read-only controls preserve permitted navigation and downloads. A write with viewer context is denied even if the interface previously showed editor access; the interface explains the failure. |
|
|
43
|
+
| Authentication wiring | Missing or invalid identity follows the existing `401` behavior. Missing required context never invents an owner or continues privileged work. |
|
|
44
|
+
|
|
45
|
+
Use the project's test runner and real service boundaries. Check persisted data or side-effect calls after denials; disabled-button assertions do not prove authorization. Select existing type or build checks when affected imports, types, or server/browser boundaries warrant them. A presentation-only change does not require investigating deployed authentication.
|
|
46
|
+
|
|
47
|
+
## Platform trust prerequisites
|
|
48
|
+
|
|
49
|
+
In production, the middleware validates the shape of `x-user-email` and `x-user-authorization` and sets context. It does not verify their origin. The platform's protected ingress must authenticate the caller, authorize access to the application, replace or reject client-supplied identity headers, and prevent bypass to an unprotected application server.
|
|
50
|
+
|
|
51
|
+
This repository does not include the upstream verifier or its deployment contract. When changing deployment or authentication integration, consult the platform's maintained contract for signature verification, header and cookie handling, and permission propagation. Do not infer those guarantees from local schema validation or promise immediate revocation. Report unverified deployment boundaries when they are part of the requested task; isolated role fixtures do not establish them.
|
package/template/.agents/skills/monolayer-application-permissions/references/interface-examples.md
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# Interface examples
|
|
2
|
+
|
|
3
|
+
Use this reference when adding or changing access behavior in the interface. Reuse capabilities already exposed by the application.
|
|
4
|
+
|
|
5
|
+
## Expose the capability the interface needs
|
|
6
|
+
|
|
7
|
+
For application-wide controls, extend the root loader while preserving existing data and middleware exports. A page-specific control can use its own route loader instead.
|
|
8
|
+
|
|
9
|
+
```ts
|
|
10
|
+
// app/root.tsx — loader addition
|
|
11
|
+
import { userContext } from "../lib/user-context.server";
|
|
12
|
+
import type { Route } from "./+types/root";
|
|
13
|
+
|
|
14
|
+
export function loader({ context }: Route.LoaderArgs) {
|
|
15
|
+
const user = context.get(userContext);
|
|
16
|
+
|
|
17
|
+
return { canWrite: user.role === "owner" || user.role === "editor" };
|
|
18
|
+
}
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Read the capability from loader data, using `useRouteLoaderData` with the actual route ID for descendants when needed. Server request context is separate from browser React context; keep `.server` runtime imports out of browser components.
|
|
22
|
+
|
|
23
|
+
Return email or role only when the interface needs them. Never serialize authentication cookies, transport assertions, or provider tokens. Keep identity-dependent responses out of shared public caches.
|
|
24
|
+
|
|
25
|
+
## Reflect access in existing controls
|
|
26
|
+
|
|
27
|
+
Pass `canWrite` from loader data into existing controls. This example belongs inside the application's existing form; its server operation must authorize the write independently.
|
|
28
|
+
|
|
29
|
+
```tsx
|
|
30
|
+
type SaveControlProps = {
|
|
31
|
+
canWrite: boolean;
|
|
32
|
+
saving: boolean;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export function SaveControl({ canWrite, saving }: SaveControlProps) {
|
|
36
|
+
if (!canWrite) {
|
|
37
|
+
return <p>You have read access. An editor or owner can make changes.</p>;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return (
|
|
41
|
+
<button type="submit" disabled={saving}>
|
|
42
|
+
{saving ? "Saving…" : "Save"}
|
|
43
|
+
</button>
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Apply the same capability to editable fields, drag handles, keyboard shortcuts, and autosave. Use read-only fields when viewers should still select their contents. Preserve permitted navigation, search, and downloads; lack of write access does not hide shared content.
|
|
49
|
+
|
|
50
|
+
## Sample and temporary data
|
|
51
|
+
|
|
52
|
+
Use the same server-derived capability for simulated content changes. Completing a sample task is an editing interaction and requires editor or owner access. Filtering the list is a viewing interaction available to viewers; personal display preferences also remain available. Apply the capability to editing controls and their event handlers.
|
|
53
|
+
|
|
54
|
+
Keep sample data and completion state in memory when refresh must reset them. Do not add persistence or external services solely to apply permissions. Preserve the existing user middleware and derive capabilities from its context. Local controls govern demo behavior; any server write still needs authorization using the current request.
|
|
55
|
+
|
|
56
|
+
## Handle permission changes and denied saves
|
|
57
|
+
|
|
58
|
+
An interface can show stale capabilities. Skip autosave when `canWrite` is false, and handle server denials even when it is true. Do not show an optimistic update as saved before server confirmation.
|
|
59
|
+
|
|
60
|
+
A thrown `403` can replace the route with its error boundary and discard a draft. When the product requires draft recovery, use the application's structured action-error pattern to preserve the editor and unsaved content while returning status `403`. Handle `401` through the existing authentication flow; a denied write is not automatically a reason to redirect to login.
|
package/template/.agents/skills/monolayer-application-permissions/references/server-examples.md
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# Server examples
|
|
2
|
+
|
|
3
|
+
Use the sections relevant to the changed server boundary. Reuse existing helpers. The helper below is illustrative; `lib/user-context.server.ts` already exists. Use the generated `Route` types for each route module.
|
|
4
|
+
|
|
5
|
+
## Require write permission
|
|
6
|
+
|
|
7
|
+
```ts
|
|
8
|
+
// lib/application-access.server.ts
|
|
9
|
+
import type { RouterContextProvider } from "react-router";
|
|
10
|
+
import { userContext } from "./user-context.server";
|
|
11
|
+
|
|
12
|
+
export function requireApplicationWriter(
|
|
13
|
+
context: Readonly<RouterContextProvider>
|
|
14
|
+
) {
|
|
15
|
+
const user = context.get(userContext);
|
|
16
|
+
|
|
17
|
+
if (user.role !== "editor" && user.role !== "owner") {
|
|
18
|
+
throw new Response("Write access required", { status: 403 });
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return user;
|
|
22
|
+
}
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Use the project's error mapping if it differs from thrown HTTP responses. Services called through multiple handlers need equivalent authorization at each entry point; a protected page loader does not authorize later mutations.
|
|
26
|
+
|
|
27
|
+
## Protect a mutation
|
|
28
|
+
|
|
29
|
+
`updateSharedContent` below represents the feature's application operation, not an existing module or SDK API. It validates input, restricts the update to the trusted application, and derives author attribution from `actor`. Preserve existing transaction and revision checks.
|
|
30
|
+
|
|
31
|
+
```ts
|
|
32
|
+
// app/routes/home.tsx — action addition
|
|
33
|
+
import { requireApplicationWriter } from "../../lib/application-access.server";
|
|
34
|
+
import { updateSharedContent } from "../../lib/shared-content.server";
|
|
35
|
+
import type { Route } from "./+types/home";
|
|
36
|
+
|
|
37
|
+
export async function action({ request, context }: Route.ActionArgs) {
|
|
38
|
+
const actor = requireApplicationWriter(context);
|
|
39
|
+
const input = await request.formData();
|
|
40
|
+
|
|
41
|
+
return await updateSharedContent({ input, actor });
|
|
42
|
+
}
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Authorize each intent when an action handles multiple operations. Client-supplied email, role, or capability fields cannot override the actor. Preserve existing origin checks and CSRF controls; they do not replace role authorization.
|
|
46
|
+
|
|
47
|
+
## Keep storage scoped to the application
|
|
48
|
+
|
|
49
|
+
For shared storage, include a trusted application identifier in read and mutation predicates. Obtain it from server configuration or trusted platform context; a submitted application ID is not authorization. Dedicated application storage can provide this boundary without adding a per-user filter.
|
|
50
|
+
|
|
51
|
+
Recorded author email is attribution only. It neither makes content private nor gives its author additional permissions.
|
|
52
|
+
|
|
53
|
+
## Protect a download
|
|
54
|
+
|
|
55
|
+
Register the resource route under the existing root middleware; route registration generates its `Route` types. `readSharedExport` is an illustrative application operation that returns only permitted export fields from the trusted application and performs no writes.
|
|
56
|
+
|
|
57
|
+
```ts
|
|
58
|
+
// app/routes/content-export.ts
|
|
59
|
+
import { readSharedExport } from "../../lib/shared-content.server";
|
|
60
|
+
import { userContext } from "../../lib/user-context.server";
|
|
61
|
+
import type { Route } from "./+types/content-export";
|
|
62
|
+
|
|
63
|
+
export async function loader({ context }: Route.LoaderArgs) {
|
|
64
|
+
context.get(userContext);
|
|
65
|
+
const content = await readSharedExport();
|
|
66
|
+
|
|
67
|
+
return new Response(JSON.stringify(content), {
|
|
68
|
+
headers: {
|
|
69
|
+
"Content-Type": "application/json; charset=utf-8",
|
|
70
|
+
"Content-Disposition": 'attachment; filename="content.json"',
|
|
71
|
+
"Cache-Control": "private, no-store",
|
|
72
|
+
},
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Viewers can download readable content unless the product defines an additional restriction. Use the same access and scope checks for attachments and signed download URLs. Link to a resource download with an ordinary anchor or a `reloadDocument` link.
|
|
78
|
+
|
|
79
|
+
## Upload capabilities
|
|
80
|
+
|
|
81
|
+
A signed upload URL grants write authority before data is uploaded. Check write permission before issuing it or accepting uploaded data, and again when a separate completion request publishes the file. These are distinct write boundaries.
|
|
@@ -12,6 +12,17 @@ Oxlint + Oxfmt (the underlying engine) provides robust linting and formatting. M
|
|
|
12
12
|
|
|
13
13
|
---
|
|
14
14
|
|
|
15
|
+
## Monolayer Application Permissions
|
|
16
|
+
|
|
17
|
+
Preserve existing authentication and workspace role behavior during feature work, including demos. Use [monolayer-application-permissions](.agents/skills/monolayer-application-permissions/SKILL.md) for application content access, editing interactions, or authentication/context changes.
|
|
18
|
+
|
|
19
|
+
The default development role is `owner`, so testing only with that role can miss permission errors affecting other users. When verifying role-dependent behavior, temporarily change the role in the `import.meta.env.DEV` branch of [lib/user-context.server.ts](lib/user-context.server.ts):
|
|
20
|
+
|
|
21
|
+
- Use `viewer` to verify that shared content remains readable, editing controls are hidden or disabled, and server writes return `403` without side effects.
|
|
22
|
+
- Use `editor` to verify that permitted create, edit, delete, and upload flows work without requiring the `owner` role.
|
|
23
|
+
|
|
24
|
+
Select checks relevant to the feature being changed. This testing exception does not require additional approval or a new role-selection mechanism. Restore the original role after testing and keep temporary role changes out of commits. Preserve the production authentication branch.
|
|
25
|
+
|
|
15
26
|
## Core Principles
|
|
16
27
|
|
|
17
28
|
Write code that is **accessible, performant, type-safe, and maintainable**. Focus on clarity and explicit intent over brevity.
|
package/template/AGENTS.md
CHANGED
|
@@ -12,6 +12,17 @@ Oxlint + Oxfmt (the underlying engine) provides robust linting and formatting. M
|
|
|
12
12
|
|
|
13
13
|
---
|
|
14
14
|
|
|
15
|
+
## Monolayer Application Permissions
|
|
16
|
+
|
|
17
|
+
Preserve existing authentication and workspace role behavior during feature work, including demos. Use [monolayer-application-permissions](.agents/skills/monolayer-application-permissions/SKILL.md) for application content access, editing interactions, or authentication/context changes.
|
|
18
|
+
|
|
19
|
+
The default development role is `owner`, so testing only with that role can miss permission errors affecting other users. When verifying role-dependent behavior, temporarily change the role in the `import.meta.env.DEV` branch of [lib/user-context.server.ts](lib/user-context.server.ts):
|
|
20
|
+
|
|
21
|
+
- Use `viewer` to verify that shared content remains readable, editing controls are hidden or disabled, and server writes return `403` without side effects.
|
|
22
|
+
- Use `editor` to verify that permitted create, edit, delete, and upload flows work without requiring the `owner` role.
|
|
23
|
+
|
|
24
|
+
Select checks relevant to the feature being changed. This testing exception does not require additional approval or a new role-selection mechanism. Restore the original role after testing and keep temporary role changes out of commits. Preserve the production authentication branch.
|
|
25
|
+
|
|
15
26
|
## Core Principles
|
|
16
27
|
|
|
17
28
|
Write code that is **accessible, performant, type-safe, and maintainable**. Focus on clarity and explicit intent over brevity.
|
package/template/app/root.tsx
CHANGED
|
@@ -7,9 +7,9 @@ import {
|
|
|
7
7
|
ScrollRestoration,
|
|
8
8
|
} from "react-router";
|
|
9
9
|
|
|
10
|
-
import type { Route } from "./+types/root";
|
|
11
|
-
|
|
12
10
|
import "./app.css";
|
|
11
|
+
import { userContextMiddleware } from "../lib/user-context.server";
|
|
12
|
+
import type { Route } from "./+types/root";
|
|
13
13
|
|
|
14
14
|
export const links: Route.LinksFunction = () => [
|
|
15
15
|
{ rel: "preconnect", href: "https://fonts.googleapis.com" },
|
|
@@ -74,3 +74,5 @@ export function ErrorBoundary({ error }: Route.ErrorBoundaryProps) {
|
|
|
74
74
|
</main>
|
|
75
75
|
);
|
|
76
76
|
}
|
|
77
|
+
|
|
78
|
+
export const middleware: Route.MiddlewareFunction[] = [userContextMiddleware];
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { createContext } from "react-router";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
|
|
4
|
+
import type { Route } from "../app/+types/root";
|
|
5
|
+
|
|
6
|
+
export const userContextMiddleware: Route.MiddlewareFunction =
|
|
7
|
+
// oxlint-disable-next-line require-await
|
|
8
|
+
async ({ request, context }, next) => {
|
|
9
|
+
const result = userSchema.safeParse(
|
|
10
|
+
import.meta.env.DEV
|
|
11
|
+
? {
|
|
12
|
+
email: "developer@example.com",
|
|
13
|
+
role: "owner",
|
|
14
|
+
}
|
|
15
|
+
: {
|
|
16
|
+
email: request.headers.get("x-user-email"),
|
|
17
|
+
role: request.headers.get("x-user-authorization"),
|
|
18
|
+
}
|
|
19
|
+
);
|
|
20
|
+
|
|
21
|
+
if (!result.success) {
|
|
22
|
+
throw new Response("Unauthorized", { status: 401 });
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
context.set(userContext, result.data);
|
|
26
|
+
|
|
27
|
+
return next();
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export const userSchema = z.object({
|
|
31
|
+
email: z.email(),
|
|
32
|
+
role: z.enum(["owner", "editor", "viewer"]),
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
export const userContext = createContext<z.infer<typeof userSchema>>();
|