@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
package/agent-meta/manifest.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 3,
|
|
3
3
|
"package": "@owlmeans/client-planning",
|
|
4
|
-
"version": "0.1.18-rc.
|
|
5
|
-
"generatedAt": "2026-10-
|
|
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.
|
|
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.
|
|
25
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
+
})
|