@anchrd/intel-contract 0.19.0 → 0.21.0

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
@@ -95,10 +95,15 @@ edge and the tool ends up in the chain. The graph is then refused with
95
95
  `Node <id> is a link and must be attached to a step` — in `title`, per the first item above. A step
96
96
  may hold several links; one link belongs to exactly one step.
97
97
 
98
- ### A flow is not a node, and carries no grants of its own
98
+ ### A flow is not a node, and has its own grant calls
99
99
 
100
- `node_grant_create` addresses nodes. A flow has no grants; it inherits those of the folder it lives
101
- in. To change who reaches a flow, share its folder or move the flow.
100
+ `node_grant_create` addresses nodes; `flow_grant_create` addresses flows. A flow is reached by both:
101
+ the folder it is filed in passes its grants down, and a grant may sit on the flow itself. The two
102
+ add up — the flow grant never replaces what the folder passes down.
103
+
104
+ ⚠️ A grant on one flow reaches **that flow** and nothing else — not the folder, and not the flows it
105
+ calls. `ShareResult` says so: `unrunnable` names the sub-flows the new principal could not start,
106
+ by title where you may see them and by count where you may not.
102
107
 
103
108
  ### Import takes its idempotency key as a header
104
109
 
@@ -24,6 +24,19 @@ export declare const BundleEntryKind: z.ZodEnum<{
24
24
  flow: "flow";
25
25
  }>;
26
26
  export type BundleEntryKind = z.infer<typeof BundleEntryKind>;
27
+ /**
28
+ * Why an entry carries no bytes although its kind normally does (#534).
29
+ *
30
+ * `no-content` is a node that has never been given any: a table created but never defined has no
31
+ * version, and an attachment created but never uploaded has none either, so there is nothing to
32
+ * write. That is a different sentence from "the content is gone", which the export refuses as
33
+ * `content_missing` — and a file of zero bytes cannot say which of the two it is, which is exactly
34
+ * what it used to do.
35
+ */
36
+ export declare const BundleEntryAbsence: z.ZodEnum<{
37
+ "no-content": "no-content";
38
+ }>;
39
+ export type BundleEntryAbsence = z.infer<typeof BundleEntryAbsence>;
27
40
  export declare const BundleManifestEntry: z.ZodObject<{
28
41
  id: z.ZodString;
29
42
  kind: z.ZodEnum<{
@@ -39,6 +52,9 @@ export declare const BundleManifestEntry: z.ZodObject<{
39
52
  description: z.ZodNullable<z.ZodString>;
40
53
  mediaType: z.ZodNullable<z.ZodString>;
41
54
  path: z.ZodString;
55
+ absence: z.ZodOptional<z.ZodEnum<{
56
+ "no-content": "no-content";
57
+ }>>;
42
58
  }, z.core.$strict>;
43
59
  export type BundleManifestEntry = z.infer<typeof BundleManifestEntry>;
44
60
  export declare const BundleExclusion: z.ZodEnum<{
@@ -67,6 +83,9 @@ export declare const BundleManifest: z.ZodObject<{
67
83
  description: z.ZodNullable<z.ZodString>;
68
84
  mediaType: z.ZodNullable<z.ZodString>;
69
85
  path: z.ZodString;
86
+ absence: z.ZodOptional<z.ZodEnum<{
87
+ "no-content": "no-content";
88
+ }>>;
70
89
  }, z.core.$strict>>;
71
90
  excluded: z.ZodArray<z.ZodEnum<{
72
91
  "version-history": "version-history";
@@ -27,17 +27,57 @@ export const BundleEntryKind = z.enum([
27
27
  "board",
28
28
  "flow",
29
29
  ]);
30
+ /**
31
+ * Why an entry carries no bytes although its kind normally does (#534).
32
+ *
33
+ * `no-content` is a node that has never been given any: a table created but never defined has no
34
+ * version, and an attachment created but never uploaded has none either, so there is nothing to
35
+ * write. That is a different sentence from "the content is gone", which the export refuses as
36
+ * `content_missing` — and a file of zero bytes cannot say which of the two it is, which is exactly
37
+ * what it used to do.
38
+ */
39
+ export const BundleEntryAbsence = z.enum(["no-content"]);
30
40
  // One entry of the manifest: the identity a re-import needs, next to the relative path where the
31
41
  // bytes sit in the zip. A folder carries no media type — it has no bytes.
32
- export const BundleManifestEntry = z.strictObject({
42
+ export const BundleManifestEntry = z
43
+ .strictObject({
33
44
  id: IntelId,
34
45
  kind: BundleEntryKind,
35
46
  title: z.string().min(1).max(240),
36
47
  description: z.string().max(2_000).nullable(),
37
48
  mediaType: z.string().min(1).max(160).nullable(),
38
49
  // Relative to the zip root, forward slashes, no leading slash. Folders end with a slash so an
39
- // empty folder still has an address.
50
+ // empty folder still has an address. An entry with an `absence` keeps its path even though no
51
+ // file sits there: the path is what puts the node in the tree on the way back in.
40
52
  path: z.string().min(1).max(4_000),
53
+ // ⚠️ OPTIONAL rather than nullable, and that is about files that already exist. This object is
54
+ // strict, so a required key would refuse every bundle written before #534 with
55
+ // `import_invalid_manifest` — a whole format broken over a word that only ever describes a node
56
+ // with nothing in it. Absent means the entry has its file, which is what every older bundle
57
+ // means.
58
+ absence: BundleEntryAbsence.optional(),
59
+ })
60
+ /**
61
+ * ⚠️ Only a table or an attachment may declare an absence, and the guard belongs HERE rather
62
+ * than at the import.
63
+ *
64
+ * A bundle is somebody else's file, and the import reads this word as permission to skip the
65
+ * check that a manifest entry has its file in the zip. Left unrestricted, an entry saying
66
+ * `kind: "document"` with `absence: "no-content"` would walk past that check even when the file
67
+ * IS in the zip — and the document would be created empty, silently, with nothing refused.
68
+ *
69
+ * The two kinds named here are the ones whose canonical content is the whole of them: a table
70
+ * IS its CSV and an attachment IS its bytes, so for them "no content" is a state of the node
71
+ * rather than a claim about a file. A document without a version is a different case and stays
72
+ * out — an empty markdown file says the same thing about it without ambiguity (#534, #560).
73
+ *
74
+ * Refusing at the boundary is the difference between a named refusal and lost content: the
75
+ * import validates this schema before it plans anything, so a manifest like that fails as
76
+ * `import_invalid_manifest` instead of importing something the bundle does not say.
77
+ */
78
+ .refine((entry) => entry.absence === undefined || entry.kind === "table" || entry.kind === "attachment", {
79
+ message: "Only a table or attachment entry may declare an absence",
80
+ path: ["absence"],
41
81
  });
42
82
  // What an export leaves out on purpose, named so a bundle says it rather than a reader guessing:
43
83
  // version history, grants/shares, flow runs, and archived nodes are not in any bundle (#136).
@@ -2,6 +2,40 @@ import { z } from "zod";
2
2
  export declare const IntelId: z.ZodString;
3
3
  export declare const IsoDateTime: z.ZodISODateTime;
4
4
  export declare const IdempotencyKey: z.ZodString;
5
+ /**
6
+ * The version a write is made against, in the one form every writer of versioned content takes it.
7
+ *
8
+ * ⚠️ It lived beside the two node writers until #459 and moved here when the flow graph became the
9
+ * third — the point at which the repository stops copying and names the thing. It could not simply
10
+ * be imported: `flow.ts` may not depend on `node.ts` (the graph is `table → node`, `flow-run →
11
+ * flow`, and everything → here), and a copy would have been a second place for one sentence to
12
+ * drift, which is exactly what `IdempotencyKey` above exists to avoid.
13
+ *
14
+ * ⚠️ Nullable and required are not the same thing, and here they stand together (#437). `null` is
15
+ * how something that has no version yet is addressed, and it is the only way to write its first
16
+ * content; leaving the field out says nothing at all and is refused. Whoever leaves it out is
17
+ * almost always holding a node or a flow created one call earlier, and the plain type error they
18
+ * used to get — "expected string, received undefined" — reads as "this field must be an id", about
19
+ * a field that cannot have one yet. Both refusals therefore name both ways instead.
20
+ *
21
+ * ⚠️ It stays required once a version exists, and that is what it is for: a mismatch is answered
22
+ * with a `409` rather than by overwriting somebody else's newer version. Making it `optional()` to
23
+ * smooth the first write is the repair that breaks the guard — an absent field would then have to
24
+ * mean "there is none", and the forgetful caller silently replaces the newest one.
25
+ *
26
+ * ⚠️ The bounds remain `IntelId`'s. The repeated `min(1)` adds no rule — it only puts the sentence
27
+ * on the second value that gets tried after `undefined`, the empty string.
28
+ *
29
+ * ⚠️ The sentence says what the field TAKES, not what the caller did wrong. It answers a wrong type
30
+ * as well as an absent field, and "is required" would be false about the first — which would be
31
+ * this same ticket one corner further on. A value that is merely too long keeps Zod's own `Too
32
+ * big`: that one is already a true statement about what was sent.
33
+ *
34
+ * ⚠️ The JSON Schema it produces is the one `IntelId.nullable()` produced — an `anyOf` of the
35
+ * bounded string and `null`, still `required` — so nothing on the MCP surface moves when a field
36
+ * changes over to it.
37
+ */
38
+ export declare function baseVersion(rule: string): z.ZodUnion<readonly [z.ZodString, z.ZodNull]>;
5
39
  export declare const UI_LANGUAGES: readonly ["en", "de", "es"];
6
40
  export type UiLanguage = (typeof UI_LANGUAGES)[number];
7
41
  export declare const ProblemDetails: z.ZodObject<{
@@ -13,6 +13,42 @@ export const IdempotencyKey = z
13
13
  .min(8)
14
14
  .max(200)
15
15
  .describe("A caller-chosen key that makes this call safe to retry: repeating the same key returns the first result instead of doing the work twice. Reuse the SAME key when retrying one attempt, and a new one for a genuinely new call.");
16
+ /**
17
+ * The version a write is made against, in the one form every writer of versioned content takes it.
18
+ *
19
+ * ⚠️ It lived beside the two node writers until #459 and moved here when the flow graph became the
20
+ * third — the point at which the repository stops copying and names the thing. It could not simply
21
+ * be imported: `flow.ts` may not depend on `node.ts` (the graph is `table → node`, `flow-run →
22
+ * flow`, and everything → here), and a copy would have been a second place for one sentence to
23
+ * drift, which is exactly what `IdempotencyKey` above exists to avoid.
24
+ *
25
+ * ⚠️ Nullable and required are not the same thing, and here they stand together (#437). `null` is
26
+ * how something that has no version yet is addressed, and it is the only way to write its first
27
+ * content; leaving the field out says nothing at all and is refused. Whoever leaves it out is
28
+ * almost always holding a node or a flow created one call earlier, and the plain type error they
29
+ * used to get — "expected string, received undefined" — reads as "this field must be an id", about
30
+ * a field that cannot have one yet. Both refusals therefore name both ways instead.
31
+ *
32
+ * ⚠️ It stays required once a version exists, and that is what it is for: a mismatch is answered
33
+ * with a `409` rather than by overwriting somebody else's newer version. Making it `optional()` to
34
+ * smooth the first write is the repair that breaks the guard — an absent field would then have to
35
+ * mean "there is none", and the forgetful caller silently replaces the newest one.
36
+ *
37
+ * ⚠️ The bounds remain `IntelId`'s. The repeated `min(1)` adds no rule — it only puts the sentence
38
+ * on the second value that gets tried after `undefined`, the empty string.
39
+ *
40
+ * ⚠️ The sentence says what the field TAKES, not what the caller did wrong. It answers a wrong type
41
+ * as well as an absent field, and "is required" would be false about the first — which would be
42
+ * this same ticket one corner further on. A value that is merely too long keeps Zod's own `Too
43
+ * big`: that one is already a true statement about what was sent.
44
+ *
45
+ * ⚠️ The JSON Schema it produces is the one `IntelId.nullable()` produced — an `anyOf` of the
46
+ * bounded string and `null`, still `required` — so nothing on the MCP surface moves when a field
47
+ * changes over to it.
48
+ */
49
+ export function baseVersion(rule) {
50
+ return z.union([IntelId.min(1, { error: rule }), z.null()], { error: rule });
51
+ }
16
52
  // The languages whose UI catalog ships inside intel-ui. They live here rather than only in the UI
17
53
  // because `intel build` has to know them: a built-in language may be chosen as `ui.defaultLanguage`
18
54
  // without the customer listing a copy under `ui.languages` that would rot at every UI update.
@@ -672,6 +672,7 @@ export declare const FlowRequirements: z.ZodObject<{
672
672
  id: z.ZodString;
673
673
  title: z.ZodString;
674
674
  }, z.core.$strict>>;
675
+ invalidNodes: z.ZodArray<z.ZodString>;
675
676
  hiddenNodes: z.ZodNumber;
676
677
  servers: z.ZodArray<z.ZodString>;
677
678
  }, z.core.$strict>;
@@ -730,7 +731,7 @@ export declare const GetFlowInput: z.ZodObject<{
730
731
  export type GetFlowInput = z.infer<typeof GetFlowInput>;
731
732
  export declare const SaveFlowVersionInput: z.ZodObject<{
732
733
  flowId: z.ZodString;
733
- baseVersionId: z.ZodNullable<z.ZodString>;
734
+ baseVersionId: z.ZodUnion<readonly [z.ZodString, z.ZodNull]>;
734
735
  graph: z.ZodObject<{
735
736
  nodes: z.ZodArray<z.ZodDiscriminatedUnion<[z.ZodObject<{
736
737
  kind: z.ZodLiteral<"trigger">;
@@ -1,5 +1,5 @@
1
1
  import { z } from "zod";
2
- import { IdempotencyKey, IntelId, IsoDateTime, LinkConfiguration } from "./contract.js";
2
+ import { baseVersion, IdempotencyKey, IntelId, IsoDateTime, LinkConfiguration, } from "./contract.js";
3
3
  import { serverOf, ToolName, ToolServerHandle } from "./tool.js";
4
4
  export const FlowNodeId = z.string().regex(/^[A-Za-z0-9][A-Za-z0-9_-]{0,99}$/);
5
5
  export const FlowPosition = z.strictObject({ x: z.number().finite(), y: z.number().finite() });
@@ -259,11 +259,24 @@ export const ReferencedNode = z.strictObject({
259
259
  // (ADR-0004 §4). For tools it can only ever be the latter, because the catalog is a live query with
260
260
  // the requesting user's own token (ADR-0003).
261
261
  //
262
- // ⚠️ `nodes` names only what the asking user may see. The rest is `hiddenNodes`, a count.
262
+ // ⚠️ `nodes` names only what the asking user may see. The rest falls into TWO fields, and keeping
263
+ // them apart is the whole of #509: `invalidNodes` are references that name nothing at all any more,
264
+ // `hiddenNodes` is a count of the ones that exist and are out of this reader's reach.
265
+ //
266
+ // ⚠️ They used to be one number, and the number was read out as "you cannot see it" — a sentence
267
+ // about a PERMISSION, in front of a broken link. The two states suggest opposite actions and only
268
+ // one of them helps: with a permission one asks for access and waits, with a dead reference the step
269
+ // has to be replaced before the flow can run at all. #492 decided that such a reference is marked
270
+ // visibly invalid rather than left silently pointed at an id nothing answers.
271
+ //
272
+ // ⚠️ `invalidNodes` carries IDS, never titles, and that is not the same concession `hiddenNodes`
273
+ // makes. There is nothing left to name — the row is gone — and the id is one the caller sent us out
274
+ // of a graph they may already read, so it discloses nothing they did not write themselves.
263
275
  export const FlowRequirements = z.strictObject({
264
276
  flowId: IntelId,
265
277
  versionId: IntelId.nullable(),
266
278
  nodes: z.array(ReferencedNode),
279
+ invalidNodes: z.array(IntelId),
267
280
  hiddenNodes: z.number().int().nonnegative(),
268
281
  servers: z.array(ToolServerHandle),
269
282
  });
@@ -375,7 +388,12 @@ export const GetFlowInput = z.strictObject({
375
388
  });
376
389
  export const SaveFlowVersionInput = z.strictObject({
377
390
  flowId: IntelId.describe("Flow to append a version to."),
378
- baseVersionId: IntelId.nullable().describe("The version this edit was made against, from flow_get, or `null` for a flow that has none yet. If the flow has moved on since, the call is refused rather than overwriting the newer version."),
391
+ // ⚠️ `baseVersion`, not `IntelId.nullable()` (#459). The field is required-nullable either way;
392
+ // what changes is what a caller who left it out is told. Zod's own `expected string, received
393
+ // undefined` reads as "this needs an id" to somebody holding a flow created one call earlier —
394
+ // whose `currentVersionId` is `null` and can be nothing else. That reading cost a session at
395
+ // #437, and this was the third field carrying it.
396
+ baseVersionId: baseVersion("baseVersionId takes the `currentVersionId` from flow_get, or null when the flow has no version yet — and it has to be sent.").describe("The version this edit was made against, from flow_get, or `null` for a flow that has none yet. If the flow has moved on since, the call is refused rather than overwriting the newer version."),
379
397
  graph: FlowGraph.describe("The complete graph — nodes and edges — not a patch. It is validated before it is stored, so an unreachable node or a dangling edge is refused here rather than at run time."),
380
398
  idempotencyKey: IdempotencyKey,
381
399
  });
@@ -438,9 +456,9 @@ export const FlowPublishPreview = z.strictObject({
438
456
  calls: z.array(FlowPublishCall),
439
457
  tools: z.array(FlowPublishTool),
440
458
  });
441
- // A flow has no share schema of its own. A grant sits on the folder a flow is filed in and inherits
442
- // down from there (ADR-0004 §2); a narrower grant beside it would destroy the subtree guarantee
443
- // section 3 rests on, so per-flow grants were removed rather than deprecated.
459
+ // A flow's own sharing schemas live in `share.ts` beside the node's (#530): `ShareFlowInput`,
460
+ // `RevokeFlowGrantInput`, `ListFlowGrantsInput`. They are there rather than here because a grant is
461
+ // one subject with two kinds of target, not two features and `ShareResult` answers for both.
444
462
  // What accesses what, for one level of the shared tree (#19). A folder answers it for its contents,
445
463
  // a single flow for itself. Documents and flows are two kinds of thing that share one tree
446
464
  // (ADR-0004 §1), so the graph carries both and says which of them it is.
@@ -1,5 +1,5 @@
1
1
  import { z } from "zod";
2
- import { IdempotencyKey, IntelId, IsoDateTime } from "./contract.js";
2
+ import { baseVersion, IdempotencyKey, IntelId, IsoDateTime } from "./contract.js";
3
3
  // What this installation is equipped to do — deployment facts, never the caller's permissions
4
4
  // (those stay behind each door, where `/session` deliberately does not carry them). `agentRuntime`
5
5
  // says whether an agent Worker is bound at all (#190): without it the UI offers no "New agent" and
@@ -95,30 +95,6 @@ export const CreateNodeInput = z.strictObject({
95
95
  .describe("Optional sentence about the node, for readers rather than for search ranking."),
96
96
  idempotencyKey: IdempotencyKey,
97
97
  });
98
- /**
99
- * The version a write is made against, in the one form both writers of node content take it.
100
- *
101
- * ⚠️ Nullable and required are not the same thing, and here they stand together (#437). `null` is
102
- * how a node that has no version yet is addressed, and it is the only way to write its first
103
- * content; leaving the field out says nothing at all and is refused. Whoever leaves it out is
104
- * almost always holding a node created one call earlier, and the plain type error they used to get
105
- * — "expected string, received undefined" — reads as "this field must be an id", about a field that
106
- * cannot have one yet. Both refusals therefore name both ways instead.
107
- *
108
- * ⚠️ It stays required once a version exists, and that is what it is for: a mismatch is answered
109
- * with a `409` rather than by overwriting somebody else's newer version.
110
- *
111
- * ⚠️ The bounds remain `IntelId`'s. The repeated `min(1)` adds no rule — it only puts the sentence
112
- * on the second value that gets tried after `undefined`, the empty string.
113
- *
114
- * ⚠️ The sentence says what the field TAKES, not what the caller did wrong. It answers a wrong type
115
- * as well as an absent field, and "is required" would be false about the first — which would be
116
- * this same ticket one corner further on. A value that is merely too long keeps Zod's own `Too
117
- * big`: that one is already a true statement about what was sent.
118
- */
119
- function baseVersion(rule) {
120
- return z.union([IntelId.min(1, { error: rule }), z.null()], { error: rule });
121
- }
122
98
  export const SaveNodeVersionInput = z.strictObject({
123
99
  nodeId: IntelId.describe("Document node to append a version to."),
124
100
  baseVersionId: baseVersion("baseVersionId takes the `currentVersionId` from node_get, or null when the node has no version yet — and it has to be sent.").describe("The version this edit was made against — `currentVersionId` from node_get. If the node has moved on since, the call is refused rather than overwriting the newer version; pass `null` only for a node that has no version yet."),
@@ -20,6 +20,10 @@ export declare const ListGrantsInput: z.ZodObject<{
20
20
  resourceId: z.ZodString;
21
21
  }, z.core.$strict>;
22
22
  export type ListGrantsInput = z.infer<typeof ListGrantsInput>;
23
+ export declare const ListFlowGrantsInput: z.ZodObject<{
24
+ flowId: z.ZodString;
25
+ }, z.core.$strict>;
26
+ export type ListFlowGrantsInput = z.infer<typeof ListFlowGrantsInput>;
23
27
  export declare const ResourceGrant: z.ZodObject<{
24
28
  id: z.ZodString;
25
29
  resourceId: z.ZodString;
@@ -70,6 +74,33 @@ export declare const RevokeGrantInput: z.ZodObject<{
70
74
  idempotencyKey: z.ZodString;
71
75
  }, z.core.$strict>;
72
76
  export type RevokeGrantInput = z.infer<typeof RevokeGrantInput>;
77
+ export declare const ShareFlowInput: z.ZodObject<{
78
+ flowId: z.ZodString;
79
+ principal: z.ZodDiscriminatedUnion<[z.ZodObject<{
80
+ type: z.ZodLiteral<"user">;
81
+ id: z.ZodString;
82
+ }, z.core.$strict>, z.ZodObject<{
83
+ type: z.ZodLiteral<"email">;
84
+ email: z.ZodEmail;
85
+ }, z.core.$strict>, z.ZodObject<{
86
+ type: z.ZodLiteral<"organization">;
87
+ }, z.core.$strict>], "type">;
88
+ verb: z.ZodEnum<{
89
+ read: "read";
90
+ write: "write";
91
+ execute: "execute";
92
+ share: "share";
93
+ }>;
94
+ expiresAt: z.ZodDefault<z.ZodNullable<z.ZodISODateTime>>;
95
+ idempotencyKey: z.ZodString;
96
+ }, z.core.$strict>;
97
+ export type ShareFlowInput = z.infer<typeof ShareFlowInput>;
98
+ export declare const RevokeFlowGrantInput: z.ZodObject<{
99
+ flowId: z.ZodString;
100
+ grantId: z.ZodString;
101
+ idempotencyKey: z.ZodString;
102
+ }, z.core.$strict>;
103
+ export type RevokeFlowGrantInput = z.infer<typeof RevokeFlowGrantInput>;
73
104
  export declare const UnreadableNodes: z.ZodObject<{
74
105
  titles: z.ZodArray<z.ZodString>;
75
106
  hidden: z.ZodNumber;
@@ -102,6 +133,10 @@ export declare const ShareResult: z.ZodObject<{
102
133
  titles: z.ZodArray<z.ZodString>;
103
134
  hidden: z.ZodNumber;
104
135
  }, z.core.$strict>;
136
+ unrunnable: z.ZodDefault<z.ZodObject<{
137
+ titles: z.ZodArray<z.ZodString>;
138
+ hidden: z.ZodNumber;
139
+ }, z.core.$strict>>;
105
140
  }, z.core.$strict>;
106
141
  export type ShareResult = z.infer<typeof ShareResult>;
107
142
  export declare const ResourceGrantList: z.ZodObject<{
@@ -17,6 +17,12 @@ export const SharePrincipal = z.discriminatedUnion("type", [
17
17
  export const ListGrantsInput = z.strictObject({
18
18
  resourceId: IntelId.describe("Node whose direct grants to list. Access inherited from a folder above is not a grant on this node and is not listed here."),
19
19
  });
20
+ // The same three shapes for a flow (#530). Separate inputs rather than a widened `resourceId`,
21
+ // because the id names a different table and a caller that passes the wrong kind should be told so
22
+ // by the route it called rather than by a lookup that finds nothing.
23
+ export const ListFlowGrantsInput = z.strictObject({
24
+ flowId: IntelId.describe("Flow whose direct grants to list. Access inherited from the folder it is filed in is not a grant on this flow and is not listed here."),
25
+ });
20
26
  export const ResourceGrant = z.strictObject({
21
27
  id: IntelId,
22
28
  resourceId: IntelId,
@@ -26,13 +32,20 @@ export const ResourceGrant = z.strictObject({
26
32
  createdBy: IntelId,
27
33
  createdAt: IsoDateTime,
28
34
  });
35
+ // The word means exactly the same thing on a node grant and on a flow grant, so it is described
36
+ // once — the rule this package states about `IdempotencyKey`, applied within one file rather than
37
+ // across the package. Two copies are two places to drift, and both of them ship in `tools/list`.
38
+ //
39
+ // ⚠️ What is refused is decided by `deps.now()` in the application layer and cannot be expressed
40
+ // here (#442); this sentence names the answer a caller gets, it does not produce it.
41
+ const GrantExpiresAt = IsoDateTime.nullable()
42
+ .default(null)
43
+ .describe("When the grant stops working, or `null` for one that does not expire on its own. A time that is not in the future is refused with `grant_already_expired`: a grant that could never work is not created.");
29
44
  export const ShareInput = z.strictObject({
30
45
  resourceId: IntelId.describe("Node to grant access to. A grant on a folder is inherited by everything beneath it, which is the usual way to share a whole area."),
31
46
  principal: SharePrincipal.describe("Who gets the access: a Gate user by id, someone by verified email address, or the whole organization."),
32
47
  verb: ResourceVerb.describe("What they may do. Each verb is granted on its own and none implies another: `read` reads, `write` writes, `execute` runs a flow, `share` passes access on. Seeing a process is deliberately separable from being allowed to start it."),
33
- expiresAt: IsoDateTime.nullable()
34
- .default(null)
35
- .describe("When the grant stops working, or `null` for one that does not expire on its own."),
48
+ expiresAt: GrantExpiresAt,
36
49
  idempotencyKey: IdempotencyKey,
37
50
  });
38
51
  export const RevokeGrantInput = z.strictObject({
@@ -40,6 +53,18 @@ export const RevokeGrantInput = z.strictObject({
40
53
  grantId: IntelId.describe("The grant to withdraw, from node_grant_list."),
41
54
  idempotencyKey: IdempotencyKey,
42
55
  });
56
+ export const ShareFlowInput = z.strictObject({
57
+ flowId: IntelId.describe("Flow to grant access to. A grant here reaches this flow and nothing else — not the folder it is filed in, and not the flows it calls."),
58
+ principal: SharePrincipal.describe("Who gets the access: a Gate user by id, someone by verified email address, or the whole organization."),
59
+ verb: ResourceVerb.describe("What they may do. Each verb is granted on its own and none implies another: `read` opens the flow, `write` edits it, `execute` runs it, `share` passes access on."),
60
+ expiresAt: GrantExpiresAt,
61
+ idempotencyKey: IdempotencyKey,
62
+ });
63
+ export const RevokeFlowGrantInput = z.strictObject({
64
+ flowId: IntelId.describe("Flow the grant sits on. Named alongside the grant id so access is decided on the flow."),
65
+ grantId: IntelId.describe("The grant to withdraw, from flow_grant_list."),
66
+ idempotencyKey: IdempotencyKey,
67
+ });
43
68
  // What a grant does not cover, reported to whoever just made it. A flow in the shared folder may
44
69
  // read a document outside it, and the run is re-authorized against the person running it — so the
45
70
  // grant can be complete and the flow still stop for them (ADR-0004 §4).
@@ -53,9 +78,17 @@ export const UnreadableNodes = z.strictObject({
53
78
  // The grant is in the answer, so the warning cannot be mistaken for a refusal: it is written first
54
79
  // and described afterwards. Blocking would force everyone who uses a central policy document to
55
80
  // duplicate it, which is the opposite of what one tree is for (ADR-0004 §4).
81
+ //
82
+ // ⚠️ `unrunnable` is the second half of the same sentence and belongs to the flow grant (#530). A
83
+ // grant on a folder covered every flow beneath it, so there was nothing to say; a grant on one flow
84
+ // reaches that flow and stops, and the sub-flows it calls are then somebody else's to grant. It
85
+ // carries the shape of `UnreadableNodes` because it answers the same question — what this grant
86
+ // does NOT reach, named where the sharer may see it and counted where they may not — and it is
87
+ // empty for every node grant.
56
88
  export const ShareResult = z.strictObject({
57
89
  grant: ResourceGrant,
58
90
  unreadable: UnreadableNodes,
91
+ unrunnable: UnreadableNodes.default({ titles: [], hidden: 0 }),
59
92
  });
60
93
  // `applicableVerbs` travels with the list because the answer is the business layer's, not the
61
94
  // screen's: a document has nothing to execute, so `execute` is not offered on one (ADR-0004 §2).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@anchrd/intel-contract",
3
- "version": "0.19.0",
3
+ "version": "0.21.0",
4
4
  "type": "module",
5
5
  "license": "UNLICENSED",
6
6
  "repository": {