@owlmeans/server-planning 0.1.18-rc.17 → 0.1.18-rc.19
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 +2 -2
- package/agent-meta/manifest.json +2 -2
- package/agent-meta/skills/server-planning/SKILL.md +71 -47
- package/build/actions/execute.d.ts +13 -3
- package/build/actions/execute.d.ts.map +1 -1
- package/build/actions/execute.js +52 -5
- package/build/actions/execute.js.map +1 -1
- package/build/actions/schemas.d.ts +3 -2
- package/build/actions/schemas.d.ts.map +1 -1
- package/build/actions/schemas.js +7 -3
- package/build/actions/schemas.js.map +1 -1
- package/build/types.d.ts +9 -0
- package/build/types.d.ts.map +1 -1
- package/build/utils/scope.d.ts +15 -1
- package/build/utils/scope.d.ts.map +1 -1
- package/build/utils/scope.js +25 -0
- package/build/utils/scope.js.map +1 -1
- package/package.json +11 -11
- package/src/actions/execute.ts +58 -6
- package/src/actions/schemas.ts +7 -3
- package/src/types.ts +9 -0
- package/src/utils/scope.ts +28 -1
- package/tests/writes.spec.ts +140 -0
package/README.md
CHANGED
|
@@ -10,7 +10,7 @@ the `@owlmeans/server-planning/store` subpath.
|
|
|
10
10
|
## Installation
|
|
11
11
|
|
|
12
12
|
```sh
|
|
13
|
-
bun add @owlmeans/server-planning@^0.1.18-rc.
|
|
13
|
+
bun add @owlmeans/server-planning@^0.1.18-rc.19 @owlmeans/planning@^0.1.18-rc.16 ajv
|
|
14
14
|
```
|
|
15
15
|
|
|
16
16
|
## Concepts
|
|
@@ -169,7 +169,7 @@ This package ships embedded agent skills under `agent-meta/`. After installing y
|
|
|
169
169
|
your project's skill store (`.agents/skills/`):
|
|
170
170
|
|
|
171
171
|
```sh
|
|
172
|
-
npx @owlmeans/agent-skills@^0.1.18-rc.
|
|
172
|
+
npx @owlmeans/agent-skills@^0.1.18-rc.46
|
|
173
173
|
```
|
|
174
174
|
|
|
175
175
|
The embedded files are version-matched to this package release. Do not edit them
|
package/agent-meta/manifest.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 2,
|
|
3
3
|
"package": "@owlmeans/server-planning",
|
|
4
|
-
"version": "0.1.18-rc.
|
|
5
|
-
"generatedAt": "2026-10-
|
|
4
|
+
"version": "0.1.18-rc.19",
|
|
5
|
+
"generatedAt": "2026-10-03T15:33:53.018Z",
|
|
6
6
|
"canonicalRepo": "https://github.com/owlmeans/common",
|
|
7
7
|
"entries": [
|
|
8
8
|
{
|
|
@@ -8,7 +8,7 @@ user-invocable: false
|
|
|
8
8
|
# @owlmeans/server-planning
|
|
9
9
|
|
|
10
10
|
**Layer:** Server
|
|
11
|
-
**Install:** `"@owlmeans/server-planning": "^0.1.18-rc.
|
|
11
|
+
**Install:** `"@owlmeans/server-planning": "^0.1.18-rc.19"` in `dependencies` (`ajv` is a peer)
|
|
12
12
|
|
|
13
13
|
The general implementation of `@owlmeans/planning`: the planning service (a plugin host), the
|
|
14
14
|
scoped facade, the executor every write goes through, the in-memory reference store, the commit
|
|
@@ -30,9 +30,10 @@ reuses the `./store` subpath (`foldPending`, `makeCommitHub`) without pulling fa
|
|
|
30
30
|
| `PlanningHandlerOptions` | `{ service?, event?, maxPoll?, scope?(req, ctx), access?(req, ctx) }` |
|
|
31
31
|
| `PlanningAccessResolver`, `PlanningAccess`, `PlanningAccessGrants`, `PlanningGrant` | The optional access decision a hosting app supplies |
|
|
32
32
|
| `listCards` … `executePlanning`, `getCommit`, `watchCommits`, `listSchemas`, `defineSchemas`, `applySchemaRequest` | The handlers, to bind one by hand |
|
|
33
|
-
| `scopeOf(req, extra?)`, `accessScopeOf(req, access, extra?)`, `handlerScopeOf`, `assertGranted`, `actorOf(req)`, `concealed(run)`, `clampSeconds` | Scope and security helpers |
|
|
33
|
+
| `scopeOf(req, extra?)`, `accessScopeOf(req, access, extra?)`, `handlerScopeOf`, `assertGranted`, `assertWrites`, `writableIn`, `assertExecutionGranted`, `assertExecutionWrites`, `actorOf(req)`, `concealed(run)`, `clampSeconds` | Scope and security helpers |
|
|
34
34
|
| `makeDefinitions(runtime, facade)`, `makeSchemaViews(opts)`, `projectCriteriaOf(projects, through?)` | Data-defined schemas and the project narrowing |
|
|
35
35
|
| `makeProjectionProcessor(ctx, opts?)`, `planningQueueHooks(ctx, opts?)` | A generic projection job body and its `onJobDead` |
|
|
36
|
+
| `./queue`: `declarePlanningQueue(cfg, opts?)`, `PLANNING_PROJECTION_QUEUE` (`planning-projection`), `PLANNING_PROJECT_JOB` (`planning:project`), `ProjectionRequest` | The projection queue's shared address; each process chooses separately whether to listen |
|
|
36
37
|
| `./store`: `makeMemoryPlanningStore`, `foldPending`, `failPending`, `revisionsFromLog`, `commitEventOf`, `makeCommitHub`, `makeCompositeStore`, `wantsSpecifications` | What stores are built from |
|
|
37
38
|
| `./conformance`: `planningConformance`, `conformanceCasesFor`, `planningConformancePlugin`, `conformanceClock`, `ConformanceFailure` | The store conformance suite, runner-agnostic |
|
|
38
39
|
|
|
@@ -115,7 +116,7 @@ await planning.definitions!.define({
|
|
|
115
116
|
| Wrong | What happens | Right |
|
|
116
117
|
|---|---|---|
|
|
117
118
|
| `execute({ action, draft: { … } })` | refused: `planning:malformed:create-without-draft` | the draft goes under `card` |
|
|
118
|
-
| `execute({ …, wait: true })` | in process the key is ignored: the receipt returns before the commit, with no `card` | `execute({ … }, { wait: true })` — only the wire body (`ExecuteRequest`) carries `wait` inside |
|
|
119
|
+
| `execute({ …, wait: true })` | in process the key is ignored: the receipt returns before the commit, with no `card` — hidden while an inline-folding store (`planning-postgres`) lands at once, exposed the first time another process holds the card's fold | `execute({ … }, { wait: true })` — only the wire body (`ExecuteRequest`) carries `wait` inside |
|
|
119
120
|
| `ctx.planning().for(…)` in a target | `ctx.planning is not a function` | `ctx.service<PlanningHostService>(PLANNING_SERVICE).for(…)` |
|
|
120
121
|
| `{ id, name: 'Repair', … }` on a flow, `{ key, name, … }` on a type | `define` refuses with `SchemaInvalid` (closed schemas); a code plugin's registry does not validate, so the key is silently dropped | a flow's key is `id`, a type's is `type`, the display name is `label` on both |
|
|
121
122
|
| `action: 'create'` | a type error — `TransitionAction` is an enum | `action: TransitionAction.Create` |
|
|
@@ -141,8 +142,7 @@ Nothing is appended before step 11 — every refusal leaves the log untouched.
|
|
|
141
142
|
default store holds data-defined schemas, the card's project is found first and everything
|
|
142
143
|
after reads that project's resolved layer (`Resolved.schemas`): a data-defined type of a
|
|
143
144
|
project with `scopedCardTypes` is admitted, a retired type creates nothing
|
|
144
|
-
(`UnknownWorkcardType('retired:…')`). Without the port `Resolved.schemas` IS the code registry
|
|
145
|
-
and the order is unchanged.
|
|
145
|
+
(`UnknownWorkcardType('retired:…')`). Without the port `Resolved.schemas` IS the code registry.
|
|
146
146
|
5. **`before` chain** — in plugin order; re-resolved once when it changed the card, type or parent.
|
|
147
147
|
6. **Validate** — shape (`planning:malformed:*`), the flow rule (`IllegalTransition`), immutables
|
|
148
148
|
(`planning:immutable:*`, `createdBy` among them — `assertCreatorFixed`), merged `fields` (`FieldsInvalid`), labels (`LabelNotAllowed`), a moved
|
|
@@ -164,22 +164,15 @@ Nothing is appended before step 11 — every refusal leaves the log untouched.
|
|
|
164
164
|
in-process caller may add `agent`/`runId` on the execution. A handler never passes a wire `actor`.
|
|
165
165
|
|
|
166
166
|
A create's `createdBy` is the scope's subject too — `creatorOf(scope)`: `profileId`, else `userId`,
|
|
167
|
-
else the same two of `scope.actor`. The executor stamps it on every path, so
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
refused with `planning:immutable:createdBy` on every action — a create's `changes` overlay too, since
|
|
177
|
-
the draft is its one source — whether it came over the wire, from an in-process caller, or from a
|
|
178
|
-
`before` plugin (validation runs after the chain), and nothing is appended. The executor holds this
|
|
179
|
-
check itself (`assertCreatorFixed`), beside `@owlmeans/planning`'s `assertMutable`, so it stands
|
|
180
|
-
whatever planning build a host resolves. `createdAt` is an identity key and refused the same way. A
|
|
181
|
-
row appended before the refusal existed still folds as written (the fold replays the log; guards
|
|
182
|
-
live at admission), and the conformance suite checks a store folds it like the pure fold does.
|
|
167
|
+
else the same two of `scope.actor`. The executor stamps it on every path, so an in-process facade
|
|
168
|
+
create and the HTTP `execute` produce the same owner. An in-process draft that names `createdBy`
|
|
169
|
+
keeps it (explicit wins — a manager creating on someone's behalf); a scope naming nobody (a service
|
|
170
|
+
or system scope) leaves it unset; the wire drops a draft's `createdBy` before the stamp.
|
|
171
|
+
|
|
172
|
+
`createdBy` never moves after the create (the rule is `planning` → The record model). The executor
|
|
173
|
+
enforces it itself with `assertCreatorFixed` at step 6, after the `before` chain, so a wire body, an
|
|
174
|
+
in-process caller and a plugin are refused alike, whatever `@owlmeans/planning` build the host
|
|
175
|
+
resolves. The conformance suite checks a store folds a stored row naming it as the pure fold does.
|
|
183
176
|
|
|
184
177
|
## Idempotency and `expectSeq`
|
|
185
178
|
|
|
@@ -221,8 +214,8 @@ the store; `bind` replaces the listener, so a store has exactly one.
|
|
|
221
214
|
the criteria asks for `kind: specification` or a `category` (`wantsSpecifications`, the rule every
|
|
222
215
|
store routes by).
|
|
223
216
|
- `schemas: true` adds the data-defined schema port (records per layer, a revision per
|
|
224
|
-
organization, synchronous `watch`); off by default
|
|
225
|
-
|
|
217
|
+
organization, synchronous `watch`); off by default — without it every type and flow resolves
|
|
218
|
+
from code.
|
|
226
219
|
- `sync` (default) folds, publishes and runs `committed` inside `project()`, serialized per card.
|
|
227
220
|
`sync: false` folds nothing until `flush(card?)` — a queued store's shape, for tests that need a
|
|
228
221
|
commit to stay pending.
|
|
@@ -288,15 +281,13 @@ mapping external records (`mappers`) belongs to that plugin's own store adapter.
|
|
|
288
281
|
|
|
289
282
|
- Without `opts.access`: `entityId` is `requireEntityKey(req)`; `opts.scope(req, ctx)` adds a
|
|
290
283
|
`channel` or `service` and can never replace the entity. `actor` and a create's `createdBy` come
|
|
291
|
-
from the request, never the body (`wireExecution` drops a draft's claim
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
(`@owlmeans/server-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
to several organizations — and, for a product with one shared organization (every person acts in
|
|
299
|
-
the application's own), the same key for everybody. Take it from the request in the handler and
|
|
284
|
+
from the request, never the body (`wireExecution` drops a draft's claim, `withCreator` stamps the
|
|
285
|
+
subject, `assertCreatorFixed` refuses a body naming `createdBy` — an update never re-owns a card).
|
|
286
|
+
- The organization of a scope is the one the request ACTS IN. `requireEntityKey(req)` answers
|
|
287
|
+
`req.entity.id`, the stable record id (the token's `entitySlug` only where no entity resolver is
|
|
288
|
+
registered). Behind an OIDC relying party (`@owlmeans/server-oidc-rp`) the acting organization is
|
|
289
|
+
session state: the guard attaches `req.entity` with `id` = the organization claim's frozen
|
|
290
|
+
`entityKey`, so the tenant key survives a rename and follows a switch. Take it in the handler and
|
|
300
291
|
pass it down; never read it from the context or from a body.
|
|
301
292
|
- Another entity's card, specification or transition answers `WorkcardNotFound` — the same as an
|
|
302
293
|
absent id — on reads and writes alike (`concealed`).
|
|
@@ -305,32 +296,65 @@ mapping external records (`mappers`) belongs to that plugin's own store adapter.
|
|
|
305
296
|
|
|
306
297
|
### The access resolver
|
|
307
298
|
|
|
308
|
-
`opts.access: (req, ctx) => Promise<{ entityId, projects?, grants? }>` is the hosting app's
|
|
299
|
+
`opts.access: (req, ctx) => Promise<{ entityId, projects?, writes?, grants? }>` is the hosting app's
|
|
309
300
|
decision per request; a throw is the request's answer (an `AuthForbidden` answers 403).
|
|
310
301
|
|
|
311
302
|
- `entityId` is the resolver's — never the token's, never `opts.scope`'s.
|
|
312
|
-
- `projects` becomes `PlanningScope.projects`, which the server facade enforces
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
`ParentNotFound
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
for specifications (`wantsSpecifications`) the facade resolves `through`
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
- A person who may see only SOME projects of their organization (a
|
|
325
|
-
|
|
303
|
+
- `projects` becomes `PlanningScope.projects`, which the server facade enforces on every read and
|
|
304
|
+
write. Visible are the projects, every card whose `parents` name one, and every specification
|
|
305
|
+
whose parent card is visible (its `parents` hold only that card). Anything else reads as absent
|
|
306
|
+
(`WorkcardNotFound`, `null`, an empty list); a create under an invisible parent is
|
|
307
|
+
`ParentNotFound`; links and transitions are filtered by `project` (and post-filtered, for a store
|
|
308
|
+
that ignores the field); commit subscriptions drop events outside the set and a status or wait
|
|
309
|
+
refuses them.
|
|
310
|
+
- **A narrowed list or count sees a specification whenever its parent card is visible** — exactly
|
|
311
|
+
what a single read admits. It is AND'd with `projectCriteriaOf(projects, through)`; when its
|
|
312
|
+
criteria ask for specifications (`wantsSpecifications`) the facade resolves `through` (the visible
|
|
313
|
+
parent cards) with ONE read per call: the query's `parent`, else the projects' own cards. A
|
|
314
|
+
summary counts no specification and pays nothing extra. `tests/narrowing.spec.ts` pins it.
|
|
315
|
+
- A person who may see only SOME projects of their organization (a tool-shed volunteer given one
|
|
316
|
+
shed) is expressed here: the resolver answers the project ids the person's grants list as
|
|
326
317
|
`projects`, and omits the key when a grant covers every project — never a hand-written filter in
|
|
327
318
|
each handler.
|
|
319
|
+
- `writes` is the subset of projects the person may WRITE in (`projects` are the ones they VIEW).
|
|
320
|
+
`execute` on a card, specification or link needs it to be writable — the rule a narrowed read
|
|
321
|
+
admits, over `writes`: the project itself, a card whose `parents` name one, a specification
|
|
322
|
+
through its parent card; a create needs every named parent writable, and a create at the root
|
|
323
|
+
(no parent, not a project) is refused. `schema.define` of a project layer needs the project in
|
|
324
|
+
`writes`. A refusal is `PlanningForbidden` (403) with `writes:<id>`, checked before the grants
|
|
325
|
+
and before the executor. NOT governed by it: a project create (`grants.createProjects` alone) and
|
|
326
|
+
the organization layer (`grants.defineSchemas`). A card the scope cannot see is still answered
|
|
327
|
+
as absent by the executor. Omitted — every visible project is writable.
|
|
328
|
+
`tests/writes.spec.ts` pins viewer, writer and omitted.
|
|
328
329
|
- `grants` gates the stock handlers' writes: `createProjects` (a project create — `true` for the
|
|
329
330
|
root, a list names the parent projects), `deleteProjects` (the delete of a project card — a list
|
|
330
331
|
names the projects), `defineSchemas` (`schema.define` — `true` for the organization's layer, a
|
|
331
332
|
list names project layers). A `grants` object refuses every flag it leaves out with
|
|
332
333
|
`PlanningForbidden` (403); no `grants` gates nothing. In-process facade writes are not gated.
|
|
333
334
|
|
|
335
|
+
## Mounting in a target
|
|
336
|
+
|
|
337
|
+
A generated target serves the STOCK tree; it never writes card, transition or schema routes of its
|
|
338
|
+
own, and its domain rules are `before` plugins.
|
|
339
|
+
|
|
340
|
+
- **common** declares the one tree the api and the web share:
|
|
341
|
+
`makePlanningProtocols({ base: { alias: 'api:planning' }, guards: DEFAULT_GUARD, definitions: true })`.
|
|
342
|
+
- **api** binds `servePlanningEntrypoints(tree, { access })` WITHOUT the commit socket —
|
|
343
|
+
`.filter(entrypoint => entrypoint.protocol !== tree.commit.events)`: a target has no socket that
|
|
344
|
+
authenticates after open, so the client long-polls (`commit.get` with `wait`, `execute` with
|
|
345
|
+
`wait: true`).
|
|
346
|
+
- **The resolver** answers `{ entityId, projects, writes, grants }`: `projects` = the containers the
|
|
347
|
+
person may view, `writes` = those they may modify, `grants` = `createProjects` / `deleteProjects`
|
|
348
|
+
/ `defineSchemas` from their create and admin rights. A creator sees what they created — stamp
|
|
349
|
+
ownership in a `before` hook and add it to both lists.
|
|
350
|
+
- **Kit types and flows** are LITERAL objects in the target's `sources/common/src/planning.ts`
|
|
351
|
+
`PLANNING` plugin, between its sentinels (`// owlmeans: add planning flows above this line`,
|
|
352
|
+
`… types above this line`) — a spread or an imported constant hides them from the library's
|
|
353
|
+
registration checks. Card types and flows carry `overridable: true`, `fields` schemas stay open
|
|
354
|
+
(`additionalProperties: true`), and the container project type carries `scopedCardTypes: true`,
|
|
355
|
+
so the people of the app can override and extend them as data through `schema.define`.
|
|
356
|
+
- The web side is `client-planning` → Mounting in a target; the store, `planning-postgres`.
|
|
357
|
+
|
|
334
358
|
## Data-defined types and flows
|
|
335
359
|
|
|
336
360
|
Where the default store has the schema port, `runtime.schemasFor(entityId, project?)` answers the
|
|
@@ -12,9 +12,19 @@ export declare const wireExecution: (body: ExecuteRequest, scope: PlanningScope)
|
|
|
12
12
|
/** `wait`/`timeout` as a server grants them: the timeout never holds a request past `maxPoll`. */
|
|
13
13
|
export declare const executeOptionsOf: (body: Pick<ExecuteRequest, 'wait' | 'timeout'>, maxPoll: number) => ExecuteOptions;
|
|
14
14
|
/**
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
15
|
+
* Refuse an execution outside the access's `writes`: the card it acts on (a specification through
|
|
16
|
+
* its parent card), or every parent a create names, must be one {@link writableIn} admits. A create
|
|
17
|
+
* of a project is `grants.createProjects`' alone. A card or parent this scope cannot see is left to
|
|
18
|
+
* the executor, which answers it as absent.
|
|
19
|
+
*
|
|
20
|
+
* @throws {PlanningForbidden}
|
|
21
|
+
*/
|
|
22
|
+
export declare const assertExecutionWrites: (facade: PlanningFacade, exec: TransitionExecution, access?: PlanningAccess) => Promise<void>;
|
|
23
|
+
/**
|
|
24
|
+
* The access a wire execution needs. `writes` first ({@link assertExecutionWrites}); then the
|
|
25
|
+
* grants: `createProjects` for a project create (its primary parent, or the root),
|
|
26
|
+
* `deleteProjects` for the delete of a project card. A card this scope cannot see is left to the
|
|
27
|
+
* executor, which answers it as absent.
|
|
18
28
|
*
|
|
19
29
|
* @throws {PlanningForbidden}
|
|
20
30
|
*/
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"execute.d.ts","sourceRoot":"","sources":["../../src/actions/execute.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,sBAAsB,CAAA;
|
|
1
|
+
{"version":3,"file":"execute.d.ts","sourceRoot":"","sources":["../../src/actions/execute.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,sBAAsB,CAAA;AAK/C,OAAO,KAAK,EACV,cAAc,EAAE,cAAc,EAAE,cAAc,EAAE,iBAAiB,EAAE,aAAa,EAAE,mBAAmB,EAEtG,MAAM,oBAAoB,CAAA;AAE3B,OAAO,KAAK,EAAE,OAAO,EAAE,cAAc,EAAE,sBAAsB,EAAE,MAAM,aAAa,CAAA;AAGlF;;;;;;GAMG;AACH,eAAO,MAAM,aAAa,SAAU,cAAc,SAAS,aAAa,KAAG,mBAO1E,CAAA;AAED,kGAAkG;AAClG,eAAO,MAAM,gBAAgB,SAAU,IAAI,CAAC,cAAc,EAAE,MAAM,GAAG,SAAS,CAAC,WAAW,MAAM,KAAG,cAOlG,CAAA;AAED;;;;;;;GAOG;AACH,eAAO,MAAM,qBAAqB,WACxB,cAAc,QAAQ,mBAAmB,WAAW,cAAc,KACzE,OAAO,CAAC,IAAI,CAqCd,CAAA;AAED;;;;;;;GAOG;AACH,eAAO,MAAM,sBAAsB,WACzB,cAAc,QAAQ,mBAAmB,WAAW,cAAc,KACzE,OAAO,CAAC,IAAI,CAed,CAAA;AAED,2BAA2B;AAC3B,eAAO,MAAM,eAAe,aAChB,iBAAiB,CAAC,SAAS,CAAC,SAAS,sBAAsB,KACpE,UAAU,CAAC,UAAU,CAAC,OAAO,QAAQ,CAAC,OAAO,CAAC,CAAC,CAAC,SAAS,CAAC,CAWxD,CAAA"}
|
package/build/actions/execute.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { handlers } from '@owlmeans/server-api';
|
|
2
|
-
import { DEFAULT_COMMIT_TIMEOUT, isProject, MAX_COMMIT_POLL, normalizeParents, TransitionAction, WorkcardKind, } from '@owlmeans/planning';
|
|
2
|
+
import { DEFAULT_COMMIT_TIMEOUT, isProject, isSpecification, MAX_COMMIT_POLL, normalizeParents, PlanningForbidden, TransitionAction, WorkcardKind, } from '@owlmeans/planning';
|
|
3
3
|
import { withCreator } from '../executor/creator.js';
|
|
4
|
-
import { assertGranted, concealed, handlerScopeOf } from '../utils/index.js';
|
|
4
|
+
import { assertGranted, concealed, handlerScopeOf, writableIn } from '../utils/index.js';
|
|
5
5
|
/**
|
|
6
6
|
* What a wire execution may say. `actor` is dropped — the scope names the writer — and a create's
|
|
7
7
|
* `createdBy` claim is dropped too: the executor's {@link withCreator} stamps the authenticated
|
|
@@ -27,13 +27,60 @@ export const executeOptionsOf = (body, maxPoll) => {
|
|
|
27
27
|
return { wait: true, timeout: Math.min(Number.isFinite(asked) && asked > 0 ? asked : DEFAULT_COMMIT_TIMEOUT, ceiling) };
|
|
28
28
|
};
|
|
29
29
|
/**
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
30
|
+
* Refuse an execution outside the access's `writes`: the card it acts on (a specification through
|
|
31
|
+
* its parent card), or every parent a create names, must be one {@link writableIn} admits. A create
|
|
32
|
+
* of a project is `grants.createProjects`' alone. A card or parent this scope cannot see is left to
|
|
33
|
+
* the executor, which answers it as absent.
|
|
34
|
+
*
|
|
35
|
+
* @throws {PlanningForbidden}
|
|
36
|
+
*/
|
|
37
|
+
export const assertExecutionWrites = async (facade, exec, access) => {
|
|
38
|
+
if (access?.writes == null) {
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
const admitted = async (card) => {
|
|
42
|
+
if (writableIn(access, card)) {
|
|
43
|
+
return true;
|
|
44
|
+
}
|
|
45
|
+
if (!isSpecification(card) || card.parent == null) {
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
const parent = await facade.cards.load(card.parent);
|
|
49
|
+
return parent == null || writableIn(access, parent);
|
|
50
|
+
};
|
|
51
|
+
if (exec.action === TransitionAction.Create && exec.card != null && typeof exec.card === 'object') {
|
|
52
|
+
if (exec.card.kind === WorkcardKind.Project) {
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
const parents = normalizeParents(exec.card.parent, exec.card.parents);
|
|
56
|
+
if (parents.length === 0) {
|
|
57
|
+
throw new PlanningForbidden('writes:root');
|
|
58
|
+
}
|
|
59
|
+
for (const id of parents) {
|
|
60
|
+
const parent = await facade.cards.load(id);
|
|
61
|
+
if (parent != null && !await admitted(parent)) {
|
|
62
|
+
throw new PlanningForbidden(`writes:${id}`);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
if (typeof exec.card === 'string' && exec.card !== '') {
|
|
68
|
+
const card = await facade.cards.load(exec.card);
|
|
69
|
+
if (card != null && !await admitted(card)) {
|
|
70
|
+
throw new PlanningForbidden(`writes:${exec.card}`);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
/**
|
|
75
|
+
* The access a wire execution needs. `writes` first ({@link assertExecutionWrites}); then the
|
|
76
|
+
* grants: `createProjects` for a project create (its primary parent, or the root),
|
|
77
|
+
* `deleteProjects` for the delete of a project card. A card this scope cannot see is left to the
|
|
78
|
+
* executor, which answers it as absent.
|
|
33
79
|
*
|
|
34
80
|
* @throws {PlanningForbidden}
|
|
35
81
|
*/
|
|
36
82
|
export const assertExecutionGranted = async (facade, exec, access) => {
|
|
83
|
+
await assertExecutionWrites(facade, exec, access);
|
|
37
84
|
if (access?.grants == null) {
|
|
38
85
|
return;
|
|
39
86
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"execute.js","sourceRoot":"","sources":["../../src/actions/execute.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,sBAAsB,CAAA;AAC/C,OAAO,EACL,sBAAsB,EAAE,SAAS,EAAE,eAAe,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,YAAY,
|
|
1
|
+
{"version":3,"file":"execute.js","sourceRoot":"","sources":["../../src/actions/execute.ts"],"names":[],"mappings":"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"}
|
|
@@ -18,8 +18,9 @@ export declare const listSchemas: (protocol: PlanningProtocols['schema']['list']
|
|
|
18
18
|
*/
|
|
19
19
|
export declare const applySchemaRequest: (definitions: PlanningDefinitions, request: SchemaDefineRequest) => Promise<SchemaDefineReply>;
|
|
20
20
|
/**
|
|
21
|
-
* The write of data-defined types and flows.
|
|
22
|
-
*
|
|
21
|
+
* The write of data-defined types and flows. A project layer needs the project in the access's
|
|
22
|
+
* `writes` when the resolver answers it; then `grants.defineSchemas` gates when the resolver answers
|
|
23
|
+
* grants (the organization-wide layer needs `true`, a project layer its id).
|
|
23
24
|
*
|
|
24
25
|
* @throws {PlanningUnsupported} where the store holds no data-defined schemas
|
|
25
26
|
* @throws {PlanningForbidden | SchemaConflict | SchemaSealed | SchemaInvalid | SchemaInUse | WorkcardNotFound}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"schemas.d.ts","sourceRoot":"","sources":["../../src/actions/schemas.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,sBAAsB,CAAA;AAE/C,OAAO,KAAK,EACV,mBAAmB,EAAE,iBAAiB,EAAE,iBAAiB,EAAE,mBAAmB,EAC/E,MAAM,oBAAoB,CAAA;AAC3B,OAAO,KAAK,EAAE,OAAO,EAAE,sBAAsB,EAAE,MAAM,aAAa,CAAA;AAGlE,KAAK,cAAc,GAAG,UAAU,CAAC,UAAU,CAAC,OAAO,QAAQ,CAAC,OAAO,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAA;AAKjF;;;;;;;GAOG;AACH,eAAO,MAAM,WAAW,aACZ,iBAAiB,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,SAAS,sBAAsB,KAC3E,cAQE,CAAA;AAEL;;;;GAIG;AACH,eAAO,MAAM,kBAAkB,gBAChB,mBAAmB,WAAW,mBAAmB,KAC7D,OAAO,CAAC,iBAAiB,CAyB3B,CAAA;AAED
|
|
1
|
+
{"version":3,"file":"schemas.d.ts","sourceRoot":"","sources":["../../src/actions/schemas.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,sBAAsB,CAAA;AAE/C,OAAO,KAAK,EACV,mBAAmB,EAAE,iBAAiB,EAAE,iBAAiB,EAAE,mBAAmB,EAC/E,MAAM,oBAAoB,CAAA;AAC3B,OAAO,KAAK,EAAE,OAAO,EAAE,sBAAsB,EAAE,MAAM,aAAa,CAAA;AAGlE,KAAK,cAAc,GAAG,UAAU,CAAC,UAAU,CAAC,OAAO,QAAQ,CAAC,OAAO,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAA;AAKjF;;;;;;;GAOG;AACH,eAAO,MAAM,WAAW,aACZ,iBAAiB,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,SAAS,sBAAsB,KAC3E,cAQE,CAAA;AAEL;;;;GAIG;AACH,eAAO,MAAM,kBAAkB,gBAChB,mBAAmB,WAAW,mBAAmB,KAC7D,OAAO,CAAC,iBAAiB,CAyB3B,CAAA;AAED;;;;;;;GAOG;AACH,eAAO,MAAM,aAAa,aACd,WAAW,CAAC,iBAAiB,CAAC,QAAQ,CAAC,CAAC,QAAQ,CAAC,CAAC,SAAS,sBAAsB,KAC1F,cAaE,CAAA"}
|
package/build/actions/schemas.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { handlers } from '@owlmeans/server-api';
|
|
2
2
|
import { PlanningUnsupported, SchemaWriteMode } from '@owlmeans/planning';
|
|
3
|
-
import { assertGranted, concealed, handlerScopeOf } from '../utils/index.js';
|
|
3
|
+
import { assertGranted, assertWrites, concealed, handlerScopeOf } from '../utils/index.js';
|
|
4
4
|
const text = (value) => value == null || value === '' ? undefined : `${Array.isArray(value) ? value[0] : value}`;
|
|
5
5
|
/**
|
|
6
6
|
* The schema bundle — what a client loads to answer `can()` with no round trip. Scoped like every
|
|
@@ -47,8 +47,9 @@ export const applySchemaRequest = async (definitions, request) => {
|
|
|
47
47
|
return { records, bundle: await definitions.bundle(request.project) };
|
|
48
48
|
};
|
|
49
49
|
/**
|
|
50
|
-
* The write of data-defined types and flows.
|
|
51
|
-
*
|
|
50
|
+
* The write of data-defined types and flows. A project layer needs the project in the access's
|
|
51
|
+
* `writes` when the resolver answers it; then `grants.defineSchemas` gates when the resolver answers
|
|
52
|
+
* grants (the organization-wide layer needs `true`, a project layer its id).
|
|
52
53
|
*
|
|
53
54
|
* @throws {PlanningUnsupported} where the store holds no data-defined schemas
|
|
54
55
|
* @throws {PlanningForbidden | SchemaConflict | SchemaSealed | SchemaInvalid | SchemaInUse | WorkcardNotFound}
|
|
@@ -59,6 +60,9 @@ export const defineSchemas = (protocol, opts) => handlers().request(protocol, as
|
|
|
59
60
|
if (facade.definitions == null) {
|
|
60
61
|
throw new PlanningUnsupported('definitions');
|
|
61
62
|
}
|
|
63
|
+
if (request.project != null) {
|
|
64
|
+
assertWrites(access, request.project);
|
|
65
|
+
}
|
|
62
66
|
assertGranted(access, 'defineSchemas', request.project);
|
|
63
67
|
return await applySchemaRequest(facade.definitions, request);
|
|
64
68
|
}));
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"schemas.js","sourceRoot":"","sources":["../../src/actions/schemas.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,sBAAsB,CAAA;AAC/C,OAAO,EAAE,mBAAmB,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAA;AAKzE,OAAO,EAAE,aAAa,EAAE,SAAS,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAA;
|
|
1
|
+
{"version":3,"file":"schemas.js","sourceRoot":"","sources":["../../src/actions/schemas.ts"],"names":[],"mappings":"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"}
|
package/build/types.d.ts
CHANGED
|
@@ -53,6 +53,15 @@ export interface PlanningAccess {
|
|
|
53
53
|
entityId: string;
|
|
54
54
|
/** Narrows every read and write to these project cards. Every project when omitted. */
|
|
55
55
|
projects?: string[];
|
|
56
|
+
/**
|
|
57
|
+
* The project cards the request may WRITE in: `execute` on a card, specification or link inside
|
|
58
|
+
* one of them (the same rule a narrowed read admits — the project, the cards whose `parents` name
|
|
59
|
+
* it, a specification through its parent card), and `schema.define` of a project layer. A project
|
|
60
|
+
* outside `projects` is still unreachable. Omitted — writes reach every project `projects` admits;
|
|
61
|
+
* present — anything else is refused with `PlanningForbidden` (403). Creating a project is
|
|
62
|
+
* `grants.createProjects`' alone; the organization-wide schema layer is `grants.defineSchemas`'.
|
|
63
|
+
*/
|
|
64
|
+
writes?: string[];
|
|
56
65
|
/**
|
|
57
66
|
* Gates the writes above. Omitted — nothing is gated; present — a flag it leaves out is refused
|
|
58
67
|
* with `PlanningForbidden`.
|
package/build/types.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAA;AAC/E,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAA;AAC3D,OAAO,KAAK,EACV,aAAa,EAAE,cAAc,EAAE,aAAa,EAAE,eAAe,EAAE,aAAa,EAAE,gBAAgB,EAC9F,mBAAmB,EACpB,MAAM,oBAAoB,CAAA;AAC3B,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAA;AAC3D,OAAO,KAAK,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,0BAA0B,CAAA;AAE3E,MAAM,WAAW,MAAO,SAAQ,YAAY;CAAI;AAEhD,MAAM,WAAW,OAAO,CAAC,CAAC,SAAS,MAAM,GAAG,MAAM,CAAE,SAAQ,aAAa,CAAC,CAAC,CAAC,EAAE,eAAe,EAAE,mBAAmB;CAAI;AAEtH,MAAM,WAAW,sBAAsB;IACrC,sFAAsF;IACtF,KAAK,CAAC,EAAE,aAAa,CAAA;IACrB,OAAO,CAAC,EAAE,cAAc,EAAE,CAAA;IAC1B,8CAA8C;IAC9C,OAAO,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,aAAa,EAAE,CAAC;QAAC,KAAK,CAAC,EAAE,gBAAgB,EAAE,CAAA;KAAE,CAAA;IACjE,6DAA6D;IAC7D,KAAK,CAAC,EAAE,OAAO,CAAA;IACf,0CAA0C;IAC1C,GAAG,CAAC,EAAE,MAAM,MAAM,CAAA;IAClB,2BAA2B;IAC3B,GAAG,CAAC,EAAE,MAAM,MAAM,CAAA;CACnB;AAED;;;;GAIG;AACH,MAAM,WAAW,mBAAoB,SAAQ,eAAe,EAAE,WAAW;CAAI;AAE7E,yFAAyF;AACzF,MAAM,WAAW,sBAAsB;IACrC,CAAC,GAAG,EAAE,eAAe,EAAE,GAAG,EAAE,YAAY,CAAC,WAAW,CAAC,GAAG,OAAO,CAAC,aAAa,CAAC,GAAG,OAAO,CAAC,OAAO,CAAC,aAAa,CAAC,CAAC,CAAA;CACjH;AAED;;;GAGG;AACH,MAAM,MAAM,aAAa,GAAG,OAAO,GAAG,MAAM,EAAE,CAAA;AAE9C,MAAM,WAAW,oBAAoB;IACnC,yFAAyF;IACzF,cAAc,CAAC,EAAE,aAAa,CAAA;IAC9B,oDAAoD;IACpD,cAAc,CAAC,EAAE,aAAa,CAAA;IAC9B,yFAAyF;IACzF,aAAa,CAAC,EAAE,aAAa,CAAA;CAC9B;AAED,4DAA4D;AAC5D,MAAM,WAAW,cAAc;IAC7B,6FAA6F;IAC7F,QAAQ,EAAE,MAAM,CAAA;IAChB,uFAAuF;IACvF,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAA;IACnB;;;OAGG;IACH,MAAM,CAAC,EAAE,oBAAoB,CAAA;CAC9B;AAED;;;GAGG;AACH,MAAM,WAAW,sBAAsB;IACrC,CAAC,GAAG,EAAE,eAAe,EAAE,GAAG,EAAE,YAAY,CAAC,WAAW,CAAC,GAAG,OAAO,CAAC,cAAc,CAAC,CAAA;CAChF;AAED,MAAM,WAAW,sBAAsB;IACrC,mEAAmE;IACnE,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,kGAAkG;IAClG,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,yGAAyG;IACzG,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,KAAK,CAAC,EAAE,sBAAsB,CAAA;IAC9B;;;OAGG;IACH,MAAM,CAAC,EAAE,sBAAsB,CAAA;CAChC"}
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAA;AAC/E,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAA;AAC3D,OAAO,KAAK,EACV,aAAa,EAAE,cAAc,EAAE,aAAa,EAAE,eAAe,EAAE,aAAa,EAAE,gBAAgB,EAC9F,mBAAmB,EACpB,MAAM,oBAAoB,CAAA;AAC3B,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAA;AAC3D,OAAO,KAAK,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,0BAA0B,CAAA;AAE3E,MAAM,WAAW,MAAO,SAAQ,YAAY;CAAI;AAEhD,MAAM,WAAW,OAAO,CAAC,CAAC,SAAS,MAAM,GAAG,MAAM,CAAE,SAAQ,aAAa,CAAC,CAAC,CAAC,EAAE,eAAe,EAAE,mBAAmB;CAAI;AAEtH,MAAM,WAAW,sBAAsB;IACrC,sFAAsF;IACtF,KAAK,CAAC,EAAE,aAAa,CAAA;IACrB,OAAO,CAAC,EAAE,cAAc,EAAE,CAAA;IAC1B,8CAA8C;IAC9C,OAAO,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,aAAa,EAAE,CAAC;QAAC,KAAK,CAAC,EAAE,gBAAgB,EAAE,CAAA;KAAE,CAAA;IACjE,6DAA6D;IAC7D,KAAK,CAAC,EAAE,OAAO,CAAA;IACf,0CAA0C;IAC1C,GAAG,CAAC,EAAE,MAAM,MAAM,CAAA;IAClB,2BAA2B;IAC3B,GAAG,CAAC,EAAE,MAAM,MAAM,CAAA;CACnB;AAED;;;;GAIG;AACH,MAAM,WAAW,mBAAoB,SAAQ,eAAe,EAAE,WAAW;CAAI;AAE7E,yFAAyF;AACzF,MAAM,WAAW,sBAAsB;IACrC,CAAC,GAAG,EAAE,eAAe,EAAE,GAAG,EAAE,YAAY,CAAC,WAAW,CAAC,GAAG,OAAO,CAAC,aAAa,CAAC,GAAG,OAAO,CAAC,OAAO,CAAC,aAAa,CAAC,CAAC,CAAA;CACjH;AAED;;;GAGG;AACH,MAAM,MAAM,aAAa,GAAG,OAAO,GAAG,MAAM,EAAE,CAAA;AAE9C,MAAM,WAAW,oBAAoB;IACnC,yFAAyF;IACzF,cAAc,CAAC,EAAE,aAAa,CAAA;IAC9B,oDAAoD;IACpD,cAAc,CAAC,EAAE,aAAa,CAAA;IAC9B,yFAAyF;IACzF,aAAa,CAAC,EAAE,aAAa,CAAA;CAC9B;AAED,4DAA4D;AAC5D,MAAM,WAAW,cAAc;IAC7B,6FAA6F;IAC7F,QAAQ,EAAE,MAAM,CAAA;IAChB,uFAAuF;IACvF,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAA;IACnB;;;;;;;OAOG;IACH,MAAM,CAAC,EAAE,MAAM,EAAE,CAAA;IACjB;;;OAGG;IACH,MAAM,CAAC,EAAE,oBAAoB,CAAA;CAC9B;AAED;;;GAGG;AACH,MAAM,WAAW,sBAAsB;IACrC,CAAC,GAAG,EAAE,eAAe,EAAE,GAAG,EAAE,YAAY,CAAC,WAAW,CAAC,GAAG,OAAO,CAAC,cAAc,CAAC,CAAA;CAChF;AAED,MAAM,WAAW,sBAAsB;IACrC,mEAAmE;IACnE,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,kGAAkG;IAClG,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,yGAAyG;IACzG,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,KAAK,CAAC,EAAE,sBAAsB,CAAA;IAC9B;;;OAGG;IACH,MAAM,CAAC,EAAE,sBAAsB,CAAA;CAChC"}
|
package/build/utils/scope.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { BasicConfig, BasicContext } from '@owlmeans/context';
|
|
2
2
|
import type { AbstractRequest } from '@owlmeans/entrypoint';
|
|
3
|
-
import type { PlanningFacade, PlanningScope, PlanningService, TransitionActor } from '@owlmeans/planning';
|
|
3
|
+
import type { PlanningFacade, PlanningScope, PlanningService, TransitionActor, Workcard } from '@owlmeans/planning';
|
|
4
4
|
import type { PlanningAccess, PlanningAccessGrants, PlanningHandlerOptions } from '../types.js';
|
|
5
5
|
/** The authenticated subject of a request, as a transition names it. */
|
|
6
6
|
export declare const actorOf: (req: AbstractRequest) => TransitionActor;
|
|
@@ -44,4 +44,18 @@ export declare const handlerFacade: (ctx: BasicContext<BasicConfig>, req: Abstra
|
|
|
44
44
|
* @throws {PlanningForbidden}
|
|
45
45
|
*/
|
|
46
46
|
export declare const assertGranted: (access: PlanningAccess | undefined, grant: keyof PlanningAccessGrants, target?: string) => void;
|
|
47
|
+
/**
|
|
48
|
+
* A card the access's `writes` admits — the rule a narrowed read admits, over `writes`: a project
|
|
49
|
+
* it names, or a card whose `parents` name one. A specification is admitted through its parent card
|
|
50
|
+
* by the caller. No `writes` admits everything.
|
|
51
|
+
*/
|
|
52
|
+
export declare const writableIn: (access: PlanningAccess | undefined, card: Pick<Workcard, 'id' | 'parents'>) => boolean;
|
|
53
|
+
/**
|
|
54
|
+
* Refuse a write into a project the access's `writes` leaves out. No `writes` refuses nothing;
|
|
55
|
+
* `target` is the project the write is in (`undefined` for the organization's root, which a set
|
|
56
|
+
* `writes` never admits).
|
|
57
|
+
*
|
|
58
|
+
* @throws {PlanningForbidden}
|
|
59
|
+
*/
|
|
60
|
+
export declare const assertWrites: (access: PlanningAccess | undefined, target?: string) => void;
|
|
47
61
|
//# sourceMappingURL=scope.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"scope.d.ts","sourceRoot":"","sources":["../../src/utils/scope.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAA;AAClE,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAA;AAE3D,OAAO,KAAK,EAAE,cAAc,EAAE,aAAa,EAAE,eAAe,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAA;
|
|
1
|
+
{"version":3,"file":"scope.d.ts","sourceRoot":"","sources":["../../src/utils/scope.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAA;AAClE,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAA;AAE3D,OAAO,KAAK,EAAE,cAAc,EAAE,aAAa,EAAE,eAAe,EAAE,eAAe,EAAE,QAAQ,EAAE,MAAM,oBAAoB,CAAA;AACnH,OAAO,KAAK,EACV,cAAc,EAAE,oBAAoB,EAAE,sBAAsB,EAC7D,MAAM,aAAa,CAAA;AAKpB,wEAAwE;AACxE,eAAO,MAAM,OAAO,QAAS,eAAe,KAAG,eAG7C,CAAA;AAeF;;;;;;;;GAQG;AACH,eAAO,MAAM,OAAO,QAAS,eAAe,UAAU,OAAO,CAAC,aAAa,CAAC,KAAG,aACjC,CAAA;AAE9C;;;GAGG;AACH,eAAO,MAAM,aAAa,QACnB,eAAe,UAAU,cAAc,UAAU,OAAO,CAAC,aAAa,CAAC,KAC3E,aAKF,CAAA;AAED,eAAO,MAAM,iBAAiB,QACvB,YAAY,CAAC,WAAW,CAAC,SAAS,IAAI,CAAC,sBAAsB,EAAE,SAAS,CAAC,KAC7E,eAAsF,CAAA;AAEzF,MAAM,WAAW,YAAY;IAC3B,MAAM,EAAE,cAAc,CAAA;IACtB,iEAAiE;IACjE,MAAM,CAAC,EAAE,cAAc,CAAA;CACxB;AAED;;;;;;GAMG;AACH,eAAO,MAAM,cAAc,QACpB,YAAY,CAAC,WAAW,CAAC,OAAO,eAAe,SAAS,sBAAsB,KAClF,OAAO,CAAC,YAAY,CAQtB,CAAA;AAED,8FAA8F;AAC9F,eAAO,MAAM,aAAa,QACnB,YAAY,CAAC,WAAW,CAAC,OAAO,eAAe,SAAS,sBAAsB,KAClF,OAAO,CAAC,cAAc,CAAkD,CAAA;AAE3E;;;;;;;GAOG;AACH,eAAO,MAAM,aAAa,WAChB,cAAc,GAAG,SAAS,SAAS,MAAM,oBAAoB,WAAW,MAAM,KACrF,IASF,CAAA;AAED;;;;GAIG;AACH,eAAO,MAAM,UAAU,WAAY,cAAc,GAAG,SAAS,QAAQ,IAAI,CAAC,QAAQ,EAAE,IAAI,GAAG,SAAS,CAAC,KAAG,OAMvG,CAAA;AAED;;;;;;GAMG;AACH,eAAO,MAAM,YAAY,WAAY,cAAc,GAAG,SAAS,WAAW,MAAM,KAAG,IAKlF,CAAA"}
|
package/build/utils/scope.js
CHANGED
|
@@ -72,4 +72,29 @@ export const assertGranted = (access, grant, target) => {
|
|
|
72
72
|
}
|
|
73
73
|
throw new PlanningForbidden(`${grant}:${target ?? 'root'}`);
|
|
74
74
|
};
|
|
75
|
+
/**
|
|
76
|
+
* A card the access's `writes` admits — the rule a narrowed read admits, over `writes`: a project
|
|
77
|
+
* it names, or a card whose `parents` name one. A specification is admitted through its parent card
|
|
78
|
+
* by the caller. No `writes` admits everything.
|
|
79
|
+
*/
|
|
80
|
+
export const writableIn = (access, card) => {
|
|
81
|
+
const writes = access?.writes;
|
|
82
|
+
if (writes == null) {
|
|
83
|
+
return true;
|
|
84
|
+
}
|
|
85
|
+
return (card.id != null && writes.includes(card.id)) || card.parents.some(parent => writes.includes(parent));
|
|
86
|
+
};
|
|
87
|
+
/**
|
|
88
|
+
* Refuse a write into a project the access's `writes` leaves out. No `writes` refuses nothing;
|
|
89
|
+
* `target` is the project the write is in (`undefined` for the organization's root, which a set
|
|
90
|
+
* `writes` never admits).
|
|
91
|
+
*
|
|
92
|
+
* @throws {PlanningForbidden}
|
|
93
|
+
*/
|
|
94
|
+
export const assertWrites = (access, target) => {
|
|
95
|
+
if (access?.writes == null || (target != null && access.writes.includes(target))) {
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
throw new PlanningForbidden(`writes:${target ?? 'root'}`);
|
|
99
|
+
};
|
|
75
100
|
//# sourceMappingURL=scope.js.map
|
package/build/utils/scope.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"scope.js","sourceRoot":"","sources":["../../src/utils/scope.ts"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"scope.js","sourceRoot":"","sources":["../../src/utils/scope.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@owlmeans/server-planning",
|
|
3
|
-
"version": "0.1.18-rc.
|
|
3
|
+
"version": "0.1.18-rc.19",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"description": "The general planning implementation — the planning service and its plugin/middleware registry, the transition executor, the in-memory store, the protocol handlers and the commit feed.",
|
|
6
6
|
"type": "module",
|
|
@@ -44,21 +44,21 @@
|
|
|
44
44
|
}
|
|
45
45
|
},
|
|
46
46
|
"dependencies": {
|
|
47
|
-
"@owlmeans/auth": "^0.1.18-rc.
|
|
48
|
-
"@owlmeans/auth-common": "^0.1.18-rc.
|
|
47
|
+
"@owlmeans/auth": "^0.1.18-rc.38",
|
|
48
|
+
"@owlmeans/auth-common": "^0.1.18-rc.44",
|
|
49
49
|
"@owlmeans/basic-ids": "^0.1.18-rc.34",
|
|
50
50
|
"@owlmeans/context": "^0.1.18-rc.32",
|
|
51
|
-
"@owlmeans/entrypoint": "^0.1.18-rc.
|
|
51
|
+
"@owlmeans/entrypoint": "^0.1.18-rc.39",
|
|
52
52
|
"@owlmeans/error": "^0.1.18-rc.36",
|
|
53
|
-
"@owlmeans/planning": "^0.1.18-rc.
|
|
54
|
-
"@owlmeans/queue": "^0.1.18-rc.
|
|
53
|
+
"@owlmeans/planning": "^0.1.18-rc.16",
|
|
54
|
+
"@owlmeans/queue": "^0.1.18-rc.38",
|
|
55
55
|
"@owlmeans/resource": "^0.1.18-rc.37",
|
|
56
56
|
"@owlmeans/route": "^0.1.18-rc.33",
|
|
57
|
-
"@owlmeans/server-api": "^0.1.18-rc.
|
|
58
|
-
"@owlmeans/server-context": "^0.1.18-rc.
|
|
59
|
-
"@owlmeans/server-entrypoint": "^0.1.18-rc.
|
|
60
|
-
"@owlmeans/server-socket": "^0.1.18-rc.
|
|
61
|
-
"@owlmeans/socket": "^0.1.18-rc.
|
|
57
|
+
"@owlmeans/server-api": "^0.1.18-rc.48",
|
|
58
|
+
"@owlmeans/server-context": "^0.1.18-rc.40",
|
|
59
|
+
"@owlmeans/server-entrypoint": "^0.1.18-rc.38",
|
|
60
|
+
"@owlmeans/server-socket": "^0.1.18-rc.51",
|
|
61
|
+
"@owlmeans/socket": "^0.1.18-rc.37"
|
|
62
62
|
},
|
|
63
63
|
"peerDependencies": {
|
|
64
64
|
"ajv": "*",
|
package/src/actions/execute.ts
CHANGED
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
import { handlers } from '@owlmeans/server-api'
|
|
2
2
|
import {
|
|
3
|
-
DEFAULT_COMMIT_TIMEOUT, isProject, MAX_COMMIT_POLL, normalizeParents,
|
|
3
|
+
DEFAULT_COMMIT_TIMEOUT, isProject, isSpecification, MAX_COMMIT_POLL, normalizeParents, PlanningForbidden,
|
|
4
|
+
TransitionAction, WorkcardKind,
|
|
4
5
|
} from '@owlmeans/planning'
|
|
5
6
|
import type {
|
|
6
7
|
ExecuteOptions, ExecuteRequest, PlanningFacade, PlanningProtocols, PlanningScope, TransitionExecution,
|
|
7
|
-
TransitionReceiptView,
|
|
8
|
+
TransitionReceiptView, Workcard,
|
|
8
9
|
} from '@owlmeans/planning'
|
|
9
10
|
import { withCreator } from '../executor/creator.js'
|
|
10
11
|
import type { Context, PlanningAccess, PlanningHandlerOptions } from '../types.js'
|
|
11
|
-
import { assertGranted, concealed, handlerScopeOf } from '../utils/index.js'
|
|
12
|
+
import { assertGranted, concealed, handlerScopeOf, writableIn } from '../utils/index.js'
|
|
12
13
|
|
|
13
14
|
/**
|
|
14
15
|
* What a wire execution may say. `actor` is dropped — the scope names the writer — and a create's
|
|
@@ -37,15 +38,66 @@ export const executeOptionsOf = (body: Pick<ExecuteRequest, 'wait' | 'timeout'>,
|
|
|
37
38
|
}
|
|
38
39
|
|
|
39
40
|
/**
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
41
|
+
* Refuse an execution outside the access's `writes`: the card it acts on (a specification through
|
|
42
|
+
* its parent card), or every parent a create names, must be one {@link writableIn} admits. A create
|
|
43
|
+
* of a project is `grants.createProjects`' alone. A card or parent this scope cannot see is left to
|
|
44
|
+
* the executor, which answers it as absent.
|
|
45
|
+
*
|
|
46
|
+
* @throws {PlanningForbidden}
|
|
47
|
+
*/
|
|
48
|
+
export const assertExecutionWrites = async (
|
|
49
|
+
facade: PlanningFacade, exec: TransitionExecution, access?: PlanningAccess
|
|
50
|
+
): Promise<void> => {
|
|
51
|
+
if (access?.writes == null) {
|
|
52
|
+
return
|
|
53
|
+
}
|
|
54
|
+
const admitted = async (card: Workcard): Promise<boolean> => {
|
|
55
|
+
if (writableIn(access, card)) {
|
|
56
|
+
return true
|
|
57
|
+
}
|
|
58
|
+
if (!isSpecification(card) || card.parent == null) {
|
|
59
|
+
return false
|
|
60
|
+
}
|
|
61
|
+
const parent = await facade.cards.load(card.parent)
|
|
62
|
+
return parent == null || writableIn(access, parent)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (exec.action === TransitionAction.Create && exec.card != null && typeof exec.card === 'object') {
|
|
66
|
+
if (exec.card.kind === WorkcardKind.Project) {
|
|
67
|
+
return
|
|
68
|
+
}
|
|
69
|
+
const parents = normalizeParents(exec.card.parent, exec.card.parents)
|
|
70
|
+
if (parents.length === 0) {
|
|
71
|
+
throw new PlanningForbidden('writes:root')
|
|
72
|
+
}
|
|
73
|
+
for (const id of parents) {
|
|
74
|
+
const parent = await facade.cards.load(id)
|
|
75
|
+
if (parent != null && !await admitted(parent)) {
|
|
76
|
+
throw new PlanningForbidden(`writes:${id}`)
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return
|
|
80
|
+
}
|
|
81
|
+
if (typeof exec.card === 'string' && exec.card !== '') {
|
|
82
|
+
const card = await facade.cards.load(exec.card)
|
|
83
|
+
if (card != null && !await admitted(card)) {
|
|
84
|
+
throw new PlanningForbidden(`writes:${exec.card}`)
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* The access a wire execution needs. `writes` first ({@link assertExecutionWrites}); then the
|
|
91
|
+
* grants: `createProjects` for a project create (its primary parent, or the root),
|
|
92
|
+
* `deleteProjects` for the delete of a project card. A card this scope cannot see is left to the
|
|
93
|
+
* executor, which answers it as absent.
|
|
43
94
|
*
|
|
44
95
|
* @throws {PlanningForbidden}
|
|
45
96
|
*/
|
|
46
97
|
export const assertExecutionGranted = async (
|
|
47
98
|
facade: PlanningFacade, exec: TransitionExecution, access?: PlanningAccess
|
|
48
99
|
): Promise<void> => {
|
|
100
|
+
await assertExecutionWrites(facade, exec, access)
|
|
49
101
|
if (access?.grants == null) {
|
|
50
102
|
return
|
|
51
103
|
}
|
package/src/actions/schemas.ts
CHANGED
|
@@ -4,7 +4,7 @@ import type {
|
|
|
4
4
|
PlanningDefinitions, PlanningProtocols, SchemaDefineReply, SchemaDefineRequest, ScopedSchemaBundle, ScopedSchemaRecord,
|
|
5
5
|
} from '@owlmeans/planning'
|
|
6
6
|
import type { Context, PlanningHandlerOptions } from '../types.js'
|
|
7
|
-
import { assertGranted, concealed, handlerScopeOf } from '../utils/index.js'
|
|
7
|
+
import { assertGranted, assertWrites, concealed, handlerScopeOf } from '../utils/index.js'
|
|
8
8
|
|
|
9
9
|
type RequestHandler = ReturnType<ReturnType<typeof handlers<Context>>['request']>
|
|
10
10
|
|
|
@@ -66,8 +66,9 @@ export const applySchemaRequest = async (
|
|
|
66
66
|
}
|
|
67
67
|
|
|
68
68
|
/**
|
|
69
|
-
* The write of data-defined types and flows.
|
|
70
|
-
*
|
|
69
|
+
* The write of data-defined types and flows. A project layer needs the project in the access's
|
|
70
|
+
* `writes` when the resolver answers it; then `grants.defineSchemas` gates when the resolver answers
|
|
71
|
+
* grants (the organization-wide layer needs `true`, a project layer its id).
|
|
71
72
|
*
|
|
72
73
|
* @throws {PlanningUnsupported} where the store holds no data-defined schemas
|
|
73
74
|
* @throws {PlanningForbidden | SchemaConflict | SchemaSealed | SchemaInvalid | SchemaInUse | WorkcardNotFound}
|
|
@@ -81,6 +82,9 @@ export const defineSchemas = (
|
|
|
81
82
|
if (facade.definitions == null) {
|
|
82
83
|
throw new PlanningUnsupported('definitions')
|
|
83
84
|
}
|
|
85
|
+
if (request.project != null) {
|
|
86
|
+
assertWrites(access, request.project)
|
|
87
|
+
}
|
|
84
88
|
assertGranted(access, 'defineSchemas', request.project)
|
|
85
89
|
|
|
86
90
|
return await applySchemaRequest(facade.definitions, request)
|
package/src/types.ts
CHANGED
|
@@ -58,6 +58,15 @@ export interface PlanningAccess {
|
|
|
58
58
|
entityId: string
|
|
59
59
|
/** Narrows every read and write to these project cards. Every project when omitted. */
|
|
60
60
|
projects?: string[]
|
|
61
|
+
/**
|
|
62
|
+
* The project cards the request may WRITE in: `execute` on a card, specification or link inside
|
|
63
|
+
* one of them (the same rule a narrowed read admits — the project, the cards whose `parents` name
|
|
64
|
+
* it, a specification through its parent card), and `schema.define` of a project layer. A project
|
|
65
|
+
* outside `projects` is still unreachable. Omitted — writes reach every project `projects` admits;
|
|
66
|
+
* present — anything else is refused with `PlanningForbidden` (403). Creating a project is
|
|
67
|
+
* `grants.createProjects`' alone; the organization-wide schema layer is `grants.defineSchemas`'.
|
|
68
|
+
*/
|
|
69
|
+
writes?: string[]
|
|
61
70
|
/**
|
|
62
71
|
* Gates the writes above. Omitted — nothing is gated; present — a flag it leaves out is refused
|
|
63
72
|
* with `PlanningForbidden`.
|
package/src/utils/scope.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { requireEntityKey } from '@owlmeans/auth-common'
|
|
|
2
2
|
import type { BasicConfig, BasicContext } from '@owlmeans/context'
|
|
3
3
|
import type { AbstractRequest } from '@owlmeans/entrypoint'
|
|
4
4
|
import { PLANNING_SERVICE, PlanningForbidden } from '@owlmeans/planning'
|
|
5
|
-
import type { PlanningFacade, PlanningScope, PlanningService, TransitionActor } from '@owlmeans/planning'
|
|
5
|
+
import type { PlanningFacade, PlanningScope, PlanningService, TransitionActor, Workcard } from '@owlmeans/planning'
|
|
6
6
|
import type {
|
|
7
7
|
PlanningAccess, PlanningAccessGrants, PlanningHandlerOptions, PlanningHostService,
|
|
8
8
|
} from '../types.js'
|
|
@@ -108,3 +108,30 @@ export const assertGranted = (
|
|
|
108
108
|
}
|
|
109
109
|
throw new PlanningForbidden(`${grant}:${target ?? 'root'}`)
|
|
110
110
|
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* A card the access's `writes` admits — the rule a narrowed read admits, over `writes`: a project
|
|
114
|
+
* it names, or a card whose `parents` name one. A specification is admitted through its parent card
|
|
115
|
+
* by the caller. No `writes` admits everything.
|
|
116
|
+
*/
|
|
117
|
+
export const writableIn = (access: PlanningAccess | undefined, card: Pick<Workcard, 'id' | 'parents'>): boolean => {
|
|
118
|
+
const writes = access?.writes
|
|
119
|
+
if (writes == null) {
|
|
120
|
+
return true
|
|
121
|
+
}
|
|
122
|
+
return (card.id != null && writes.includes(card.id)) || card.parents.some(parent => writes.includes(parent))
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Refuse a write into a project the access's `writes` leaves out. No `writes` refuses nothing;
|
|
127
|
+
* `target` is the project the write is in (`undefined` for the organization's root, which a set
|
|
128
|
+
* `writes` never admits).
|
|
129
|
+
*
|
|
130
|
+
* @throws {PlanningForbidden}
|
|
131
|
+
*/
|
|
132
|
+
export const assertWrites = (access: PlanningAccess | undefined, target?: string): void => {
|
|
133
|
+
if (access?.writes == null || (target != null && access.writes.includes(target))) {
|
|
134
|
+
return
|
|
135
|
+
}
|
|
136
|
+
throw new PlanningForbidden(`writes:${target ?? 'root'}`)
|
|
137
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test'
|
|
2
|
+
import {
|
|
3
|
+
makePlanningProtocols, PlanningForbidden, SchemaOrigin, SpecificationFormat, TransitionAction, WorkcardKind,
|
|
4
|
+
} from '@owlmeans/planning'
|
|
5
|
+
import type { SchemaDefineReply, ScopedSchemaBundle, Specification } from '@owlmeans/planning'
|
|
6
|
+
import { defineSchemas, executePlanning, getCard, listCards, listSchemas } from '../src/actions/index.js'
|
|
7
|
+
import type { PlanningAccess, PlanningHandlerOptions } from '../src/types.js'
|
|
8
|
+
import { createProject, createTask, ENTITY, invoke, makeTestPlanning, PROJECT, session, TASK } from './context.js'
|
|
9
|
+
|
|
10
|
+
const tree = makePlanningProtocols({ base: { alias: 'test:planning' }, guards: 'test-guard', definitions: true })
|
|
11
|
+
|
|
12
|
+
const withAccess = (access: PlanningAccess): PlanningHandlerOptions => ({ access: async () => access })
|
|
13
|
+
|
|
14
|
+
const flow = { id: 'test:rota', version: 1, statuses: [{ key: 'due', intrinsic: 'planned' }], transitions: [] }
|
|
15
|
+
|
|
16
|
+
/** Two projects: the caller views both and writes only in `written`. */
|
|
17
|
+
const setup = async () => {
|
|
18
|
+
const planning = await makeTestPlanning({}, { schemas: true })
|
|
19
|
+
const { facade } = planning
|
|
20
|
+
const viewed = await createProject(facade(), 'Viewed')
|
|
21
|
+
const written = await createProject(facade(), 'Written')
|
|
22
|
+
const viewedTask = (await createTask(facade(), viewed.id!, 'Viewed task')).card!
|
|
23
|
+
const writtenTask = (await createTask(facade(), written.id!, 'Written task')).card!
|
|
24
|
+
const access: PlanningAccess = { entityId: ENTITY, projects: [viewed.id!, written.id!], writes: [written.id!] }
|
|
25
|
+
|
|
26
|
+
return { ...planning, viewed, written, viewedTask, writtenTask, access, opts: withAccess(access) }
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
describe('@owlmeans/server-planning — per-project writes', () => {
|
|
30
|
+
test('a viewer reads a project it may not write in', async () => {
|
|
31
|
+
const { context, viewed, viewedTask, opts } = await setup()
|
|
32
|
+
|
|
33
|
+
expect((await invoke(getCard(tree.card.get, opts), context, { ...session(), params: { id: viewedTask.id } })).id).toBe(viewedTask.id)
|
|
34
|
+
const listed = await invoke(listCards(tree.card.list, opts), context, { ...session(), query: { parent: viewed.id } })
|
|
35
|
+
expect(listed.items.map((card: { id: string }) => card.id)).toEqual([viewedTask.id])
|
|
36
|
+
const bundle: ScopedSchemaBundle = await invoke(listSchemas(tree.schema.list), context, { ...session(), query: { project: viewed.id } })
|
|
37
|
+
expect(bundle.types.some(type => type.type === TASK)).toBe(true)
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
test('a viewer\'s execute answers 403 on a card, a project, a create and a link; nothing is appended', async () => {
|
|
41
|
+
const { context, facade, viewed, viewedTask, writtenTask, opts } = await setup()
|
|
42
|
+
const execute = executePlanning(tree.execute, opts)
|
|
43
|
+
const refused = async (body: Record<string, unknown>) =>
|
|
44
|
+
await expect(invoke(execute, context, { ...session(), body })).rejects.toBeInstanceOf(PlanningForbidden)
|
|
45
|
+
|
|
46
|
+
expect(PlanningForbidden.httpStatus).toBe(403)
|
|
47
|
+
await refused({ card: viewedTask.id, action: TransitionAction.Update, changes: { title: 'Renamed' } })
|
|
48
|
+
await refused({ card: viewedTask.id, action: TransitionAction.Transit, transition: 'start' })
|
|
49
|
+
await refused({ card: viewed.id, action: TransitionAction.Update, changes: { title: 'Renamed' } })
|
|
50
|
+
await refused({ card: viewed.id, action: TransitionAction.Delete })
|
|
51
|
+
await refused({ card: { kind: WorkcardKind.Card, type: TASK, parent: viewed.id, title: 'Sneaky' }, action: TransitionAction.Create })
|
|
52
|
+
await refused({ card: { kind: WorkcardKind.Card, type: TASK, parent: writtenTask.id, parents: [viewed.id], title: 'Sneaky' }, action: TransitionAction.Create })
|
|
53
|
+
await refused({ card: viewedTask.id, action: TransitionAction.Link, link: { type: 'blocks', to: writtenTask.id } })
|
|
54
|
+
|
|
55
|
+
expect((await facade().cards.get(viewedTask.id!)).title).toBe('Viewed task')
|
|
56
|
+
expect((await facade().cards.get(viewed.id!)).title).toBe('Viewed')
|
|
57
|
+
expect((await facade().cards.list({ parent: viewed.id })).items.map(card => card.id)).toEqual([viewedTask.id])
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
test('a specification is written through its parent card', async () => {
|
|
61
|
+
const { context, facade, viewedTask, writtenTask, opts } = await setup()
|
|
62
|
+
const execute = executePlanning(tree.execute, opts)
|
|
63
|
+
const design = async (task: string) => ((await (await facade().model(await facade().cards.get(task)))
|
|
64
|
+
.write('design', '{"v":1}', { wait: true })).card as Specification)
|
|
65
|
+
const viewedDesign = await design(viewedTask.id!)
|
|
66
|
+
const writtenDesign = await design(writtenTask.id!)
|
|
67
|
+
|
|
68
|
+
await expect(invoke(execute, context, {
|
|
69
|
+
...session(), body: { card: viewedDesign.id, action: TransitionAction.Update, changes: { body: '{"v":2}' } },
|
|
70
|
+
})).rejects.toBeInstanceOf(PlanningForbidden)
|
|
71
|
+
await expect(invoke(execute, context, {
|
|
72
|
+
...session(), body: {
|
|
73
|
+
card: { kind: WorkcardKind.Specification, type: viewedDesign.type, parent: viewedTask.id, category: 'design', format: SpecificationFormat.Json, title: 'd', body: '{}' },
|
|
74
|
+
action: TransitionAction.Create,
|
|
75
|
+
},
|
|
76
|
+
})).rejects.toBeInstanceOf(PlanningForbidden)
|
|
77
|
+
const updated = await invoke(execute, context, {
|
|
78
|
+
...session(), body: { card: writtenDesign.id, action: TransitionAction.Update, changes: { body: '{"v":2}' }, wait: true },
|
|
79
|
+
})
|
|
80
|
+
expect(updated.card.body).toBe('{"v":2}')
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
test('a writer writes in its project: create, transit, link, and its project layer', async () => {
|
|
84
|
+
const { context, facade, written, writtenTask, viewedTask, opts } = await setup()
|
|
85
|
+
const execute = executePlanning(tree.execute, opts)
|
|
86
|
+
|
|
87
|
+
const made = await invoke(execute, context, {
|
|
88
|
+
...session(), body: { card: { kind: WorkcardKind.Card, type: TASK, parent: written.id, title: 'Allowed' }, action: TransitionAction.Create, wait: true },
|
|
89
|
+
})
|
|
90
|
+
expect(made.card.parent).toBe(written.id)
|
|
91
|
+
await invoke(execute, context, { ...session(), body: { card: writtenTask.id, action: TransitionAction.Transit, transition: 'start', wait: true } })
|
|
92
|
+
expect((await facade().cards.get(writtenTask.id!)).status).toBe('doing')
|
|
93
|
+
// A link FROM a writable card may point at a card the caller only views.
|
|
94
|
+
await invoke(execute, context, {
|
|
95
|
+
...session(), body: { card: writtenTask.id, action: TransitionAction.Link, link: { type: 'blocks', to: viewedTask.id }, wait: true },
|
|
96
|
+
})
|
|
97
|
+
await invoke(execute, context, { ...session(), body: { card: written.id, action: TransitionAction.Update, changes: { title: 'Renamed' }, wait: true } })
|
|
98
|
+
expect((await facade().cards.get(written.id!)).title).toBe('Renamed')
|
|
99
|
+
|
|
100
|
+
const layer: SchemaDefineReply = await invoke(defineSchemas(tree.schema.define!, opts), context, {
|
|
101
|
+
...session(), body: { project: written.id, flows: [flow] },
|
|
102
|
+
})
|
|
103
|
+
expect(layer.bundle.origins?.flows['test:rota']).toBe(SchemaOrigin.Project)
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
test('a viewer\'s project-layer schema define answers 403; writes never reach the root', async () => {
|
|
107
|
+
const { context, viewed, access, opts } = await setup()
|
|
108
|
+
|
|
109
|
+
await expect(invoke(defineSchemas(tree.schema.define!, opts), context, {
|
|
110
|
+
...session(), body: { project: viewed.id, flows: [flow] },
|
|
111
|
+
})).rejects.toBeInstanceOf(PlanningForbidden)
|
|
112
|
+
// `writes` does not gate the organization layer (that is `grants.defineSchemas`), nor a project create.
|
|
113
|
+
const project = await invoke(executePlanning(tree.execute, opts), context, {
|
|
114
|
+
...session(), body: { card: { kind: WorkcardKind.Project, type: PROJECT, title: 'New' }, action: TransitionAction.Create, wait: true },
|
|
115
|
+
})
|
|
116
|
+
expect(project.card.kind).toBe(WorkcardKind.Project)
|
|
117
|
+
await expect(invoke(defineSchemas(tree.schema.define!, withAccess({ ...access, grants: { defineSchemas: [viewed.id!] } })), context, {
|
|
118
|
+
...session(), body: { project: viewed.id, flows: [flow] },
|
|
119
|
+
})).rejects.toBeInstanceOf(PlanningForbidden)
|
|
120
|
+
// A project create stays `createProjects`' decision when grants are present.
|
|
121
|
+
await expect(invoke(executePlanning(tree.execute, withAccess({ ...access, grants: {} })), context, {
|
|
122
|
+
...session(), body: { card: { kind: WorkcardKind.Project, type: PROJECT, title: 'Refused' }, action: TransitionAction.Create },
|
|
123
|
+
})).rejects.toBeInstanceOf(PlanningForbidden)
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
test('absent writes keep the old behaviour: every visible project is writable', async () => {
|
|
127
|
+
const { context, facade, viewed, viewedTask, access } = await setup()
|
|
128
|
+
const { writes: _writes, ...open } = access
|
|
129
|
+
const opts = withAccess(open)
|
|
130
|
+
|
|
131
|
+
await invoke(executePlanning(tree.execute, opts), context, {
|
|
132
|
+
...session(), body: { card: viewedTask.id, action: TransitionAction.Update, changes: { title: 'Renamed' }, wait: true },
|
|
133
|
+
})
|
|
134
|
+
expect((await facade().cards.get(viewedTask.id!)).title).toBe('Renamed')
|
|
135
|
+
const layer: SchemaDefineReply = await invoke(defineSchemas(tree.schema.define!, opts), context, {
|
|
136
|
+
...session(), body: { project: viewed.id, flows: [flow] },
|
|
137
|
+
})
|
|
138
|
+
expect(layer.records.map(record => record.key)).toEqual(['test:rota'])
|
|
139
|
+
})
|
|
140
|
+
})
|