@stigmer/server 3.27.2 → 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.
- package/README.md +2 -0
- package/dist/authorization/authorizer.d.ts +3 -3
- package/dist/authorization/authorizer.js +3 -3
- package/dist/authorization/derived-tuples.js +6 -1
- package/dist/authorization/derived-tuples.js.map +1 -1
- package/dist/authorization/evaluator.d.ts +9 -10
- package/dist/authorization/evaluator.d.ts.map +1 -1
- package/dist/authorization/evaluator.js.map +1 -1
- package/dist/authorization/list-read-scope.d.ts.map +1 -1
- package/dist/authorization/list-read-scope.js +29 -18
- package/dist/authorization/list-read-scope.js.map +1 -1
- package/dist/authorization/model/bindings.d.ts +41 -0
- package/dist/authorization/model/bindings.d.ts.map +1 -0
- package/dist/authorization/model/bindings.js +89 -0
- package/dist/authorization/model/bindings.js.map +1 -0
- package/dist/authorization/model/data/authorization-model.json +3141 -0
- package/dist/authorization/model/default-of.d.ts +2 -2
- package/dist/authorization/model/execution-viewer.d.ts +3 -0
- package/dist/authorization/model/execution-viewer.d.ts.map +1 -0
- package/dist/authorization/model/execution-viewer.js +50 -0
- package/dist/authorization/model/execution-viewer.js.map +1 -0
- package/dist/authorization/model/index.d.ts +33 -21
- package/dist/authorization/model/index.d.ts.map +1 -1
- package/dist/authorization/model/index.js +74 -52
- package/dist/authorization/model/index.js.map +1 -1
- package/dist/authorization/model/openfga-json.d.ts +18 -0
- package/dist/authorization/model/openfga-json.d.ts.map +1 -0
- package/dist/authorization/model/openfga-json.js +266 -0
- package/dist/authorization/model/openfga-json.js.map +1 -0
- package/dist/authorization/model/rewrite.d.ts +39 -67
- package/dist/authorization/model/rewrite.d.ts.map +1 -1
- package/dist/authorization/model/rewrite.js +1 -117
- package/dist/authorization/model/rewrite.js.map +1 -1
- package/dist/authorization/strict-json.d.ts +20 -0
- package/dist/authorization/strict-json.d.ts.map +1 -0
- package/dist/authorization/strict-json.js +45 -0
- package/dist/authorization/strict-json.js.map +1 -0
- package/dist/boot/compose.d.ts.map +1 -1
- package/dist/boot/compose.js +9 -7
- package/dist/boot/compose.js.map +1 -1
- package/dist/domain/workflowexecution/list-index.d.ts +1 -1
- package/dist/extensions/list-read-scope.d.ts +20 -3
- package/dist/extensions/list-read-scope.d.ts.map +1 -1
- package/dist/extensions/list-read-scope.js +20 -1
- package/dist/extensions/list-read-scope.js.map +1 -1
- package/dist/index.d.ts +7 -8
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +15 -5
- package/dist/index.js.map +1 -1
- package/package.json +7 -6
- package/src/authorization/README.md +41 -34
- package/src/authorization/__tests__/authorizer.test.ts +5 -14
- package/src/authorization/__tests__/derived-tuples.test.ts +29 -27
- package/src/authorization/__tests__/evaluator.test.ts +53 -160
- package/src/authorization/__tests__/facts.test.ts +5 -11
- package/src/authorization/__tests__/list-read-scope.measure.test.ts +12 -20
- package/src/authorization/__tests__/list-read-scope.test.ts +15 -14
- package/src/authorization/__tests__/organization-directory.test.ts +2 -2
- package/src/authorization/__tests__/store-test-kit.ts +246 -0
- package/src/authorization/__tests__/store-tests.test.ts +92 -223
- package/src/authorization/__tests__/support.ts +30 -2
- package/src/authorization/__tests__/throwaway-model.ts +75 -0
- package/src/authorization/__tests__/wire-permissions.test.ts +141 -107
- package/src/authorization/authorizer.ts +3 -3
- package/src/authorization/derived-tuples.ts +7 -2
- package/src/authorization/evaluator.ts +8 -10
- package/src/authorization/list-read-scope.ts +37 -22
- package/src/authorization/model/__tests__/openfga-json.test.ts +353 -0
- package/src/authorization/model/__tests__/registry.test.ts +345 -277
- package/src/authorization/model/bindings.ts +131 -0
- package/src/authorization/model/data/authorization-model.json +3141 -0
- package/src/authorization/model/default-of.ts +2 -2
- package/src/authorization/model/execution-viewer.ts +56 -0
- package/src/authorization/model/index.ts +81 -72
- package/src/authorization/model/openfga-json.ts +310 -0
- package/src/authorization/model/rewrite.ts +39 -188
- package/src/authorization/strict-json.ts +52 -0
- package/src/boot/compose.ts +9 -7
- package/src/extensions/__tests__/built-in-authorization-composed.test.ts +3 -6
- package/src/extensions/list-read-scope.ts +32 -4
- package/src/index.ts +15 -44
- package/dist/authorization/model/agent.d.ts +0 -2
- package/dist/authorization/model/agent.d.ts.map +0 -1
- package/dist/authorization/model/agent.js +0 -43
- package/dist/authorization/model/agent.js.map +0 -1
- package/dist/authorization/model/agent_channel.d.ts +0 -2
- package/dist/authorization/model/agent_channel.d.ts.map +0 -1
- package/dist/authorization/model/agent_channel.js +0 -39
- package/dist/authorization/model/agent_channel.js.map +0 -1
- package/dist/authorization/model/agent_execution.d.ts +0 -2
- package/dist/authorization/model/agent_execution.d.ts.map +0 -1
- package/dist/authorization/model/agent_execution.js +0 -25
- package/dist/authorization/model/agent_execution.js.map +0 -1
- package/dist/authorization/model/agent_instance.d.ts +0 -2
- package/dist/authorization/model/agent_instance.d.ts.map +0 -1
- package/dist/authorization/model/agent_instance.js +0 -48
- package/dist/authorization/model/agent_instance.js.map +0 -1
- package/dist/authorization/model/agent_share.d.ts +0 -2
- package/dist/authorization/model/agent_share.d.ts.map +0 -1
- package/dist/authorization/model/agent_share.js +0 -29
- package/dist/authorization/model/agent_share.js.map +0 -1
- package/dist/authorization/model/api_key.d.ts +0 -2
- package/dist/authorization/model/api_key.d.ts.map +0 -1
- package/dist/authorization/model/api_key.js +0 -27
- package/dist/authorization/model/api_key.js.map +0 -1
- package/dist/authorization/model/artifact.d.ts +0 -2
- package/dist/authorization/model/artifact.d.ts.map +0 -1
- package/dist/authorization/model/artifact.js +0 -27
- package/dist/authorization/model/artifact.js.map +0 -1
- package/dist/authorization/model/channel_app.d.ts +0 -2
- package/dist/authorization/model/channel_app.d.ts.map +0 -1
- package/dist/authorization/model/channel_app.js +0 -28
- package/dist/authorization/model/channel_app.js.map +0 -1
- package/dist/authorization/model/environment.d.ts +0 -2
- package/dist/authorization/model/environment.d.ts.map +0 -1
- package/dist/authorization/model/environment.js +0 -33
- package/dist/authorization/model/environment.js.map +0 -1
- package/dist/authorization/model/execution_context.d.ts +0 -2
- package/dist/authorization/model/execution_context.d.ts.map +0 -1
- package/dist/authorization/model/execution_context.js +0 -19
- package/dist/authorization/model/execution_context.js.map +0 -1
- package/dist/authorization/model/iam_policy.d.ts +0 -2
- package/dist/authorization/model/iam_policy.d.ts.map +0 -1
- package/dist/authorization/model/iam_policy.js +0 -31
- package/dist/authorization/model/iam_policy.js.map +0 -1
- package/dist/authorization/model/identity_account.d.ts +0 -2
- package/dist/authorization/model/identity_account.d.ts.map +0 -1
- package/dist/authorization/model/identity_account.js +0 -25
- package/dist/authorization/model/identity_account.js.map +0 -1
- package/dist/authorization/model/mcp_server.d.ts +0 -2
- package/dist/authorization/model/mcp_server.d.ts.map +0 -1
- package/dist/authorization/model/mcp_server.js +0 -38
- package/dist/authorization/model/mcp_server.js.map +0 -1
- package/dist/authorization/model/memory.d.ts +0 -2
- package/dist/authorization/model/memory.d.ts.map +0 -1
- package/dist/authorization/model/memory.js +0 -26
- package/dist/authorization/model/memory.js.map +0 -1
- package/dist/authorization/model/oauth_app.d.ts +0 -2
- package/dist/authorization/model/oauth_app.d.ts.map +0 -1
- package/dist/authorization/model/oauth_app.js +0 -31
- package/dist/authorization/model/oauth_app.js.map +0 -1
- package/dist/authorization/model/organization.d.ts +0 -2
- package/dist/authorization/model/organization.d.ts.map +0 -1
- package/dist/authorization/model/organization.js +0 -58
- package/dist/authorization/model/organization.js.map +0 -1
- package/dist/authorization/model/platform_client.d.ts +0 -2
- package/dist/authorization/model/platform_client.d.ts.map +0 -1
- package/dist/authorization/model/platform_client.js +0 -31
- package/dist/authorization/model/platform_client.js.map +0 -1
- package/dist/authorization/model/plugin.d.ts +0 -2
- package/dist/authorization/model/plugin.d.ts.map +0 -1
- package/dist/authorization/model/plugin.js +0 -45
- package/dist/authorization/model/plugin.js.map +0 -1
- package/dist/authorization/model/schedule.d.ts +0 -2
- package/dist/authorization/model/schedule.d.ts.map +0 -1
- package/dist/authorization/model/schedule.js +0 -34
- package/dist/authorization/model/schedule.js.map +0 -1
- package/dist/authorization/model/session.d.ts +0 -2
- package/dist/authorization/model/session.d.ts.map +0 -1
- package/dist/authorization/model/session.js +0 -38
- package/dist/authorization/model/session.js.map +0 -1
- package/dist/authorization/model/skill.d.ts +0 -2
- package/dist/authorization/model/skill.d.ts.map +0 -1
- package/dist/authorization/model/skill.js +0 -39
- package/dist/authorization/model/skill.js.map +0 -1
- package/dist/authorization/model/workflow.d.ts +0 -2
- package/dist/authorization/model/workflow.d.ts.map +0 -1
- package/dist/authorization/model/workflow.js +0 -39
- package/dist/authorization/model/workflow.js.map +0 -1
- package/dist/authorization/model/workflow_execution.d.ts +0 -2
- package/dist/authorization/model/workflow_execution.d.ts.map +0 -1
- package/dist/authorization/model/workflow_execution.js +0 -33
- package/dist/authorization/model/workflow_execution.js.map +0 -1
- package/dist/authorization/model/workflow_instance.d.ts +0 -2
- package/dist/authorization/model/workflow_instance.d.ts.map +0 -1
- package/dist/authorization/model/workflow_instance.js +0 -96
- package/dist/authorization/model/workflow_instance.js.map +0 -1
- package/dist/authorization/store-test-kit.d.ts +0 -62
- package/dist/authorization/store-test-kit.d.ts.map +0 -1
- package/dist/authorization/store-test-kit.js +0 -265
- package/dist/authorization/store-test-kit.js.map +0 -1
- package/src/authorization/__tests__/enterprise-model.ts +0 -59
- package/src/authorization/__tests__/fixtures/fga/agent-channel-owner.fga.yaml +0 -112
- package/src/authorization/__tests__/fixtures/fga/agent-instance-creation.fga.yaml +0 -177
- package/src/authorization/__tests__/fixtures/fga/agent-share-owner.fga.yaml +0 -109
- package/src/authorization/__tests__/fixtures/fga/artifact-org-and-owner.fga.yaml +0 -128
- package/src/authorization/__tests__/fixtures/fga/blueprint-private-visibility.fga.yaml +0 -318
- package/src/authorization/__tests__/fixtures/fga/channel-app-owner.fga.yaml +0 -125
- package/src/authorization/__tests__/fixtures/fga/channel-participation.fga.yaml +0 -201
- package/src/authorization/__tests__/fixtures/fga/channel-session-visibility.fga.yaml +0 -187
- package/src/authorization/__tests__/fixtures/fga/default-instance-inheritance.fga.yaml +0 -214
- package/src/authorization/__tests__/fixtures/fga/mcp-server-authoring.fga.yaml +0 -129
- package/src/authorization/__tests__/fixtures/fga/memory-subject-only.fga.yaml +0 -72
- package/src/authorization/__tests__/fixtures/fga/org-admin-owner-inheritance.fga.yaml +0 -308
- package/src/authorization/__tests__/fixtures/fga/org-shared-environment.fga.yaml +0 -144
- package/src/authorization/__tests__/fixtures/fga/platform-visibility.fga.yaml +0 -199
- package/src/authorization/__tests__/fixtures/fga/provider-standing-viewer.fga.yaml +0 -40
- package/src/authorization/__tests__/fixtures/fga/reserved-label-writer.fga.yaml +0 -41
- package/src/authorization/__tests__/fixtures/fga/schedule-owner.fga.yaml +0 -113
- package/src/authorization/__tests__/fixtures/fga/schedule-session-visibility.fga.yaml +0 -214
- package/src/authorization/__tests__/fixtures/fga/session-personal-resource.fga.yaml +0 -117
- package/src/authorization/__tests__/fixtures/fga/workflow-execution-sharing.fga.yaml +0 -190
- package/src/authorization/model/agent.ts +0 -64
- package/src/authorization/model/agent_channel.ts +0 -57
- package/src/authorization/model/agent_execution.ts +0 -33
- package/src/authorization/model/agent_instance.ts +0 -66
- package/src/authorization/model/agent_share.ts +0 -40
- package/src/authorization/model/api_key.ts +0 -28
- package/src/authorization/model/artifact.ts +0 -38
- package/src/authorization/model/channel_app.ts +0 -39
- package/src/authorization/model/environment.ts +0 -48
- package/src/authorization/model/execution_context.ts +0 -20
- package/src/authorization/model/iam_policy.ts +0 -42
- package/src/authorization/model/identity_account.ts +0 -26
- package/src/authorization/model/mcp_server.ts +0 -59
- package/src/authorization/model/memory.ts +0 -27
- package/src/authorization/model/oauth_app.ts +0 -43
- package/src/authorization/model/organization.ts +0 -59
- package/src/authorization/model/platform_client.ts +0 -43
- package/src/authorization/model/plugin.ts +0 -66
- package/src/authorization/model/schedule.ts +0 -49
- package/src/authorization/model/session.ts +0 -50
- package/src/authorization/model/skill.ts +0 -60
- package/src/authorization/model/workflow.ts +0 -60
- package/src/authorization/model/workflow_execution.ts +0 -46
- package/src/authorization/model/workflow_instance.ts +0 -125
- package/src/authorization/store-test-kit.ts +0 -395
|
@@ -15,6 +15,11 @@
|
|
|
15
15
|
* parent), which is what lets the parity pin run over all of them.
|
|
16
16
|
* `spec` and `status` overrides sit on top for the arms that need a
|
|
17
17
|
* pointer or a level set.
|
|
18
|
+
*
|
|
19
|
+
* A fixture row exists only for a kind this edition stores rows of: every
|
|
20
|
+
* type of the model but a rowless one (`platform`, model/bindings.ts), so
|
|
21
|
+
* the builders take a StoredKindDeclaration and `storedDeclaration`
|
|
22
|
+
* refuses a rowless type by name.
|
|
18
23
|
*/
|
|
19
24
|
import { create } from "@bufbuild/protobuf";
|
|
20
25
|
import type { DescMessage, Message } from "@bufbuild/protobuf";
|
|
@@ -26,6 +31,7 @@ import {
|
|
|
26
31
|
getKindMeta,
|
|
27
32
|
kindEnumName,
|
|
28
33
|
} from "../../pipeline/apiresource-meta.js";
|
|
34
|
+
import { builtInModel } from "../model/index.js";
|
|
29
35
|
import type { KindDeclaration } from "../model/rewrite.js";
|
|
30
36
|
|
|
31
37
|
export interface FixtureRowFacts {
|
|
@@ -39,6 +45,28 @@ export interface FixtureRowFacts {
|
|
|
39
45
|
readonly status?: Readonly<Record<string, unknown>>;
|
|
40
46
|
}
|
|
41
47
|
|
|
48
|
+
/** A declaration of a kind with stored rows: what a fixture row is built, saved and decoded with. */
|
|
49
|
+
export type StoredKindDeclaration = KindDeclaration & { readonly schema: DescMessage };
|
|
50
|
+
|
|
51
|
+
/** Whether rows of the declaration's kind are stored: every type but a rowless one. */
|
|
52
|
+
export function hasStoredRows(
|
|
53
|
+
declaration: KindDeclaration,
|
|
54
|
+
): declaration is StoredKindDeclaration {
|
|
55
|
+
return declaration.schema !== undefined;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** The built-in model's declaration of `type`, which must be a kind with stored rows. */
|
|
59
|
+
export function storedDeclaration(type: string): StoredKindDeclaration {
|
|
60
|
+
const declaration = builtInModel.byType(type);
|
|
61
|
+
if (declaration === undefined) {
|
|
62
|
+
throw new Error(`${type} is not a type of the model`);
|
|
63
|
+
}
|
|
64
|
+
if (!hasStoredRows(declaration)) {
|
|
65
|
+
throw new Error(`${type} is rowless: no row of it is stored`);
|
|
66
|
+
}
|
|
67
|
+
return declaration;
|
|
68
|
+
}
|
|
69
|
+
|
|
42
70
|
/** The id the fixture gives the parent of `parentKind`: `<type>-1`. */
|
|
43
71
|
export function fixtureParentId(parentKind: string): string {
|
|
44
72
|
return `${kindEnumName(getKindEnum(parentKind))}-1`;
|
|
@@ -46,7 +74,7 @@ export function fixtureParentId(parentKind: string): string {
|
|
|
46
74
|
|
|
47
75
|
/** A row of `declaration.kind` carrying exactly the given facts, its parents filled. */
|
|
48
76
|
export function fixtureRow(
|
|
49
|
-
declaration:
|
|
77
|
+
declaration: StoredKindDeclaration,
|
|
50
78
|
facts: FixtureRowFacts,
|
|
51
79
|
): Message {
|
|
52
80
|
// Typed loosely on purpose: the schema is any declared kind's, and
|
|
@@ -70,7 +98,7 @@ export function fixtureRow(
|
|
|
70
98
|
}
|
|
71
99
|
|
|
72
100
|
/** `kind_meta`'s parent spec fields for the kind, by generated name, each naming `<parentKind>-1`. */
|
|
73
|
-
function parentFieldsOf(declaration:
|
|
101
|
+
function parentFieldsOf(declaration: StoredKindDeclaration): Record<string, string> {
|
|
74
102
|
const config = getKindMeta(declaration.kind).authorization;
|
|
75
103
|
const spec = specSchemaOf(declaration.schema);
|
|
76
104
|
const fields: Record<string, string> = {};
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Builders for tests' throwaway models: a declaration written by hand in
|
|
3
|
+
* the evaluator's vocabulary (model/rewrite.ts), for the shapes the one
|
|
4
|
+
* real model must never hold (a cycle, a chain past the depth bound) and
|
|
5
|
+
* for proving a mechanism over a model small enough to read in the test.
|
|
6
|
+
* The built-in model is never written this way: it is the compiled
|
|
7
|
+
* OpenFGA JSON, read by model/openfga-json.ts.
|
|
8
|
+
*
|
|
9
|
+
* Nothing here validates: a test that declares a cycle means to. The
|
|
10
|
+
* builders are named so a declaration reads like the `.fga` line it
|
|
11
|
+
* stands for:
|
|
12
|
+
*
|
|
13
|
+
* define viewer: [identity_account, organization#viewer] or owner
|
|
14
|
+
* → union(direct(objectOf("identity_account"), usersetOf("organization", "viewer")), computed("owner"))
|
|
15
|
+
*/
|
|
16
|
+
import type { DescMessage } from "@bufbuild/protobuf";
|
|
17
|
+
|
|
18
|
+
import type { ApiResourceKind } from "@stigmer/protos/ai/stigmer/commons/apiresource/apiresourcekind/api_resource_kind_pb";
|
|
19
|
+
|
|
20
|
+
import { kindEnumName } from "../../pipeline/apiresource-meta.js";
|
|
21
|
+
import type {
|
|
22
|
+
DerivedRelation,
|
|
23
|
+
KindDeclaration,
|
|
24
|
+
Rewrite,
|
|
25
|
+
SubjectType,
|
|
26
|
+
} from "../model/rewrite.js";
|
|
27
|
+
|
|
28
|
+
export function objectOf(type: string): SubjectType {
|
|
29
|
+
return { form: "object", type };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function usersetOf(type: string, relation: string): SubjectType {
|
|
33
|
+
return { form: "userset", type, relation };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function direct(...subjects: ReadonlyArray<SubjectType>): Rewrite {
|
|
37
|
+
return { node: "this", subjects };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function computed(relation: string): Rewrite {
|
|
41
|
+
return { node: "computed", relation };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function from(relation: string, tupleset: string): Rewrite {
|
|
45
|
+
return { node: "from", relation, tupleset };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function union(...members: ReadonlyArray<Rewrite>): Rewrite {
|
|
49
|
+
return { node: "union", members };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function intersection(...members: ReadonlyArray<Rewrite>): Rewrite {
|
|
53
|
+
return { node: "intersection", members };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export interface ThrowawayDeclarationInput {
|
|
57
|
+
readonly kind: ApiResourceKind;
|
|
58
|
+
/** Omitted for a rowless type. */
|
|
59
|
+
readonly schema?: DescMessage;
|
|
60
|
+
readonly source?: string;
|
|
61
|
+
readonly relations: ReadonlyArray<readonly [name: string, rewrite: Rewrite]>;
|
|
62
|
+
readonly derived?: ReadonlyArray<readonly [name: string, rule: DerivedRelation]>;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** A declaration exactly as written, for `newModel`. */
|
|
66
|
+
export function throwawayDeclaration(input: ThrowawayDeclarationInput): KindDeclaration {
|
|
67
|
+
return {
|
|
68
|
+
kind: input.kind,
|
|
69
|
+
type: kindEnumName(input.kind),
|
|
70
|
+
schema: input.schema,
|
|
71
|
+
source: input.source ?? "throwaway",
|
|
72
|
+
relations: new Map(input.relations),
|
|
73
|
+
derived: new Map(input.derived ?? []),
|
|
74
|
+
};
|
|
75
|
+
}
|
|
@@ -1,83 +1,136 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Pins
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
2
|
+
* Pins the contract to the model over the whole wire: every question an
|
|
3
|
+
* RPC annotation asks the authorization model is a relation the model
|
|
4
|
+
* defines. The model (fga/model) and the contract (apis/, reaching this
|
|
5
|
+
* package as `@stigmer/protos`) are both this repository's, so their
|
|
6
|
+
* agreement is this repository's to keep, whichever edition serves the
|
|
7
|
+
* RPC. That is why the scope here is every service in `@stigmer/protos`,
|
|
8
|
+
* where authorize-annotation-completeness.test.ts scopes itself to the
|
|
9
|
+
* methods open source serves: that test governs the Authorize step, and a
|
|
10
|
+
* composition's own services are its own conformance suite's business;
|
|
11
|
+
* this one asks whether the model and the contract say the same thing.
|
|
11
12
|
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
13
|
+
* Two invariants:
|
|
14
|
+
* - every `rpc.config` annotation that names a static kind asks a
|
|
15
|
+
* permission that kind's type defines. A relation the type does not
|
|
16
|
+
* define answers `deny` in the built-in evaluator and a validation
|
|
17
|
+
* error in OpenFGA, so such an RPC refuses every caller, dark until
|
|
18
|
+
* someone runs it under enforcement;
|
|
19
|
+
* - the permission of every `resource_kind_path` lane (the IamPolicy
|
|
20
|
+
* lanes, whose kind is the request's) is defined on every kind whose
|
|
21
|
+
* `kind_meta` lists grantable roles, because each such kind is one the
|
|
22
|
+
* grant path admits.
|
|
23
|
+
*
|
|
24
|
+
* Outside the pin, as in the annotation-completeness precedent: `is_public`
|
|
25
|
+
* and `is_skip_authorization` methods (their annotation is never
|
|
26
|
+
* resolved), and a `config` that names no kind (`IamPolicyQueryController.get`,
|
|
27
|
+
* which authorizes the loaded policy's resource).
|
|
28
|
+
*
|
|
29
|
+
* What does not hold today is pinned, never dropped: `KNOWN_GAPS` lists
|
|
30
|
+
* each disagreement, and the test requires the computed list to equal it,
|
|
31
|
+
* so a fix to the model or the contract fails here until its line is
|
|
32
|
+
* removed.
|
|
33
|
+
*
|
|
34
|
+
* The walk reads the stubs' built `dist`. `make test-server` builds them
|
|
35
|
+
* first; a bare `vitest` run in a checkout whose `dist` is stale reads
|
|
36
|
+
* services the contract no longer has.
|
|
21
37
|
*/
|
|
22
|
-
import {
|
|
23
|
-
import {
|
|
38
|
+
import { readdirSync } from "node:fs";
|
|
39
|
+
import { createRequire } from "node:module";
|
|
24
40
|
import path from "node:path";
|
|
25
41
|
|
|
42
|
+
import type { DescService } from "@bufbuild/protobuf";
|
|
26
43
|
import { getOption, hasOption } from "@bufbuild/protobuf";
|
|
27
|
-
import {
|
|
44
|
+
import { beforeAll, describe, expect, it } from "vitest";
|
|
28
45
|
|
|
29
|
-
import {
|
|
46
|
+
import {
|
|
47
|
+
ApiResourceKind,
|
|
48
|
+
ApiResourceKindSchema,
|
|
49
|
+
} from "@stigmer/protos/ai/stigmer/commons/apiresource/apiresourcekind/api_resource_kind_pb";
|
|
30
50
|
import {
|
|
31
51
|
config as rpcAuthorizationConfig,
|
|
32
52
|
is_public,
|
|
33
53
|
is_skip_authorization,
|
|
34
54
|
} from "@stigmer/protos/ai/stigmer/commons/rpc/method_options_pb";
|
|
35
55
|
import { IamPermission } from "@stigmer/protos/ai/stigmer/iam/v1/enum_pb";
|
|
36
|
-
import { ServerEdition } from "@stigmer/protos/ai/stigmer/platform/v1/server_info_pb";
|
|
37
56
|
|
|
38
|
-
import {
|
|
39
|
-
import {
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
57
|
+
import { grantableRolesFor, kindEnumName } from "../../pipeline/apiresource-meta.js";
|
|
58
|
+
import { builtInModel } from "../model/index.js";
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Today's disagreements between the contract and the model, each with its
|
|
62
|
+
* reason. Removing a gap from the model or the contract fails the test
|
|
63
|
+
* until its line is removed here.
|
|
64
|
+
*/
|
|
65
|
+
const KNOWN_GAPS: ReadonlyArray<string> = [
|
|
66
|
+
// Contract ahead of model, on purpose: Plan's proto says the relation
|
|
67
|
+
// lands in the model with the entry that serves the kind
|
|
68
|
+
// (apis/ai/stigmer/billing/plan/v1/command.proto), and no edition serves
|
|
69
|
+
// Plan yet.
|
|
70
|
+
"ai.stigmer.billing.plan.v1.PlanCommandController/create asks can_manage_plans on platform",
|
|
71
|
+
"ai.stigmer.billing.plan.v1.PlanCommandController/retire asks can_manage_plans on platform",
|
|
72
|
+
// Artifacts are grantable in kind_meta, but the model defines no access
|
|
73
|
+
// relations on them: https://github.com/stigmer/stigmer/issues/1268.
|
|
74
|
+
"the resource_kind_path lanes ask can_grant_access on artifact",
|
|
75
|
+
"the resource_kind_path lanes ask can_view_access on artifact",
|
|
76
|
+
];
|
|
51
77
|
|
|
52
|
-
/** One annotated
|
|
78
|
+
/** One annotated method: the question it asks the model. */
|
|
53
79
|
interface WireQuestion {
|
|
54
80
|
readonly method: string;
|
|
81
|
+
/** The static kind, or unknown when the kind is the request's (`kindPath`). */
|
|
55
82
|
readonly kind: ApiResourceKind;
|
|
83
|
+
readonly kindPath: string;
|
|
56
84
|
readonly permission: string;
|
|
57
85
|
}
|
|
58
86
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
87
|
+
/** Every service descriptor the stubs package exports, loaded through its own specifiers. */
|
|
88
|
+
async function everyService(): Promise<ReadonlyArray<DescService>> {
|
|
89
|
+
const require = createRequire(import.meta.url);
|
|
90
|
+
const anchor = "ai/stigmer/iam/v1/enum_pb";
|
|
91
|
+
const resolved = require.resolve(`@stigmer/protos/${anchor}`);
|
|
92
|
+
const dist = resolved.slice(0, resolved.length - `${anchor}.js`.length);
|
|
93
|
+
const modules = readdirSync(path.join(dist, "ai"), { recursive: true, encoding: "utf8" })
|
|
94
|
+
.filter((file) => file.endsWith("_pb.js"))
|
|
95
|
+
.map((file) => `ai/${file.slice(0, -".js".length).split(path.sep).join("/")}`)
|
|
96
|
+
.sort();
|
|
97
|
+
const services: DescService[] = [];
|
|
98
|
+
for (const specifier of modules) {
|
|
99
|
+
const loaded: Record<string, unknown> = await import(`@stigmer/protos/${specifier}`);
|
|
100
|
+
for (const value of Object.values(loaded)) {
|
|
101
|
+
if (isService(value)) {
|
|
102
|
+
services.push(value);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return services;
|
|
107
|
+
}
|
|
62
108
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
109
|
+
function isService(value: unknown): value is DescService {
|
|
110
|
+
return (
|
|
111
|
+
typeof value === "object" &&
|
|
112
|
+
value !== null &&
|
|
113
|
+
"kind" in value &&
|
|
114
|
+
value.kind === "service" &&
|
|
115
|
+
"methods" in value
|
|
116
|
+
);
|
|
117
|
+
}
|
|
72
118
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
119
|
+
/** The model's relations on a kind, or none when the model does not define it. */
|
|
120
|
+
function relationsOf(kind: ApiResourceKind): ReadonlySet<string> {
|
|
121
|
+
return new Set(builtInModel.byKind(kind)?.relations.keys() ?? []);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
describe("the contract asks the model only what the model defines", () => {
|
|
125
|
+
let services: ReadonlyArray<DescService>;
|
|
126
|
+
|
|
127
|
+
beforeAll(async () => {
|
|
128
|
+
services = await everyService();
|
|
76
129
|
});
|
|
77
130
|
|
|
78
|
-
function
|
|
79
|
-
const
|
|
80
|
-
for (const service of
|
|
131
|
+
function annotated(): ReadonlyArray<WireQuestion> {
|
|
132
|
+
const out: WireQuestion[] = [];
|
|
133
|
+
for (const service of services) {
|
|
81
134
|
for (const method of service.methods) {
|
|
82
135
|
if (
|
|
83
136
|
!hasOption(method, rpcAuthorizationConfig) ||
|
|
@@ -87,68 +140,49 @@ describe("every static (kind, permission) the served RPCs ask about is a declare
|
|
|
87
140
|
continue;
|
|
88
141
|
}
|
|
89
142
|
const config = getOption(method, rpcAuthorizationConfig);
|
|
90
|
-
|
|
91
|
-
config.resourceKind === ApiResourceKind.api_resource_kind_unknown ||
|
|
92
|
-
!kindServedByEdition(config.resourceKind, ServerEdition.oss)
|
|
93
|
-
) {
|
|
94
|
-
continue;
|
|
95
|
-
}
|
|
96
|
-
questions.push({
|
|
143
|
+
out.push({
|
|
97
144
|
method: `${service.typeName}/${method.name}`,
|
|
98
145
|
kind: config.resourceKind,
|
|
146
|
+
kindPath: config.resourceKindPath,
|
|
99
147
|
permission: IamPermission[config.permission] ?? "",
|
|
100
148
|
});
|
|
101
149
|
}
|
|
102
150
|
}
|
|
103
|
-
return
|
|
151
|
+
return out;
|
|
104
152
|
}
|
|
105
153
|
|
|
106
|
-
it("
|
|
107
|
-
const questions =
|
|
108
|
-
expect(
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
(q) => declarationFor(q.kind)?.relations.has(q.permission) !== true,
|
|
112
|
-
)
|
|
113
|
-
.map(
|
|
114
|
-
(q) =>
|
|
115
|
-
`${q.method} asks ${q.permission} on ${kindEnumName(q.kind)}, which its transcript does not declare`,
|
|
116
|
-
);
|
|
117
|
-
expect(undeclared).toEqual([]);
|
|
154
|
+
it("reads the whole contract, the IamPolicy lanes included", () => {
|
|
155
|
+
const questions = annotated();
|
|
156
|
+
expect(services.length).toBeGreaterThan(60);
|
|
157
|
+
expect(questions.length).toBeGreaterThan(150);
|
|
158
|
+
expect(questions.some((q) => q.kindPath !== "")).toBe(true);
|
|
118
159
|
});
|
|
119
160
|
|
|
120
|
-
it("
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
"agent_share",
|
|
133
|
-
"api_key",
|
|
134
|
-
"artifact",
|
|
135
|
-
"channel_app",
|
|
136
|
-
"environment",
|
|
137
|
-
"execution_context",
|
|
138
|
-
"identity_account",
|
|
139
|
-
"mcp_server",
|
|
140
|
-
"memory",
|
|
141
|
-
"oauth_app",
|
|
142
|
-
"organization",
|
|
143
|
-
"platform_client",
|
|
144
|
-
"plugin",
|
|
145
|
-
"schedule",
|
|
146
|
-
"session",
|
|
147
|
-
"skill",
|
|
148
|
-
"workflow",
|
|
149
|
-
"workflow_execution",
|
|
150
|
-
"workflow_instance",
|
|
151
|
-
].sort(),
|
|
161
|
+
it("defines every relation the contract asks about, except the known gaps", () => {
|
|
162
|
+
const questions = annotated();
|
|
163
|
+
const gaps: string[] = [];
|
|
164
|
+
|
|
165
|
+
for (const q of questions) {
|
|
166
|
+
if (q.kind !== ApiResourceKind.api_resource_kind_unknown && !relationsOf(q.kind).has(q.permission)) {
|
|
167
|
+
gaps.push(`${q.method} asks ${q.permission} on ${kindEnumName(q.kind)}`);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const pathPermissions = new Set(
|
|
172
|
+
questions.filter((q) => q.kindPath !== "").map((q) => q.permission),
|
|
152
173
|
);
|
|
174
|
+
for (const value of ApiResourceKindSchema.values) {
|
|
175
|
+
const kind = value.number as ApiResourceKind;
|
|
176
|
+
if (kind === ApiResourceKind.api_resource_kind_unknown || grantableRolesFor(kind).length === 0) {
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
179
|
+
for (const permission of [...pathPermissions].sort()) {
|
|
180
|
+
if (!relationsOf(kind).has(permission)) {
|
|
181
|
+
gaps.push(`the resource_kind_path lanes ask ${permission} on ${kindEnumName(kind)}`);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
expect(gaps.sort()).toEqual([...KNOWN_GAPS].sort());
|
|
153
187
|
});
|
|
154
188
|
});
|
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
* tuples this row would have had, derived on the fly (evaluator.ts over
|
|
6
6
|
* derived-tuples.ts). Every arm below mirrors the cloud's driver
|
|
7
7
|
* (stigmer-cloud src/authorizer/fga-authorizer.ts) by name and copy, so
|
|
8
|
-
* the two editions answer one question the same way
|
|
9
|
-
*
|
|
8
|
+
* the two editions answer one question the same way, over the one model
|
|
9
|
+
* both read (model/index.ts).
|
|
10
10
|
*
|
|
11
11
|
* The arms, in order:
|
|
12
12
|
* 1. The cloud's four pre-checks with the Java service's copy, each a
|
|
@@ -87,7 +87,7 @@ import { formatObjectRef } from "./tuples.js";
|
|
|
87
87
|
* their rows live in its own tables, out of its probe's reach; matching
|
|
88
88
|
* the set here keeps the wire identical across editions (an unknown
|
|
89
89
|
* platform client's id is PERMISSION_DENIED with the annotation's copy in
|
|
90
|
-
* both). The cloud
|
|
90
|
+
* both). The cloud pins every kind here inside its own
|
|
91
91
|
* `PROBE_EXEMPT_KINDS`, which is why this set is exported; a kind that
|
|
92
92
|
* moves into open source joins this set in the same change.
|
|
93
93
|
*
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
* - owner SELF: <row>#owner@identity_account:<row id>
|
|
13
13
|
* - creator: <row>#creator@identity_account:<creator stamp> (kinds flagged requires_creator_tuple)
|
|
14
14
|
* - org-viewer: <row>#viewer@organization:<org>#viewer (visibility_org; cloud#257's shape)
|
|
15
|
-
* - platform-viewer: none — it fans out over identity providers,
|
|
15
|
+
* - platform-viewer: none — it fans out over identity providers, which this edition does not serve: the model defines the type, and no tuple of it is derived here
|
|
16
16
|
*
|
|
17
17
|
* Two facts are this edition's own and are stated here, nowhere else:
|
|
18
18
|
* - The ORGANIZATION's owner is a ROW, not a derivation. 2b's role
|
|
@@ -372,7 +372,11 @@ export function newDerivedTupleSource(
|
|
|
372
372
|
};
|
|
373
373
|
}
|
|
374
374
|
|
|
375
|
-
/**
|
|
375
|
+
/**
|
|
376
|
+
* The decoded row for an object, or undefined when the kind is not
|
|
377
|
+
* declared, is rowless (`platform`, which resolves over tuples alone), or
|
|
378
|
+
* the row does not exist.
|
|
379
|
+
*/
|
|
376
380
|
async function loadRow(
|
|
377
381
|
deps: Pick<DerivedTupleSourceDeps, "store" | "accounts">,
|
|
378
382
|
model: Model,
|
|
@@ -382,6 +386,7 @@ async function loadRow(
|
|
|
382
386
|
const kind = kindByEnumName(object.type);
|
|
383
387
|
if (
|
|
384
388
|
declaration === undefined ||
|
|
389
|
+
declaration.schema === undefined ||
|
|
385
390
|
kind === ApiResourceKind.api_resource_kind_unknown ||
|
|
386
391
|
object.id === ""
|
|
387
392
|
) {
|
|
@@ -30,11 +30,11 @@
|
|
|
30
30
|
* it is being resolved is a cycle in the declarations, which is a bug,
|
|
31
31
|
* and a resolution deeper than MAX_RESOLUTION_DEPTH is the same class of
|
|
32
32
|
* bug; both throw, because the alternative — a quiet `false` — would turn
|
|
33
|
-
* a broken
|
|
34
|
-
* not declare is a fault as the check's TARGET (the
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
33
|
+
* a broken model into a lockout nobody can see. A kind the model does
|
|
34
|
+
* not declare is a fault as the check's TARGET (the built-in model
|
|
35
|
+
* declares every kind, so reaching here is a registry gap) and simply an
|
|
36
|
+
* object with no relations when reached through a tuple (only a test's
|
|
37
|
+
* throwaway model omits a type).
|
|
38
38
|
* A relation the kind does not declare is `false` — the ruled answer for
|
|
39
39
|
* a wire question the model has no line for, where OpenFGA
|
|
40
40
|
* itself answers a validation error.
|
|
@@ -59,14 +59,12 @@ import { ACCOUNT_TYPE, formatObjectRef, pairKey } from "./tuples.js";
|
|
|
59
59
|
export const MAX_RESOLUTION_DEPTH = 25;
|
|
60
60
|
|
|
61
61
|
/**
|
|
62
|
-
* The faults an evaluation can raise.
|
|
63
|
-
* contracts (a kind the model does not declare
|
|
64
|
-
*
|
|
65
|
-
* answers before any driver); the rest are the model's own.
|
|
62
|
+
* The faults an evaluation can raise. One is a consumer bug by the seam
|
|
63
|
+
* contracts (a kind the model does not declare, or a rowless one asked
|
|
64
|
+
* for its rows); the rest are the model's own.
|
|
66
65
|
*/
|
|
67
66
|
export type EvaluationFault =
|
|
68
67
|
| "undeclared-target-kind"
|
|
69
|
-
| "internal-caller-offered"
|
|
70
68
|
| "resolution-depth-exceeded"
|
|
71
69
|
| "resolution-cycle";
|
|
72
70
|
|
|
@@ -40,27 +40,30 @@
|
|
|
40
40
|
* returns the org-narrowed rows for the class before any driver is asked
|
|
41
41
|
* (extensions/list-read-scope.ts, the header's contract line), the
|
|
42
42
|
* Authorize step's trust-domain rule applied to a list answer. So this
|
|
43
|
-
* driver's `restrictListEntries` REFUSES the class
|
|
44
|
-
* being offered it means a caller reached
|
|
45
|
-
* consumption idiom, and evaluating the server
|
|
46
|
-
* a quiet short list — the failure shape
|
|
47
|
-
* enumeration verb has no helper in front of it
|
|
48
|
-
* every id of the kind, as the built-in directory
|
|
49
|
-
* every organization. No in-process edge reaches
|
|
50
|
-
* today; the arm is pinned so a future one meets the
|
|
43
|
+
* driver's `restrictListEntries` REFUSES the class with the seam's
|
|
44
|
+
* `InternalCallerOfferedError`: being offered it means a caller reached
|
|
45
|
+
* the driver around the one consumption idiom, and evaluating the server
|
|
46
|
+
* as a person would return a quiet short list — the failure shape
|
|
47
|
+
* stigmer#1207 fixed. The enumeration verb has no helper in front of it
|
|
48
|
+
* and keeps its own arm: every id of the kind, as the built-in directory
|
|
49
|
+
* answers the class with every organization. No in-process edge reaches
|
|
50
|
+
* an enumeration lane today; the arm is pinned so a future one meets the
|
|
51
|
+
* rule.
|
|
51
52
|
*
|
|
52
53
|
* Faults THROW and are never an empty answer (the seam: an empty set means
|
|
53
54
|
* "authorized to see nothing"; an outage is the pipeline's sanitized
|
|
54
|
-
* INTERNAL). A kind the model does not declare,
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
* served lane
|
|
55
|
+
* INTERNAL). A kind the model does not declare, or a rowless one
|
|
56
|
+
* (`platform`, which has no stored rows to list), is, like the internal
|
|
57
|
+
* class above, a consumer bug by the seam's contract, and faults through
|
|
58
|
+
* the evaluator's own error, loud; every served list lane lists a kind
|
|
59
|
+
* with rows.
|
|
58
60
|
*
|
|
59
61
|
* Logging. One debug line per call — kind, offered, kept, elapsed — what
|
|
60
62
|
* an operator needs to answer "why is Alice's list short" beside the
|
|
61
63
|
* Authorizer's per-denial lines; never the token.
|
|
62
64
|
*/
|
|
63
65
|
import { fromBinary } from "@bufbuild/protobuf";
|
|
66
|
+
import type { DescMessage } from "@bufbuild/protobuf";
|
|
64
67
|
|
|
65
68
|
import type { ApiResourceKind } from "@stigmer/protos/ai/stigmer/commons/apiresource/apiresourcekind/api_resource_kind_pb";
|
|
66
69
|
|
|
@@ -71,6 +74,7 @@ import type {
|
|
|
71
74
|
ListEntryMeta,
|
|
72
75
|
ListReadScope,
|
|
73
76
|
} from "../extensions/list-read-scope.js";
|
|
77
|
+
import { InternalCallerOfferedError } from "../extensions/list-read-scope.js";
|
|
74
78
|
import { kindEnumName } from "../pipeline/apiresource-meta.js";
|
|
75
79
|
import type { Store } from "../store/interface.js";
|
|
76
80
|
import { newDerivedTupleSource } from "./derived-tuples.js";
|
|
@@ -87,6 +91,9 @@ import type { ObjectRef, Person } from "./tuples.js";
|
|
|
87
91
|
/** The relation every list lane reads through (the cloud's LIST_RELATION). */
|
|
88
92
|
const LIST_RELATION = "can_view";
|
|
89
93
|
|
|
94
|
+
/** A declaration of a kind with stored rows: the only kind a list lane can list. */
|
|
95
|
+
type ListableDeclaration = KindDeclaration & { readonly schema: DescMessage };
|
|
96
|
+
|
|
90
97
|
export interface BuiltInListReadScopeDeps {
|
|
91
98
|
readonly store: Store;
|
|
92
99
|
/** The port the grant path writes through — the person's rows are read from the same instance. */
|
|
@@ -153,8 +160,12 @@ export function newBuiltInListReadScope(
|
|
|
153
160
|
return kept;
|
|
154
161
|
}
|
|
155
162
|
|
|
156
|
-
/**
|
|
157
|
-
|
|
163
|
+
/**
|
|
164
|
+
* The declaration a verb evaluates against, which must be a kind with
|
|
165
|
+
* stored rows. An undeclared kind, or a rowless one, is the evaluator's
|
|
166
|
+
* own fault, raised before any read.
|
|
167
|
+
*/
|
|
168
|
+
function requireListable(kind: ApiResourceKind): ListableDeclaration {
|
|
158
169
|
const declaration = model.byKind(kind);
|
|
159
170
|
if (declaration === undefined) {
|
|
160
171
|
throw new AuthorizationEvaluationError(
|
|
@@ -162,11 +173,18 @@ export function newBuiltInListReadScope(
|
|
|
162
173
|
`no declaration for kind '${kindEnumName(kind)}' — a list lane asked the scope about a kind the model does not declare`,
|
|
163
174
|
);
|
|
164
175
|
}
|
|
165
|
-
|
|
176
|
+
const schema = declaration.schema;
|
|
177
|
+
if (schema === undefined) {
|
|
178
|
+
throw new AuthorizationEvaluationError(
|
|
179
|
+
"undeclared-target-kind",
|
|
180
|
+
`kind '${kindEnumName(kind)}' has no stored rows — a list lane asked the scope about a rowless kind`,
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
return { ...declaration, schema };
|
|
166
184
|
}
|
|
167
185
|
|
|
168
186
|
/** Every row of `kind` the store holds, as facts; a row that does not decode is skipped. */
|
|
169
|
-
async function everyRowOf(declaration:
|
|
187
|
+
async function everyRowOf(declaration: ListableDeclaration): Promise<RowFacts[]> {
|
|
170
188
|
const kind = declaration.kind;
|
|
171
189
|
const rows = await deps.store.listResources(kind);
|
|
172
190
|
const facts: RowFacts[] = [];
|
|
@@ -189,12 +207,9 @@ export function newBuiltInListReadScope(
|
|
|
189
207
|
kind: ApiResourceKind,
|
|
190
208
|
entries: ReadonlyArray<ListEntryMeta>,
|
|
191
209
|
): Promise<ReadonlySet<string>> {
|
|
192
|
-
|
|
210
|
+
requireListable(kind);
|
|
193
211
|
if (caller.callerClass === "internal") {
|
|
194
|
-
throw new
|
|
195
|
-
"internal-caller-offered",
|
|
196
|
-
`the internal caller class was offered to the list scope for kind '${kindEnumName(kind)}' — the shared helper answers that class before any driver; a list lane reached the driver around it`,
|
|
197
|
-
);
|
|
212
|
+
throw new InternalCallerOfferedError(kind);
|
|
198
213
|
}
|
|
199
214
|
// One candidate per unique id: a lane offers each row once, and a
|
|
200
215
|
// duplicate would only spend a walk to learn the same answer.
|
|
@@ -211,7 +226,7 @@ export function newBuiltInListReadScope(
|
|
|
211
226
|
caller: CallerIdentity,
|
|
212
227
|
kind: ApiResourceKind,
|
|
213
228
|
): Promise<ReadonlySet<string>> {
|
|
214
|
-
const candidates = await everyRowOf(
|
|
229
|
+
const candidates = await everyRowOf(requireListable(kind));
|
|
215
230
|
if (caller.callerClass === "internal") {
|
|
216
231
|
return new Set(candidates.map((facts) => facts.id));
|
|
217
232
|
}
|