@stigmer/server 3.28.0 → 3.29.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.
@@ -10,6 +10,7 @@
10
10
  "can_grant_access": {},
11
11
  "can_impersonate": {},
12
12
  "can_issue_license": {},
13
+ "can_manage_credits": {},
13
14
  "can_manage_cursor_accounts": {},
14
15
  "can_manage_identity_accounts": {},
15
16
  "can_manage_model_pricing": {},
@@ -17,6 +18,13 @@
17
18
  "can_view_access": {},
18
19
  "can_view_provider_standing": {},
19
20
  "can_write_reserved_labels": {},
21
+ "credit_issuer": {
22
+ "directly_related_user_types": [
23
+ {
24
+ "type": "identity_account"
25
+ }
26
+ ]
27
+ },
20
28
  "operator": {
21
29
  "directly_related_user_types": [
22
30
  {
@@ -55,6 +63,22 @@
55
63
  "relation": "operator"
56
64
  }
57
65
  },
66
+ "can_manage_credits": {
67
+ "union": {
68
+ "child": [
69
+ {
70
+ "computedUserset": {
71
+ "relation": "operator"
72
+ }
73
+ },
74
+ {
75
+ "computedUserset": {
76
+ "relation": "credit_issuer"
77
+ }
78
+ }
79
+ ]
80
+ }
81
+ },
58
82
  "can_manage_cursor_accounts": {
59
83
  "computedUserset": {
60
84
  "relation": "operator"
@@ -90,6 +114,9 @@
90
114
  "relation": "operator"
91
115
  }
92
116
  },
117
+ "credit_issuer": {
118
+ "this": {}
119
+ },
93
120
  "operator": {
94
121
  "this": {}
95
122
  }
@@ -99,7 +99,8 @@
99
99
  * with. A driver MAY answer the parent's question in place of the
100
100
  * child's: by the model's construction the two answers are identical
101
101
  * (the child's `owner`, `viewer` and `can_view` are all `from` the
102
- * parent), and the parent's resolution is direct tuples where the
102
+ * parent; authorization/model/__tests__/registry.test.ts holds that
103
+ * for every such kind), and the parent's resolution is direct tuples where the
103
104
  * child's walks the parent's whole set (OpenFGA's learned `weight2`
104
105
  * strategy read the founder's 423 sessions per execution; that is what
105
106
  * rolled back twice on 2026-09-14). A driver that ignores the field is
@@ -1 +1 @@
1
- {"version":3,"file":"list-read-scope.d.ts","sourceRoot":"","sources":["../../src/extensions/list-read-scope.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2HG;AACH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,qFAAqF,CAAC;AAC3H,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,wDAAwD,CAAC;AACpG,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,0DAA0D,CAAC;AAajG,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AACpD,OAAO,KAAK,EACV,kBAAkB,EAClB,qBAAqB,EACtB,MAAM,6BAA6B,CAAC;AAErC;;;;;;GAMG;AACH,MAAM,WAAW,aAAc,SAAQ,qBAAqB;IAC1D,QAAQ,CAAC,MAAM,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;IAClD;;;;;;;;;OASG;IACH,QAAQ,CAAC,mBAAmB,CAAC,EAAE,kBAAkB,CAAC;CACnD;AAED,kFAAkF;AAClF,MAAM,WAAW,aAAa;IAC5B;;;;;;;;;;;;;OAaG;IACH,qBAAqB,CACnB,MAAM,EAAE,cAAc,EACtB,IAAI,EAAE,eAAe,GACpB,OAAO,CAAC,WAAW,CAAC,MAAM,CAAC,CAAC,CAAC;IAEhC;;;;;;;;;;;;OAYG;IACH,mBAAmB,CACjB,MAAM,EAAE,cAAc,EACtB,IAAI,EAAE,eAAe,EACrB,OAAO,EAAE,aAAa,CAAC,aAAa,CAAC,GACpC,OAAO,CAAC,WAAW,CAAC,MAAM,CAAC,CAAC,CAAC;CACjC;AAED;;;;;;;;;;GAUG;AACH,qBAAa,0BAA2B,SAAQ,KAAK;IACnD,QAAQ,CAAC,IAAI,EAAE,eAAe,CAAC;gBAEnB,IAAI,EAAE,eAAe;CAOlC;AAED;;;;;;;GAOG;AACH,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,EAAE;QACT,EAAE,CAAC,EAAE,MAAM,CAAC;QACZ,GAAG,CAAC,EAAE,MAAM,CAAC;QACb,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;QAChC,UAAU,CAAC,EAAE,qBAAqB,CAAC;KACpC,CAAC;IACF,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,gBAAgB,CAAA;KAAE,CAAC;CACvC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,wBAAsB,uBAAuB,CAAC,CAAC,SAAS,kBAAkB,EACxE,KAAK,EAAE,aAAa,GAAG,SAAS,EAChC,MAAM,EAAE,cAAc,EACtB,IAAI,EAAE,eAAe,EACrB,SAAS,EAAE,aAAa,CAAC,CAAC,CAAC,EAC3B,UAAU,EAAE,MAAM,GACjB,OAAO,CAAC,CAAC,EAAE,CAAC,CA2Bd"}
1
+ {"version":3,"file":"list-read-scope.d.ts","sourceRoot":"","sources":["../../src/extensions/list-read-scope.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4HG;AACH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,qFAAqF,CAAC;AAC3H,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,wDAAwD,CAAC;AACpG,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,0DAA0D,CAAC;AAajG,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AACpD,OAAO,KAAK,EACV,kBAAkB,EAClB,qBAAqB,EACtB,MAAM,6BAA6B,CAAC;AAErC;;;;;;GAMG;AACH,MAAM,WAAW,aAAc,SAAQ,qBAAqB;IAC1D,QAAQ,CAAC,MAAM,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;IAClD;;;;;;;;;OASG;IACH,QAAQ,CAAC,mBAAmB,CAAC,EAAE,kBAAkB,CAAC;CACnD;AAED,kFAAkF;AAClF,MAAM,WAAW,aAAa;IAC5B;;;;;;;;;;;;;OAaG;IACH,qBAAqB,CACnB,MAAM,EAAE,cAAc,EACtB,IAAI,EAAE,eAAe,GACpB,OAAO,CAAC,WAAW,CAAC,MAAM,CAAC,CAAC,CAAC;IAEhC;;;;;;;;;;;;OAYG;IACH,mBAAmB,CACjB,MAAM,EAAE,cAAc,EACtB,IAAI,EAAE,eAAe,EACrB,OAAO,EAAE,aAAa,CAAC,aAAa,CAAC,GACpC,OAAO,CAAC,WAAW,CAAC,MAAM,CAAC,CAAC,CAAC;CACjC;AAED;;;;;;;;;;GAUG;AACH,qBAAa,0BAA2B,SAAQ,KAAK;IACnD,QAAQ,CAAC,IAAI,EAAE,eAAe,CAAC;gBAEnB,IAAI,EAAE,eAAe;CAOlC;AAED;;;;;;;GAOG;AACH,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,EAAE;QACT,EAAE,CAAC,EAAE,MAAM,CAAC;QACZ,GAAG,CAAC,EAAE,MAAM,CAAC;QACb,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;QAChC,UAAU,CAAC,EAAE,qBAAqB,CAAC;KACpC,CAAC;IACF,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,gBAAgB,CAAA;KAAE,CAAC;CACvC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,wBAAsB,uBAAuB,CAAC,CAAC,SAAS,kBAAkB,EACxE,KAAK,EAAE,aAAa,GAAG,SAAS,EAChC,MAAM,EAAE,cAAc,EACtB,IAAI,EAAE,eAAe,EACrB,SAAS,EAAE,aAAa,CAAC,CAAC,CAAC,EAC3B,UAAU,EAAE,MAAM,GACjB,OAAO,CAAC,CAAC,EAAE,CAAC,CA2Bd"}
@@ -1 +1 @@
1
- {"version":3,"file":"list-read-scope.js","sourceRoot":"","sources":["../../src/extensions/list-read-scope.ts"],"names":[],"mappings":"AAgIA,sEAAsE;AACtE,uEAAuE;AACvE,yEAAyE;AACzE,yEAAyE;AACzE,sBAAsB;AACtB,OAAO,EACL,8BAA8B,EAC9B,YAAY,GACb,MAAM,iCAAiC,CAAC;AACzC,OAAO,EAAE,uBAAuB,EAAE,MAAM,0CAA0C,CAAC;AAuEnF;;;;;;;;;;GAUG;AACH,MAAM,OAAO,0BAA2B,SAAQ,KAAK;IAC1C,IAAI,CAAkB;IAE/B,YAAY,IAAqB;QAC/B,KAAK,CACH,qEAAqE,YAAY,CAAC,IAAI,CAAC,sGAAsG,CAC9L,CAAC;QACF,IAAI,CAAC,IAAI,GAAG,4BAA4B,CAAC;QACzC,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;IACnB,CAAC;CACF;AAqBD;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,MAAM,CAAC,KAAK,UAAU,uBAAuB,CAC3C,KAAgC,EAChC,MAAsB,EACtB,IAAqB,EACrB,SAA2B,EAC3B,UAAkB;IAElB,IAAI,KAAK,KAAK,SAAS,EAAE,CAAC;QACxB,OAAO,CAAC,GAAG,SAAS,CAAC,CAAC;IACxB,CAAC;IACD,MAAM,OAAO,GACX,UAAU,KAAK,EAAE;QACf,CAAC,CAAC,SAAS;QACX,CAAC,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC,QAAQ,EAAE,EAAE,CAAC,QAAQ,CAAC,QAAQ,EAAE,GAAG,KAAK,UAAU,CAAC,CAAC;IAC5E,sEAAsE;IACtE,wEAAwE;IACxE,qEAAqE;IACrE,IAAI,MAAM,CAAC,WAAW,KAAK,UAAU,EAAE,CAAC;QACtC,OAAO,CAAC,GAAG,OAAO,CAAC,CAAC;IACtB,CAAC;IACD,MAAM,QAAQ,GAAG,yBAAyB,CAAC,IAAI,CAAC,CAAC;IACjD,MAAM,OAAO,GAAoB,OAAO,CAAC,GAAG,CAAC,CAAC,QAAQ,EAAE,EAAE;QACxD,MAAM,KAAK,GAAkB;YAC3B,GAAG,uBAAuB,CAAC,IAAI,EAAE,QAAQ,CAAC;YAC1C,MAAM,EAAE,QAAQ,CAAC,QAAQ,EAAE,MAAM,IAAI,EAAE;SACxC,CAAC;QACF,MAAM,mBAAmB,GAAG,QAAQ,CAAC,KAAK,CAAC,WAAW,CAAC,CAAC;QACxD,OAAO,mBAAmB,KAAK,SAAS;YACtC,CAAC,CAAC,KAAK;YACP,CAAC,CAAC,EAAE,GAAG,KAAK,EAAE,mBAAmB,EAAE,CAAC;IACxC,CAAC,CAAC,CAAC;IACH,MAAM,OAAO,GAAG,MAAM,KAAK,CAAC,mBAAmB,CAAC,MAAM,EAAE,IAAI,EAAE,OAAO,CAAC,CAAC;IACvE,OAAO,OAAO,CAAC,MAAM,CAAC,CAAC,QAAQ,EAAE,EAAE,CAAC,OAAO,CAAC,GAAG,CAAC,QAAQ,CAAC,QAAQ,EAAE,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC;AAChF,CAAC;AAED;;;;;;;GAOG;AACH,SAAS,yBAAyB,CAChC,IAAqB;IAIrB,MAAM,MAAM,GAAG,8BAA8B,CAAC,IAAI,CAAC,CAAC;IACpD,IAAI,MAAM,KAAK,SAAS,EAAE,CAAC;QACzB,OAAO,GAAG,EAAE,CAAC,SAAS,CAAC;IACzB,CAAC;IACD,OAAO,CAAC,WAAW,EAAE,EAAE,CACrB,WAAW,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,QAAQ,KAAK,MAAM,CAAC,QAAQ,CAAC,CAAC;AAClE,CAAC"}
1
+ {"version":3,"file":"list-read-scope.js","sourceRoot":"","sources":["../../src/extensions/list-read-scope.ts"],"names":[],"mappings":"AAiIA,sEAAsE;AACtE,uEAAuE;AACvE,yEAAyE;AACzE,yEAAyE;AACzE,sBAAsB;AACtB,OAAO,EACL,8BAA8B,EAC9B,YAAY,GACb,MAAM,iCAAiC,CAAC;AACzC,OAAO,EAAE,uBAAuB,EAAE,MAAM,0CAA0C,CAAC;AAuEnF;;;;;;;;;;GAUG;AACH,MAAM,OAAO,0BAA2B,SAAQ,KAAK;IAC1C,IAAI,CAAkB;IAE/B,YAAY,IAAqB;QAC/B,KAAK,CACH,qEAAqE,YAAY,CAAC,IAAI,CAAC,sGAAsG,CAC9L,CAAC;QACF,IAAI,CAAC,IAAI,GAAG,4BAA4B,CAAC;QACzC,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;IACnB,CAAC;CACF;AAqBD;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,MAAM,CAAC,KAAK,UAAU,uBAAuB,CAC3C,KAAgC,EAChC,MAAsB,EACtB,IAAqB,EACrB,SAA2B,EAC3B,UAAkB;IAElB,IAAI,KAAK,KAAK,SAAS,EAAE,CAAC;QACxB,OAAO,CAAC,GAAG,SAAS,CAAC,CAAC;IACxB,CAAC;IACD,MAAM,OAAO,GACX,UAAU,KAAK,EAAE;QACf,CAAC,CAAC,SAAS;QACX,CAAC,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC,QAAQ,EAAE,EAAE,CAAC,QAAQ,CAAC,QAAQ,EAAE,GAAG,KAAK,UAAU,CAAC,CAAC;IAC5E,sEAAsE;IACtE,wEAAwE;IACxE,qEAAqE;IACrE,IAAI,MAAM,CAAC,WAAW,KAAK,UAAU,EAAE,CAAC;QACtC,OAAO,CAAC,GAAG,OAAO,CAAC,CAAC;IACtB,CAAC;IACD,MAAM,QAAQ,GAAG,yBAAyB,CAAC,IAAI,CAAC,CAAC;IACjD,MAAM,OAAO,GAAoB,OAAO,CAAC,GAAG,CAAC,CAAC,QAAQ,EAAE,EAAE;QACxD,MAAM,KAAK,GAAkB;YAC3B,GAAG,uBAAuB,CAAC,IAAI,EAAE,QAAQ,CAAC;YAC1C,MAAM,EAAE,QAAQ,CAAC,QAAQ,EAAE,MAAM,IAAI,EAAE;SACxC,CAAC;QACF,MAAM,mBAAmB,GAAG,QAAQ,CAAC,KAAK,CAAC,WAAW,CAAC,CAAC;QACxD,OAAO,mBAAmB,KAAK,SAAS;YACtC,CAAC,CAAC,KAAK;YACP,CAAC,CAAC,EAAE,GAAG,KAAK,EAAE,mBAAmB,EAAE,CAAC;IACxC,CAAC,CAAC,CAAC;IACH,MAAM,OAAO,GAAG,MAAM,KAAK,CAAC,mBAAmB,CAAC,MAAM,EAAE,IAAI,EAAE,OAAO,CAAC,CAAC;IACvE,OAAO,OAAO,CAAC,MAAM,CAAC,CAAC,QAAQ,EAAE,EAAE,CAAC,OAAO,CAAC,GAAG,CAAC,QAAQ,CAAC,QAAQ,EAAE,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC;AAChF,CAAC;AAED;;;;;;;GAOG;AACH,SAAS,yBAAyB,CAChC,IAAqB;IAIrB,MAAM,MAAM,GAAG,8BAA8B,CAAC,IAAI,CAAC,CAAC;IACpD,IAAI,MAAM,KAAK,SAAS,EAAE,CAAC;QACzB,OAAO,GAAG,EAAE,CAAC,SAAS,CAAC;IACzB,CAAC;IACD,OAAO,CAAC,WAAW,EAAE,EAAE,CACrB,WAAW,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,QAAQ,KAAK,MAAM,CAAC,QAAQ,CAAC,CAAC;AAClE,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stigmer/server",
3
- "version": "3.28.0",
3
+ "version": "3.29.0",
4
4
  "description": "The Stigmer OSS control plane as a composable library: composeServer, the extension registry and the driver seams a composition builds on. The self-contained deployable of the same server is @stigmer/server-slim.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -63,11 +63,11 @@
63
63
  "@connectrpc/connect-node": "^2.0.0",
64
64
  "@kubernetes/client-node": "2.0.0",
65
65
  "@opentelemetry/api": "^1.9.0",
66
- "@stigmer/outbound": "3.28.0",
67
- "@stigmer/plugin-package": "3.28.0",
68
- "@stigmer/protos": "3.28.0",
69
- "@stigmer/temporal-codecs": "3.28.0",
70
- "@stigmer/zip-structure": "3.28.0",
66
+ "@stigmer/outbound": "3.29.0",
67
+ "@stigmer/plugin-package": "3.29.0",
68
+ "@stigmer/protos": "3.29.0",
69
+ "@stigmer/temporal-codecs": "3.29.0",
70
+ "@stigmer/zip-structure": "3.29.0",
71
71
  "@temporalio/activity": "1.16.2",
72
72
  "@temporalio/client": "1.16.2",
73
73
  "@temporalio/common": "1.16.2",
@@ -55,6 +55,7 @@ const DOCUMENTS = [
55
55
  "channel-app-owner.fga.yaml",
56
56
  "channel-participation.fga.yaml",
57
57
  "channel-session-visibility.fga.yaml",
58
+ "credit-issuer.fga.yaml",
58
59
  "default-instance-inheritance.fga.yaml",
59
60
  "identity-provider-administration.fga.yaml",
60
61
  "invitation-administration.fga.yaml",
@@ -17,7 +17,14 @@
17
17
  * step and the model cannot disagree about where a team may be
18
18
  * granted;
19
19
  * - the derived rules are exactly the three relations `kind_meta` cannot
20
- * derive.
20
+ * derive;
21
+ * - a kind whose authorization is its parent's whole
22
+ * (`inheritedAuthorizationParentOf`) holds nothing of its own: its one
23
+ * direct relation is the parent link, every other relation is built
24
+ * through it, and its `can_view` answers exactly what the parent's
25
+ * does, so the list read scope may ask the parent in the child's place
26
+ * (extensions/list-read-scope.ts) without hiding or showing a row the
27
+ * child's own check would not.
21
28
  *
22
29
  * Whether the model defines what the wire asks is the whole contract's
23
30
  * question, pinned in __tests__/wire-permissions.test.ts.
@@ -39,12 +46,21 @@ import { IamRole } from "@stigmer/protos/ai/stigmer/iam/v1/enum_pb";
39
46
  import {
40
47
  getKindMeta,
41
48
  grantableRolesFor,
49
+ inheritedAuthorizationParentOf,
42
50
  kindEnumName,
43
51
  teamGrantableRolesFor,
44
52
  } from "../../../pipeline/apiresource-meta.js";
53
+ import {
54
+ computed,
55
+ direct,
56
+ from,
57
+ objectOf,
58
+ throwawayDeclaration,
59
+ union,
60
+ } from "../../__tests__/throwaway-model.js";
45
61
  import { KIND_BINDINGS, ROWLESS } from "../bindings.js";
46
62
  import { builtInModel, declarationFor } from "../index.js";
47
- import type { Rewrite, SubjectType } from "../rewrite.js";
63
+ import type { KindDeclaration, Rewrite, SubjectType } from "../rewrite.js";
48
64
 
49
65
  /** The server package root, where fga/model lives. */
50
66
  const PACKAGE_ROOT = new URL("../../../../", import.meta.url);
@@ -93,6 +109,180 @@ function directSubjects(rewrite: Rewrite): ReadonlyArray<SubjectType> {
93
109
  }
94
110
  }
95
111
 
112
+ /** The relation every list lane reads through, on the child and on its parent. */
113
+ const LIST_RELATION = "can_view";
114
+
115
+ /** The `this`, `computed` and `from` nodes a rewrite is built from, in the line's order. */
116
+ function leavesOf(rewrite: Rewrite): ReadonlyArray<Rewrite> {
117
+ switch (rewrite.node) {
118
+ case "this":
119
+ case "computed":
120
+ case "from":
121
+ return [rewrite];
122
+ case "union":
123
+ case "intersection":
124
+ return rewrite.members.flatMap(leavesOf);
125
+ default: {
126
+ const exhaustive: never = rewrite;
127
+ throw new Error(`unknown rewrite node: ${JSON.stringify(exhaustive)}`);
128
+ }
129
+ }
130
+ }
131
+
132
+ /**
133
+ * The relations of `declaration` that `relation` includes whole: itself,
134
+ * and every sibling a union member or a computed node names, transitively.
135
+ * An intersection includes none of its members whole, so it stops here.
136
+ */
137
+ function includedWhole(declaration: KindDeclaration, relation: string): ReadonlySet<string> {
138
+ const included = new Set<string>();
139
+ const visit = (name: string): void => {
140
+ if (included.has(name)) {
141
+ return;
142
+ }
143
+ included.add(name);
144
+ const walk = (rewrite: Rewrite | undefined): void => {
145
+ if (rewrite?.node === "computed") {
146
+ visit(rewrite.relation);
147
+ } else if (rewrite?.node === "union") {
148
+ rewrite.members.forEach(walk);
149
+ }
150
+ };
151
+ walk(declaration.relations.get(name));
152
+ };
153
+ visit(relation);
154
+ return included;
155
+ }
156
+
157
+ /**
158
+ * The parent relations `relation` of `child` reaches through `link`,
159
+ * following the child's own computed relations to their leaves.
160
+ */
161
+ function parentRelationsReached(
162
+ child: KindDeclaration,
163
+ relation: string,
164
+ link: string,
165
+ ): ReadonlySet<string> {
166
+ const reached = new Set<string>();
167
+ const seen = new Set<string>();
168
+ const visit = (name: string): void => {
169
+ if (seen.has(name)) {
170
+ return;
171
+ }
172
+ seen.add(name);
173
+ const rewrite = child.relations.get(name);
174
+ for (const leaf of rewrite === undefined ? [] : leavesOf(rewrite)) {
175
+ if (leaf.node === "computed") {
176
+ visit(leaf.relation);
177
+ } else if (leaf.node === "from" && leaf.tupleset === link) {
178
+ reached.add(leaf.relation);
179
+ }
180
+ }
181
+ };
182
+ visit(relation);
183
+ return reached;
184
+ }
185
+
186
+ /**
187
+ * Every way `child` breaks the promise its `kind_meta` makes when it says
188
+ * the kind's authorization is its parent's whole (PARENT scope, INHERITED
189
+ * owner); empty when the model keeps it. The list read scope reads that
190
+ * promise to let a driver ask the parent's `can_view` in the child's
191
+ * place, which gives the child's answer only while:
192
+ * - the child's one direct relation is the link, to the parent type
193
+ * alone, so nothing is granted on the child that the parent does not
194
+ * hold;
195
+ * - every other relation is built through the link or from the child's
196
+ * own relations, never from another object;
197
+ * - the child's `can_view` includes the parent's (`can_view from <link>`
198
+ * as a union member), so asking the parent hides nothing;
199
+ * - every parent relation the child's `can_view` reaches is one the
200
+ * parent's `can_view` includes whole, so asking the parent shows
201
+ * nothing more.
202
+ * The last reads inclusion structurally (union and computed), so it errs
203
+ * toward a violation: a shape it cannot prove is a design review, never a
204
+ * pass.
205
+ */
206
+ function inheritedWholeViolations(
207
+ child: KindDeclaration,
208
+ parent: KindDeclaration,
209
+ link: string,
210
+ ): ReadonlyArray<string> {
211
+ const violations: string[] = [];
212
+ const linkRewrite = child.relations.get(link);
213
+ const [linked, ...more] = linkRewrite?.node === "this" ? linkRewrite.subjects : [];
214
+ if (linked?.form !== "object" || linked.type !== parent.type || more.length > 0) {
215
+ violations.push(`${child.type}#${link} is not the direct link [${parent.type}] alone`);
216
+ }
217
+ for (const [name, rewrite] of child.relations) {
218
+ if (name === link) {
219
+ continue;
220
+ }
221
+ for (const leaf of leavesOf(rewrite)) {
222
+ if (leaf.node === "this") {
223
+ violations.push(`${child.type}#${name} admits a tuple on the child itself`);
224
+ } else if (leaf.node === "from" && leaf.tupleset !== link) {
225
+ violations.push(
226
+ `${child.type}#${name} reads ${leaf.relation} from ${leaf.tupleset}, not from ${link}`,
227
+ );
228
+ } else if (leaf.node === "computed" && !child.relations.has(leaf.relation)) {
229
+ violations.push(
230
+ `${child.type}#${name} names ${leaf.relation}, which the child does not declare`,
231
+ );
232
+ }
233
+ }
234
+ }
235
+ const childView = child.relations.get(LIST_RELATION);
236
+ if (childView === undefined || !parent.relations.has(LIST_RELATION)) {
237
+ violations.push(`${child.type} and ${parent.type} must both declare ${LIST_RELATION}`);
238
+ return violations;
239
+ }
240
+ const members = childView.node === "union" ? childView.members : [childView];
241
+ if (
242
+ !members.some(
243
+ (member) =>
244
+ member.node === "from" && member.relation === LIST_RELATION && member.tupleset === link,
245
+ )
246
+ ) {
247
+ violations.push(
248
+ `${child.type}#${LIST_RELATION} does not include ${LIST_RELATION} from ${link}`,
249
+ );
250
+ }
251
+ const parentView = includedWhole(parent, LIST_RELATION);
252
+ for (const relation of parentRelationsReached(child, LIST_RELATION, link)) {
253
+ if (!parentView.has(relation)) {
254
+ violations.push(
255
+ `${child.type}#${LIST_RELATION} reaches ${parent.type}#${relation}, which ${parent.type}#${LIST_RELATION} does not include`,
256
+ );
257
+ }
258
+ }
259
+ return violations;
260
+ }
261
+
262
+ /** A declaration the model must hold, or a loud failure naming it. */
263
+ function declared(declaration: KindDeclaration | undefined, type: string): KindDeclaration {
264
+ if (declaration === undefined) {
265
+ throw new Error(`${type} is not in the built-in model`);
266
+ }
267
+ return declaration;
268
+ }
269
+
270
+ /** The kinds whose `kind_meta` makes their authorization their parent's whole, with that parent. */
271
+ function inheritedWholeKinds(): ReadonlyArray<{
272
+ readonly kind: ApiResourceKind;
273
+ readonly parentType: string;
274
+ readonly link: string;
275
+ }> {
276
+ return ApiResourceKindSchema.values.flatMap((value) => {
277
+ const kind = value.number as ApiResourceKind;
278
+ if (kind === ApiResourceKind.api_resource_kind_unknown) {
279
+ return [];
280
+ }
281
+ const parent = inheritedAuthorizationParentOf(kind);
282
+ return parent === undefined ? [] : [{ kind, parentType: parent.kind, link: parent.relation }];
283
+ });
284
+ }
285
+
96
286
  function isTeamMembers(subject: SubjectType): boolean {
97
287
  return subject.form === "userset" && subject.type === "team" && subject.relation === "member";
98
288
  }
@@ -202,4 +392,56 @@ describe("the built-in model", () => {
202
392
  "workflow_instance#execution_viewer",
203
393
  ]);
204
394
  });
395
+
396
+ it("gives a kind whose authorization is its parent's nothing of its own, so asking the parent answers for it", () => {
397
+ const inherited = inheritedWholeKinds();
398
+ expect(inherited.length, "kinds whose authorization is their parent's").toBeGreaterThan(0);
399
+ for (const { kind, parentType, link } of inherited) {
400
+ const child = declared(declarationFor(kind), kindEnumName(kind));
401
+ const parent = declared(builtInModel.byType(parentType), parentType);
402
+ expect(inheritedWholeViolations(child, parent, link), kindEnumName(kind)).toEqual([]);
403
+ }
404
+ });
405
+ });
406
+
407
+ describe("the parent-inheritance walker refuses what would make asking the parent wrong", () => {
408
+ const execution = declared(declarationFor(ApiResourceKind.agent_execution), "agent_execution");
409
+ const session = declared(builtInModel.byType("session"), "session");
410
+
411
+ /** agent_execution as the model has it, with one line rewritten. */
412
+ function executionWith(relation: string, rewrite: Rewrite): KindDeclaration {
413
+ return throwawayDeclaration({
414
+ kind: ApiResourceKind.agent_execution,
415
+ relations: [...execution.relations].map(([name, current]) =>
416
+ name === relation ? ([name, rewrite] as const) : ([name, current] as const),
417
+ ),
418
+ });
419
+ }
420
+
421
+ it.each([
422
+ {
423
+ shape: "a viewer granted on the execution itself",
424
+ child: executionWith(
425
+ "viewer",
426
+ union(direct(objectOf("identity_account")), from("viewer", "session"), computed("owner")),
427
+ ),
428
+ violation: "agent_execution#viewer admits a tuple on the child itself",
429
+ },
430
+ {
431
+ shape: "a can_view that no longer includes the session's",
432
+ child: executionWith("can_view", computed("viewer")),
433
+ violation: "agent_execution#can_view does not include can_view from session",
434
+ },
435
+ {
436
+ shape: "a can_view reaching a session relation its can_view does not include",
437
+ child: executionWith(
438
+ "can_view",
439
+ union(computed("viewer"), from("can_view", "session"), from("can_delete", "session")),
440
+ ),
441
+ violation:
442
+ "agent_execution#can_view reaches session#can_delete, which session#can_view does not include",
443
+ },
444
+ ])("$shape", ({ child, violation }) => {
445
+ expect(inheritedWholeViolations(child, session, "session")).toEqual([violation]);
446
+ });
205
447
  });
@@ -10,6 +10,7 @@
10
10
  "can_grant_access": {},
11
11
  "can_impersonate": {},
12
12
  "can_issue_license": {},
13
+ "can_manage_credits": {},
13
14
  "can_manage_cursor_accounts": {},
14
15
  "can_manage_identity_accounts": {},
15
16
  "can_manage_model_pricing": {},
@@ -17,6 +18,13 @@
17
18
  "can_view_access": {},
18
19
  "can_view_provider_standing": {},
19
20
  "can_write_reserved_labels": {},
21
+ "credit_issuer": {
22
+ "directly_related_user_types": [
23
+ {
24
+ "type": "identity_account"
25
+ }
26
+ ]
27
+ },
20
28
  "operator": {
21
29
  "directly_related_user_types": [
22
30
  {
@@ -55,6 +63,22 @@
55
63
  "relation": "operator"
56
64
  }
57
65
  },
66
+ "can_manage_credits": {
67
+ "union": {
68
+ "child": [
69
+ {
70
+ "computedUserset": {
71
+ "relation": "operator"
72
+ }
73
+ },
74
+ {
75
+ "computedUserset": {
76
+ "relation": "credit_issuer"
77
+ }
78
+ }
79
+ ]
80
+ }
81
+ },
58
82
  "can_manage_cursor_accounts": {
59
83
  "computedUserset": {
60
84
  "relation": "operator"
@@ -90,6 +114,9 @@
90
114
  "relation": "operator"
91
115
  }
92
116
  },
117
+ "credit_issuer": {
118
+ "this": {}
119
+ },
93
120
  "operator": {
94
121
  "this": {}
95
122
  }
@@ -99,7 +99,8 @@
99
99
  * with. A driver MAY answer the parent's question in place of the
100
100
  * child's: by the model's construction the two answers are identical
101
101
  * (the child's `owner`, `viewer` and `can_view` are all `from` the
102
- * parent), and the parent's resolution is direct tuples where the
102
+ * parent; authorization/model/__tests__/registry.test.ts holds that
103
+ * for every such kind), and the parent's resolution is direct tuples where the
103
104
  * child's walks the parent's whole set (OpenFGA's learned `weight2`
104
105
  * strategy read the founder's 423 sessions per execution; that is what
105
106
  * rolled back twice on 2026-09-14). A driver that ignores the field is