@owlmeans/server-planning 0.1.18-rc.18 → 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 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.18 @owlmeans/planning@^0.1.18-rc.15 ajv
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
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "schemaVersion": 2,
3
3
  "package": "@owlmeans/server-planning",
4
- "version": "0.1.18-rc.18",
5
- "generatedAt": "2026-10-03T11:19:50.770Z",
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.18"` in `dependencies` (`ajv` is a peer)
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,7 +30,7 @@ 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
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 |
@@ -296,7 +296,7 @@ mapping external records (`mappers`) belongs to that plugin's own store adapter.
296
296
 
297
297
  ### The access resolver
298
298
 
299
- `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
300
300
  decision per request; a throw is the request's answer (an `AuthForbidden` answers 403).
301
301
 
302
302
  - `entityId` is the resolver's — never the token's, never `opts.scope`'s.
@@ -316,12 +316,45 @@ decision per request; a throw is the request's answer (an `AuthForbidden` answer
316
316
  shed) is expressed here: the resolver answers the project ids the person's grants list as
317
317
  `projects`, and omits the key when a grant covers every project — never a hand-written filter in
318
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.
319
329
  - `grants` gates the stock handlers' writes: `createProjects` (a project create — `true` for the
320
330
  root, a list names the parent projects), `deleteProjects` (the delete of a project card — a list
321
331
  names the projects), `defineSchemas` (`schema.define` — `true` for the organization's layer, a
322
332
  list names project layers). A `grants` object refuses every flag it leaves out with
323
333
  `PlanningForbidden` (403); no `grants` gates nothing. In-process facade writes are not gated.
324
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
+
325
358
  ## Data-defined types and flows
326
359
 
327
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
- * The grants a wire execution needs: `createProjects` for a project create (its primary parent,
16
- * or the root), `deleteProjects` for the delete of a project card. A card this scope cannot see is
17
- * left to the executor, which answers it as absent.
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;AAI/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;;;;;;GAMG;AACH,eAAO,MAAM,sBAAsB,WACzB,cAAc,QAAQ,mBAAmB,WAAW,cAAc,KACzE,OAAO,CAAC,IAAI,CAcd,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"}
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"}
@@ -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
- * The grants a wire execution needs: `createProjects` for a project create (its primary parent,
31
- * or the root), `deleteProjects` for the delete of a project card. A card this scope cannot see is
32
- * left to the executor, which answers it as absent.
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":"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"}
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. Gated by `grants.defineSchemas` when the access
22
- * resolver answers grants (the organization-wide layer needs `true`, a project layer its id).
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;;;;;;GAMG;AACH,eAAO,MAAM,aAAa,aACd,WAAW,CAAC,iBAAiB,CAAC,QAAQ,CAAC,CAAC,QAAQ,CAAC,CAAC,SAAS,sBAAsB,KAC1F,cAUE,CAAA"}
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"}
@@ -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. Gated by `grants.defineSchemas` when the access
51
- * resolver answers grants (the organization-wide layer needs `true`, a project layer its id).
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":"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"}
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`.
@@ -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"}
@@ -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;AACzG,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"}
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"}
@@ -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
@@ -1 +1 @@
1
- {"version":3,"file":"scope.js","sourceRoot":"","sources":["../../src/utils/scope.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAA;AAGxD,OAAO,EAAE,gBAAgB,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAA;AAMxE,MAAM,KAAK,GAAG,CAAmB,MAAS,EAAK,EAAE,CAC/C,MAAM,CAAC,WAAW,CAAC,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,KAAK,CAAC,EAAE,EAAE,CAAC,KAAK,IAAI,IAAI,IAAI,KAAK,KAAK,EAAE,CAAC,CAAM,CAAA;AAEtG,wEAAwE;AACxE,MAAM,CAAC,MAAM,OAAO,GAAG,CAAC,GAAoB,EAAmB,EAAE,CAAC,KAAK,CAAC;IACtE,SAAS,EAAE,GAAG,CAAC,IAAI,EAAE,SAAS;IAC9B,MAAM,EAAE,GAAG,CAAC,IAAI,EAAE,MAAM;CACzB,CAAC,CAAA;AAEF,MAAM,SAAS,GAAG,CAAC,GAAoB,EAAE,QAAgB,EAAE,KAA8B,EAAiB,EAAE;IAC1G,MAAM,OAAO,GAAG,OAAO,CAAC,GAAG,CAAC,CAAA;IAC5B,MAAM,OAAO,GAAG,KAAK,EAAE,OAAO,IAAI,KAAK,EAAE,KAAK,EAAE,OAAO,CAAA;IAEvD,OAAO,KAAK,CAAC;QACX,GAAG,KAAK;QACR,GAAG,OAAO;QACV,QAAQ;QACR,OAAO;QACP,KAAK,EAAE,KAAK,CAAC,EAAE,GAAG,KAAK,EAAE,KAAK,EAAE,GAAG,OAAO,EAAE,OAAO,EAAE,CAAC;KACvD,CAAkB,CAAA;AACrB,CAAC,CAAA;AAED;;;;;;;;GAQG;AACH,MAAM,CAAC,MAAM,OAAO,GAAG,CAAC,GAAoB,EAAE,KAA8B,EAAiB,EAAE,CAC7F,SAAS,CAAC,GAAG,EAAE,gBAAgB,CAAC,GAAG,CAAC,EAAE,KAAK,CAAC,CAAA;AAE9C;;;GAGG;AACH,MAAM,CAAC,MAAM,aAAa,GAAG,CAC3B,GAAoB,EAAE,MAAsB,EAAE,KAA8B,EAC7D,EAAE;IACjB,MAAM,EAAE,QAAQ,EAAE,SAAS,EAAE,QAAQ,EAAE,SAAS,EAAE,GAAG,IAAI,EAAE,GAAG,KAAK,IAAI,EAAE,CAAA;IACzE,MAAM,KAAK,GAAG,SAAS,CAAC,GAAG,EAAE,MAAM,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAA;IAEnD,OAAO,MAAM,CAAC,QAAQ,IAAI,IAAI,CAAC,CAAC,CAAC,EAAE,GAAG,KAAK,EAAE,QAAQ,EAAE,CAAC,GAAG,MAAM,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC,CAAC,KAAK,CAAA;AACvF,CAAC,CAAA;AAED,MAAM,CAAC,MAAM,iBAAiB,GAAG,CAC/B,GAA8B,EAAE,IAA8C,EAC7D,EAAE,CAAC,GAAG,CAAC,OAAO,CAAsB,IAAI,EAAE,OAAO,IAAI,gBAAgB,CAAC,CAAA;AAQzF;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,cAAc,GAAG,KAAK,EACjC,GAA8B,EAAE,GAAoB,EAAE,IAA6B,EAC5D,EAAE;IACzB,MAAM,KAAK,GAAG,MAAM,IAAI,EAAE,KAAK,EAAE,CAAC,GAAG,EAAE,GAAG,CAAC,CAAA;IAC3C,IAAI,IAAI,EAAE,MAAM,IAAI,IAAI,EAAE,CAAC;QACzB,OAAO,EAAE,MAAM,EAAE,iBAAiB,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,GAAG,EAAE,KAAK,IAAI,SAAS,CAAC,CAAC,EAAE,CAAA;IACvF,CAAC;IACD,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,MAAM,CAAC,GAAG,EAAE,GAAG,CAAC,CAAA;IAE1C,OAAO,EAAE,MAAM,EAAE,iBAAiB,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC,GAAG,CAAC,aAAa,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,IAAI,SAAS,CAAC,CAAC,EAAE,MAAM,EAAE,CAAA;AAC7G,CAAC,CAAA;AAED,8FAA8F;AAC9F,MAAM,CAAC,MAAM,aAAa,GAAG,KAAK,EAChC,GAA8B,EAAE,GAAoB,EAAE,IAA6B,EAC1D,EAAE,CAAC,CAAC,MAAM,cAAc,CAAC,GAAG,EAAE,GAAG,EAAE,IAAI,CAAC,CAAC,CAAC,MAAM,CAAA;AAE3E;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,aAAa,GAAG,CAC3B,MAAkC,EAAE,KAAiC,EAAE,MAAe,EAChF,EAAE;IACR,IAAI,MAAM,EAAE,MAAM,IAAI,IAAI,EAAE,CAAC;QAC3B,OAAM;IACR,CAAC;IACD,MAAM,OAAO,GAAG,MAAM,CAAC,MAAM,CAAC,KAAK,CAAC,CAAA;IACpC,IAAI,OAAO,KAAK,IAAI,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,OAAO,CAAC,IAAI,MAAM,IAAI,IAAI,IAAI,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,EAAE,CAAC;QAC/F,OAAM;IACR,CAAC;IACD,MAAM,IAAI,iBAAiB,CAAC,GAAG,KAAK,IAAI,MAAM,IAAI,MAAM,EAAE,CAAC,CAAA;AAC7D,CAAC,CAAA"}
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.18",
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",
@@ -50,7 +50,7 @@
50
50
  "@owlmeans/context": "^0.1.18-rc.32",
51
51
  "@owlmeans/entrypoint": "^0.1.18-rc.39",
52
52
  "@owlmeans/error": "^0.1.18-rc.36",
53
- "@owlmeans/planning": "^0.1.18-rc.15",
53
+ "@owlmeans/planning": "^0.1.18-rc.16",
54
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",
@@ -1,14 +1,15 @@
1
1
  import { handlers } from '@owlmeans/server-api'
2
2
  import {
3
- DEFAULT_COMMIT_TIMEOUT, isProject, MAX_COMMIT_POLL, normalizeParents, TransitionAction, WorkcardKind,
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
- * The grants a wire execution needs: `createProjects` for a project create (its primary parent,
41
- * or the root), `deleteProjects` for the delete of a project card. A card this scope cannot see is
42
- * left to the executor, which answers it as absent.
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
  }
@@ -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. Gated by `grants.defineSchemas` when the access
70
- * resolver answers grants (the organization-wide layer needs `true`, a project layer its id).
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`.
@@ -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
+ })