@owlmeans/create-app 0.1.18-rc.15 → 0.1.18-rc.16

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@owlmeans/create-app",
3
- "version": "0.1.18-rc.15",
3
+ "version": "0.1.18-rc.16",
4
4
  "license": "MIT",
5
5
  "description": "Scaffold a fullstack OwlMeans Common app — common + api + web workspaces, shadcn UI navigation/layout, no auth, and a session-scoped in-memory resource, or --bare for the demo-free shell. Also usable programmatically via scaffold(). Deploys agent skills via @owlmeans/agent-skills by default.",
6
6
  "type": "module",
@@ -30,7 +30,7 @@
30
30
  "template"
31
31
  ],
32
32
  "dependencies": {
33
- "@owlmeans/agent-skills": "^0.1.18-rc.12"
33
+ "@owlmeans/agent-skills": "^0.1.18-rc.13"
34
34
  },
35
35
  "devDependencies": {
36
36
  "@owlmeans/dep-config": "workspace:*",
@@ -79,6 +79,14 @@ package (in the installed packages and at https://github.com/owlmeans/common) or
79
79
  already solves the problem **before** proposing a third-party library or a custom solution, and
80
80
  simplify whatever you do write. This is required for every planning and development task.
81
81
 
82
+ ## Runtime config advertisement
83
+
84
+ The config endpoint is default-deny. A backend config field reaches the browser only when the
85
+ package that owns its browser consumer imports `apiConfigPlugin({ allow, deny? })` from
86
+ `@owlmeans/api-config` at module scope. Name public nested fields precisely; use `every()` and a
87
+ nested `deny` selector when a public collection carries a credential. Databases, queues, SMTP,
88
+ tokens, secrets and internal addresses never belong in an `allow` selector.
89
+
82
90
  ## Skills
83
91
 
84
92
  Reusable guidance lives in `.agents/skills/<name>/SKILL.md`, deployed by `@owlmeans/agent-skills`
@@ -57,7 +57,7 @@ other `@owlmeans/*` packages:
57
57
 
58
58
  ```json
59
59
  "dependencies": {
60
- "@owlmeans/queue": "^0.1.18-rc.9"
60
+ "@owlmeans/queue": "^0.1.18-rc.10"
61
61
  }
62
62
  ```
63
63
 
@@ -1,13 +1,14 @@
1
1
  import { randomUUID } from 'node:crypto'
2
- import { handleBody } from '@owlmeans/server-app'
3
- import type { AddItemPayload, SessionItem, SessionParams } from '__APP_SLUG__-common'
2
+ import { handlers } from '@owlmeans/server-app'
3
+ import { session, type SessionItem } from '__APP_SLUG__-common'
4
4
  import { SESSION_ITEMS } from '../../consts.js'
5
5
  import type { Context } from '../../types.js'
6
6
 
7
- export const add = handleBody<AddItemPayload>(async (payload, context, req) => {
8
- const ctx = context as Context
9
- const { sid } = req.params as SessionParams
10
- const resource = ctx.getStaticResource<SessionItem>(SESSION_ITEMS)
7
+ const handle = handlers<Context>()
8
+
9
+ export const add = handle.body(session.add, async (payload, context, request) => {
10
+ const { sid } = request.params
11
+ const resource = context.getStaticResource<SessionItem>(SESSION_ITEMS)
11
12
 
12
13
  const item: SessionItem = {
13
14
  id: randomUUID(),
@@ -1,11 +1,12 @@
1
- import { handleParams } from '@owlmeans/server-app'
2
- import type { SessionItem, SessionParams } from '__APP_SLUG__-common'
1
+ import { handlers } from '@owlmeans/server-app'
2
+ import { session, type SessionItem } from '__APP_SLUG__-common'
3
3
  import { SESSION_ITEMS } from '../../consts.js'
4
4
  import type { Context } from '../../types.js'
5
5
 
6
- export const list = handleParams<SessionParams>(async (params, context) => {
7
- const ctx = context as Context
8
- const resource = ctx.getStaticResource<SessionItem>(SESSION_ITEMS)
6
+ const handle = handlers<Context>()
7
+
8
+ export const list = handle.params(session.list, async (params, context) => {
9
+ const resource = context.getStaticResource<SessionItem>(SESSION_ITEMS)
9
10
 
10
11
  // The resource answers the whole question — this session's items, newest first.
11
12
  const { items } = await resource.list(
@@ -1,11 +1,12 @@
1
- import { handleParams } from '@owlmeans/server-app'
2
- import type { ItemParams, SessionItem } from '__APP_SLUG__-common'
1
+ import { handlers } from '@owlmeans/server-app'
2
+ import { session, type SessionItem } from '__APP_SLUG__-common'
3
3
  import { SESSION_ITEMS } from '../../consts.js'
4
4
  import type { Context } from '../../types.js'
5
5
 
6
- export const remove = handleParams<ItemParams>(async (params, context) => {
7
- const ctx = context as Context
8
- const resource = ctx.getStaticResource<SessionItem>(SESSION_ITEMS)
6
+ const handle = handlers<Context>()
7
+
8
+ export const remove = handle.params(session.remove, async (params, context) => {
9
+ const resource = context.getStaticResource<SessionItem>(SESSION_ITEMS)
9
10
 
10
11
  const existing = await resource.load(params.id)
11
12
  // Only remove the item if it belongs to the requesting session.
@@ -1,6 +1,5 @@
1
- import { entrypoints } from '@owlmeans/server-app'
1
+ import { elevate, entrypoints } from '@owlmeans/server-app'
2
2
  import { sharedEntrypoints } from '__APP_SLUG__-common'
3
3
 
4
- // Handlers attach to the shared declarations, never to a route re-declared here:
5
- // `elevate(sharedEntrypoints, alias, handler)` from '@owlmeans/server-app'.
6
- export const appEntrypoints = [...entrypoints, ...sharedEntrypoints]
4
+ // Handlers bind to a protocol; a bare shell starts without any implementations.
5
+ export const appEntrypoints = [...entrypoints, ...elevate(sharedEntrypoints, [])]
@@ -1,11 +1,8 @@
1
1
  import { elevate, entrypoints } from '@owlmeans/server-app'
2
- import { session, sessionEntrypoints } from '__APP_SLUG__-common'
2
+ import { sessionEntrypoints } from '__APP_SLUG__-common'
3
3
  import * as handlers from './app/session/index.js'
4
4
 
5
- // Attach handler implementations to the shared entrypoint declarations.
6
- elevate(sessionEntrypoints, session.base)
7
- elevate(sessionEntrypoints, session.list, handlers.list)
8
- elevate(sessionEntrypoints, session.add, handlers.add)
9
- elevate(sessionEntrypoints, session.remove, handlers.remove)
5
+ // Each handler is already bound to its protocol; elevation materializes the local server entries.
6
+ const sessionModules = elevate(sessionEntrypoints, [handlers.list, handlers.add, handlers.remove])
10
7
 
11
- export const appEntrypoints = [...entrypoints, ...sessionEntrypoints]
8
+ export const appEntrypoints = [...entrypoints, ...sessionModules]
@@ -6,17 +6,3 @@ export const APP_API = '__APP_SLUG__-api'
6
6
  /** Local development ports. */
7
7
  export const WEB_PORT = 3001
8
8
  export const API_PORT = 3000
9
-
10
- /** Backend (API) entrypoint identifiers. */
11
- export const session = {
12
- base: '__APP_SLUG__:api:session',
13
- list: '__APP_SLUG__:api:session:list',
14
- add: '__APP_SLUG__:api:session:add',
15
- remove: '__APP_SLUG__:api:session:remove',
16
- }
17
-
18
- /** Frontend (web) entrypoint identifiers. */
19
- export const web = {
20
- session: '__APP_SLUG__:web:session',
21
- about: '__APP_SLUG__:web:about',
22
- }
@@ -1,8 +1,8 @@
1
- import type { CommonEntrypoint } from '@owlmeans/entrypoint'
1
+ import type { EntrypointProtocol } from '@owlmeans/entrypoint'
2
2
 
3
3
  /**
4
4
  * The entrypoint declarations both sides share: the api elevates them with handlers, the web
5
5
  * elevates them with screens or just calls them. Routes resolve under the api service `base`
6
6
  * (`/api`), so an `entrypoint(route(alias, '/items', ...))` added here answers on `/api/items`.
7
7
  */
8
- export const sharedEntrypoints: CommonEntrypoint[] = []
8
+ export const sharedEntrypoints: EntrypointProtocol[] = []
@@ -1,26 +1,56 @@
1
- import { body, entrypoint, filter, params } from '@owlmeans/entrypoint'
2
- import { route, RouteMethod } from '@owlmeans/route'
3
- import { session } from './consts.js'
1
+ import { contract, openProtocol, protocol, protocols, typed } from '@owlmeans/entrypoint'
2
+ import { route, RouteMethod, frontend } from '@owlmeans/route'
4
3
  import { AddItemSchema, ItemParamsSchema, SessionParamsSchema } from './schemas.js'
5
- import type { AddItemPayload, ItemParams, SessionParams } from './types.js'
4
+ import type { SessionItem } from './types.js'
5
+
6
+ const aliases = {
7
+ session: {
8
+ base: '__APP_SLUG__:api:session',
9
+ list: '__APP_SLUG__:api:session:list',
10
+ add: '__APP_SLUG__:api:session:add',
11
+ remove: '__APP_SLUG__:api:session:remove',
12
+ },
13
+ web: {
14
+ base: '__APP_SLUG__:web:base',
15
+ home: '__APP_SLUG__:web:home',
16
+ session: '__APP_SLUG__:web:session',
17
+ about: '__APP_SLUG__:web:about',
18
+ },
19
+ }
20
+
21
+ const sessionBase = protocol(route(aliases.session.base, '/session'), contract())
22
+ const webBase = openProtocol(route(aliases.web.base, '/', frontend()))
6
23
 
7
24
  /**
8
25
  * Shared entrypoint declarations. The api elevates these with handlers; the web
9
26
  * elevates them with screen components and calls them. Routes resolve under the
10
27
  * api service `base` (`/api`), so e.g. `session.list` → `GET /api/session/:sid/items`.
11
28
  */
12
- export const sessionEntrypoints = [
13
- entrypoint(route(session.base, '/session')),
14
- entrypoint(
15
- route(session.list, '/:sid/items', { parent: session.base, method: RouteMethod.GET }),
16
- filter(params<SessionParams>(SessionParamsSchema)),
29
+ export const session = {
30
+ base: sessionBase,
31
+ list: protocol(
32
+ route(aliases.session.list, '/:sid/items', { parent: sessionBase, method: RouteMethod.GET }),
33
+ contract.request({ params: SessionParamsSchema }, typed<SessionItem[]>()),
17
34
  ),
18
- entrypoint(
19
- route(session.add, '/:sid/items', { parent: session.base, method: RouteMethod.POST }),
20
- filter(params<SessionParams>(SessionParamsSchema, body<AddItemPayload>(AddItemSchema))),
35
+ add: protocol(
36
+ route(aliases.session.add, '/:sid/items', { parent: sessionBase, method: RouteMethod.POST }),
37
+ contract.request({
38
+ params: SessionParamsSchema,
39
+ body: AddItemSchema,
40
+ }, typed<SessionItem>()),
21
41
  ),
22
- entrypoint(
23
- route(session.remove, '/:sid/items/:id', { parent: session.base, method: RouteMethod.DELETE }),
24
- filter(params<ItemParams>(ItemParamsSchema)),
42
+ remove: protocol(
43
+ route(aliases.session.remove, '/:sid/items/:id', { parent: sessionBase, method: RouteMethod.DELETE }),
44
+ contract.request({ params: ItemParamsSchema }, typed<{ removed: boolean }>()),
25
45
  ),
26
- ]
46
+ }
47
+
48
+ /** Frontend routes are protocol declarations too; their renderer is bound by the web project. */
49
+ export const web = {
50
+ base: webBase,
51
+ home: openProtocol(route(aliases.web.home, '/', frontend({ default: true, parent: webBase }))),
52
+ session: openProtocol(route(aliases.web.session, '/session', frontend({ parent: webBase }))),
53
+ about: openProtocol(route(aliases.web.about, '/about', frontend({ parent: webBase }))),
54
+ }
55
+
56
+ export const sessionEntrypoints = protocols(session)
@@ -1,25 +1,25 @@
1
- import type { JSONSchemaType } from 'ajv'
1
+ import { schema } from '@owlmeans/entrypoint'
2
2
  import type { AddItemPayload, ItemParams, SessionParams } from './types.js'
3
3
 
4
- export const AddItemSchema: JSONSchemaType<AddItemPayload> = {
4
+ export const AddItemSchema = schema<AddItemPayload>({
5
5
  type: 'object',
6
6
  properties: {
7
7
  text: { type: 'string', minLength: 1, maxLength: 280 },
8
8
  },
9
9
  required: ['text'],
10
10
  additionalProperties: false,
11
- }
11
+ })
12
12
 
13
- export const SessionParamsSchema: JSONSchemaType<SessionParams> = {
13
+ export const SessionParamsSchema = schema<SessionParams>({
14
14
  type: 'object',
15
15
  properties: {
16
16
  sid: { type: 'string', minLength: 1 },
17
17
  },
18
18
  required: ['sid'],
19
19
  additionalProperties: false,
20
- }
20
+ })
21
21
 
22
- export const ItemParamsSchema: JSONSchemaType<ItemParams> = {
22
+ export const ItemParamsSchema = schema<ItemParams>({
23
23
  type: 'object',
24
24
  properties: {
25
25
  sid: { type: 'string', minLength: 1 },
@@ -27,4 +27,4 @@ export const ItemParamsSchema: JSONSchemaType<ItemParams> = {
27
27
  },
28
28
  required: ['sid', 'id'],
29
29
  additionalProperties: false,
30
- }
30
+ })
@@ -1,11 +1,10 @@
1
- import { BASE, entrypoint, entrypoints as baseEntrypoints, frontend, handler, HOME, route } from '@owlmeans/web-panel'
1
+ import { BASE, bind, entrypoint, entrypoints as baseEntrypoints, frontend, handler, HOME, route } from '@owlmeans/web-panel'
2
2
  import { sharedEntrypoints } from '__APP_SLUG__-common'
3
3
  import { MainLayout } from './layout/main.js'
4
4
  import { HomeScreen } from './screens/home.js'
5
5
 
6
- // Backend entrypoints arrive as declarations only `elevate(entrypoints, alias)` (from
7
- // '@owlmeans/web-panel') makes one callable here without giving it a component.
8
- const entrypoints = [...baseEntrypoints, ...sharedEntrypoints]
6
+ // Backend protocol declarations are materialized locally so each can be called from the browser.
7
+ const entrypoints = [...baseEntrypoints, ...sharedEntrypoints.map(protocol => bind(protocol))]
9
8
 
10
9
  // Frontend layout + screens. BASE renders the shared layout; HOME is its default child.
11
10
  entrypoints.push(entrypoint(route(BASE, '/', frontend()), handler(MainLayout)))
@@ -1,22 +1,16 @@
1
- import { BASE, elevate, entrypoint, entrypoints as baseEntrypoints, frontend, handler, HOME, route } from '@owlmeans/web-panel'
2
- import { session, sessionEntrypoints, web } from '__APP_SLUG__-common'
1
+ import { bindAll, bindScreen, entrypoints as baseEntrypoints, handler } from '@owlmeans/web-panel'
2
+ import { session, web } from '__APP_SLUG__-common'
3
3
  import { MainLayout } from './layout/main.js'
4
4
  import { AboutScreen } from './screens/about.js'
5
5
  import { HomeScreen } from './screens/home.js'
6
6
  import { SessionScreen } from './screens/session.js'
7
7
 
8
- const entrypoints = [...baseEntrypoints, ...sessionEntrypoints]
8
+ const entrypoints = [...baseEntrypoints, ...bindAll(session)]
9
9
 
10
- // Backend entrypoints elevated without a component so the client can call them.
11
- elevate(entrypoints, session.base)
12
- elevate(entrypoints, session.list)
13
- elevate(entrypoints, session.add)
14
- elevate(entrypoints, session.remove)
15
-
16
- // Frontend layout + screens. BASE renders the shared layout; HOME is its default child.
17
- entrypoints.push(entrypoint(route(BASE, '/', frontend()), handler(MainLayout)))
18
- entrypoints.push(entrypoint(route(HOME, '/', frontend({ default: true, parent: BASE })), handler(HomeScreen)))
19
- entrypoints.push(entrypoint(route(web.session, '/session', frontend({ parent: BASE })), handler(SessionScreen)))
20
- entrypoints.push(entrypoint(route(web.about, '/about', frontend({ parent: BASE })), handler(AboutScreen)))
10
+ // Frontend protocol declarations bind to renderers in the web project.
11
+ entrypoints.push(bindScreen(web.base, handler(MainLayout)))
12
+ entrypoints.push(bindScreen(web.home, handler(HomeScreen)))
13
+ entrypoints.push(bindScreen(web.session, handler(SessionScreen)))
14
+ entrypoints.push(bindScreen(web.about, handler(AboutScreen)))
21
15
 
22
16
  export const appEntrypoints = entrypoints
@@ -1,5 +1,4 @@
1
1
  import { useEffect, useState, type FC } from 'react'
2
- import type { ClientEntrypoint } from '@owlmeans/client-entrypoint'
3
2
  import { useStoreList } from '@owlmeans/client'
4
3
  import { session, type SessionItem } from '__APP_SLUG__-common'
5
4
  import { SESSION_STATE, useContext } from '../context.js'
@@ -39,7 +38,7 @@ export const SessionScreen: FC = () => {
39
38
  // leave the store too, and one write wakes the subscribers once instead of once per record.
40
39
  const load = async () => {
41
40
  const data = await ctx
42
- .entrypoint<ClientEntrypoint<SessionItem[]>>(session.list)
41
+ .entrypoint(session.list)
43
42
  .call({ params: { sid } })
44
43
  await store.replace(data ?? [])
45
44
  }
@@ -51,7 +50,7 @@ export const SessionScreen: FC = () => {
51
50
  setBusy(true)
52
51
  try {
53
52
  const item = await ctx
54
- .entrypoint<ClientEntrypoint<SessionItem>>(session.add)
53
+ .entrypoint(session.add)
55
54
  .call({ params: { sid }, body: { text } })
56
55
  setText('')
57
56
  if (item != null) {
@@ -64,7 +63,7 @@ export const SessionScreen: FC = () => {
64
63
 
65
64
  const remove = async (id: string) => {
66
65
  await ctx
67
- .entrypoint<ClientEntrypoint<{ removed: boolean }>>(session.remove)
66
+ .entrypoint(session.remove)
68
67
  .call({ params: { sid, id } })
69
68
  await store.delete(id)
70
69
  }