@owlmeans/client-planning 0.1.18-rc.40 → 0.1.18-rc.41

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/README.md CHANGED
@@ -9,7 +9,7 @@ planning service runs in-process.
9
9
  ## Installation
10
10
 
11
11
  ```bash
12
- bun add @owlmeans/client-planning@^0.1.18-rc.40
12
+ bun add @owlmeans/client-planning@^0.1.18-rc.41
13
13
  ```
14
14
 
15
15
  ## Concepts
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "schemaVersion": 3,
3
3
  "package": "@owlmeans/client-planning",
4
- "version": "0.1.18-rc.40",
5
- "generatedAt": "2026-10-26T22:23:53.853Z",
4
+ "version": "0.1.18-rc.41",
5
+ "generatedAt": "2026-10-27T08:58:03.615Z",
6
6
  "canonicalRepo": "https://github.com/owlmeans/common",
7
7
  "entries": [
8
8
  {
@@ -8,7 +8,7 @@ user-invocable: false
8
8
  # @owlmeans/client-planning
9
9
 
10
10
  **Layer:** Client
11
- **Install:** `"@owlmeans/client-planning": "^0.1.18-rc.40"` in `dependencies`
11
+ **Install:** `"@owlmeans/client-planning": "^0.1.18-rc.41"` in `dependencies`
12
12
 
13
13
  The client half of OwlMeans planning. It answers the `PlanningFacade` interface of
14
14
  `@owlmeans/planning` over the protocol tree a server mounted with `@owlmeans/server-planning`, keeps
@@ -21,8 +21,12 @@ occurs, and the schema is left unchanged. Preserve nested money objects and cale
21
21
  The native server validates every write again.
22
22
 
23
23
  The validator interprets schemas with `@cfworker/json-schema`, with no `eval`, `new Function` or
24
- network reference loading. Do not call `makeSchemaRegistry(...).validator()` or
25
- `.assigneeValidator()` in a browser with strict `script-src`: those remain native AJV compilers.
24
+ network reference loading. Building, loading and merging a registry is safe under a strict
25
+ `script-src` — `loadSchemas()`, `definitions.registry()`/`bundle()`, `model()`, `makeSchemaRegistry(bundle)`,
26
+ `scopedRegistryOf` — because a page runs only the closed-form checks of the served `app` declarations
27
+ (`planning` → types part). Do not call `.validator()`, `.assigneeValidator()`, `appSchemaHelper.assertBag`,
28
+ `scopedSchemaHelper.assert*Schema` or `validateHelper.*` in a browser with strict `script-src`: those
29
+ remain native AJV compilers, the server's.
26
30
  The Draft 7 meta-schema is DATA in `src/validation/consts.ts` (`JSON_SCHEMA_DRAFT_07`, pinned to ajv's
27
31
  `refs/json-schema-draft-07.json` by `tests/draft7.spec.ts`), never an import of that file: Node ESM needs
28
32
  `with { type: 'json' }` on it, and a bundler that also meets ajv's own attribute-less `require` of the
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@owlmeans/client-planning",
3
- "version": "0.1.18-rc.40",
3
+ "version": "0.1.18-rc.41",
4
4
  "license": "MIT",
5
5
  "description": "The client half of OwlMeans planning — the remote planning facade, the state mirror and its commit fold, and the subscribe-then-poll commit wait. React-free: it runs in a browser and in a Node client alike.",
6
6
  "type": "module",
@@ -30,7 +30,7 @@
30
30
  "@owlmeans/context": "^0.1.18-rc.35",
31
31
  "@owlmeans/entrypoint": "^0.1.18-rc.42",
32
32
  "@owlmeans/error": "^0.1.18-rc.39",
33
- "@owlmeans/planning": "^0.1.18-rc.31",
33
+ "@owlmeans/planning": "^0.1.18-rc.32",
34
34
  "@owlmeans/resource": "^0.1.18-rc.41",
35
35
  "@owlmeans/log": "^0.1.18-rc.4",
36
36
  "@owlmeans/route": "^0.1.18-rc.36",
@@ -43,7 +43,7 @@
43
43
  },
44
44
  "devDependencies": {
45
45
  "@owlmeans/dep-config": "workspace:*",
46
- "@owlmeans/server-planning": "^0.1.18-rc.38",
46
+ "@owlmeans/server-planning": "^0.1.18-rc.39",
47
47
  "@owlmeans/test": "^0.1.18-rc.39",
48
48
  "@types/bun": "^1.4.2",
49
49
  "ajv": "^8.20.0",
@@ -0,0 +1,119 @@
1
+ import { afterEach, describe, expect, test } from 'bun:test'
2
+ import { appSchemaHelper, makeSchemaRegistry } from '@owlmeans/planning'
3
+ import type { PlanningAppDeclarations, PlanningService } from '@owlmeans/planning'
4
+ import { makePlanningFieldValidator } from '../src/index.js'
5
+ import { makeSuite, STORY } from './context.js'
6
+ import type { Suite, SuiteOptions } from './context.js'
7
+
8
+ /** What a browser says when a page's `script-src` lacks `'unsafe-eval'`. */
9
+ const REFUSAL = "Evaluating a string as JavaScript violates the following Content Security Policy directive because 'unsafe-eval' is not an allowed source of script"
10
+
11
+ /** The work-management seed's declarations: every type and flow may carry a description. */
12
+ const APP: PlanningAppDeclarations = {
13
+ type: { properties: { description: { type: 'string', maxLength: 2000 } } },
14
+ flow: { properties: { description: { type: 'string', maxLength: 2000 } } },
15
+ }
16
+ const SERVED = appSchemaHelper.merge([APP])
17
+
18
+ /**
19
+ * The CLIENT runs in a browser page under the publisher's policy — a `document`, and every code
20
+ * generation refused — while the in-process server it calls does not: the suite's transport leaves
21
+ * the page before the server handler runs (`before`) and re-enters it once the server answered
22
+ * (`after`). Armed only by a test, so the suite's own set-up runs outside it.
23
+ */
24
+ const makeStrictPage = () => {
25
+ const scope = globalThis as unknown as Record<string, unknown>
26
+ const saved = { Function: scope.Function as FunctionConstructor, eval: scope.eval as typeof eval, error: console.error }
27
+ const counts = { generated: 0, logged: 0 }
28
+ const refuse = (): never => {
29
+ counts.generated++
30
+ throw new EvalError(REFUSAL)
31
+ }
32
+ const refusing = { Function: new Proxy(saved.Function, { construct: refuse, apply: refuse }), eval: new Proxy(saved.eval, { apply: refuse }) }
33
+ let armed = false
34
+
35
+ const enter = (): void => {
36
+ scope.document = {}
37
+ scope.Function = refusing.Function
38
+ scope.eval = refusing.eval
39
+ console.error = () => { counts.logged++ }
40
+ }
41
+ const leave = (): void => {
42
+ delete scope.document
43
+ scope.Function = saved.Function
44
+ scope.eval = saved.eval
45
+ console.error = saved.error
46
+ }
47
+
48
+ return {
49
+ counts,
50
+ arm: () => { armed = true; counts.generated = 0; counts.logged = 0; enter() },
51
+ disarm: () => { armed = false; leave() },
52
+ hooks: {
53
+ before: async () => { if (armed) leave() },
54
+ after: async () => { if (armed) enter() },
55
+ } satisfies Pick<SuiteOptions, 'before' | 'after'>,
56
+ }
57
+ }
58
+
59
+ const page = makeStrictPage()
60
+
61
+ /** A seed-like target: the application's declarations registered on the server, as its plugin does. */
62
+ const declared = async (opts: SuiteOptions = {}): Promise<Suite> => {
63
+ const suite = await makeSuite({ schemas: false, ...opts, ...page.hooks })
64
+ ;(suite.server as unknown as { planning: () => PlanningService }).planning().use({ name: 'seed', schemas: { app: APP } })
65
+
66
+ return suite
67
+ }
68
+
69
+ afterEach(() => page.disarm())
70
+
71
+ describe('@owlmeans/client-planning — a page under a strict script-src', () => {
72
+ test('the served bundle loads, a model resolves and a form validates without generating code', async () => {
73
+ const suite = await declared()
74
+ const project = await suite.project('Spring')
75
+ const story = await suite.story(project.id!, 'Warm-up', { area: 'stage' })
76
+ const service = suite.client.planning()
77
+
78
+ page.arm()
79
+ const registry = await service.loadSchemas()
80
+ expect(registry.app()).toEqual(SERVED!)
81
+ const model = await suite.planning.model(story.id!)
82
+ expect(model.available().map(rule => rule.name)).toContain('start')
83
+ const form = makePlanningFieldValidator(registry.fieldsSchema(STORY))
84
+ expect(form.validateFields({ area: 'stage' }).valid).toBe(true)
85
+ expect(form.validateFields({ area: 2 }).valid).toBe(false)
86
+ page.disarm()
87
+
88
+ expect(page.counts).toEqual({ generated: 0, logged: 0 })
89
+ })
90
+
91
+ test('an organization layer and a project layer resolve through definitions without generating code', async () => {
92
+ const suite = await declared({ definitions: true })
93
+ const project = await suite.project('Spring')
94
+ const story = await suite.story(project.id!, 'Warm-up', { area: 'stage' })
95
+ const definitions = suite.planning.definitions!
96
+
97
+ page.arm()
98
+ const layer = await definitions.registry()
99
+ expect(layer.app()).toEqual(SERVED!)
100
+ expect((await definitions.bundle()).app).toEqual(SERVED!)
101
+ expect((await definitions.registry(project.id!)).app()).toEqual(SERVED!)
102
+ const model = await suite.planning.model(story.id!)
103
+ expect(model.schema().type).toBe(STORY)
104
+ page.disarm()
105
+
106
+ expect(page.counts).toEqual({ generated: 0, logged: 0 })
107
+ })
108
+
109
+ test('the page really refuses code generation: a native validator cannot be compiled in it', async () => {
110
+ const suite = await declared()
111
+ const bundle = (await suite.client.planning().loadSchemas()).bundle()
112
+
113
+ page.arm()
114
+ expect(() => makeSchemaRegistry(bundle).validator(STORY)).toThrow(EvalError)
115
+ page.disarm()
116
+
117
+ expect(page.counts.generated).toBeGreaterThan(0)
118
+ })
119
+ })