@stigmer/server 3.15.2 → 3.15.3-dev.20260914100347
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/dist/boot/compose.d.ts.map +1 -1
- package/dist/boot/compose.js +103 -9
- package/dist/boot/compose.js.map +1 -1
- package/dist/domain/iampolicy/access-lists.d.ts +27 -0
- package/dist/domain/iampolicy/access-lists.d.ts.map +1 -0
- package/dist/domain/iampolicy/access-lists.js +124 -0
- package/dist/domain/iampolicy/access-lists.js.map +1 -0
- package/dist/domain/iampolicy/constants.d.ts +135 -0
- package/dist/domain/iampolicy/constants.d.ts.map +1 -0
- package/dist/domain/iampolicy/constants.js +203 -0
- package/dist/domain/iampolicy/constants.js.map +1 -0
- package/dist/domain/iampolicy/controller.d.ts +29 -0
- package/dist/domain/iampolicy/controller.d.ts.map +1 -0
- package/dist/domain/iampolicy/controller.js +451 -0
- package/dist/domain/iampolicy/controller.js.map +1 -0
- package/dist/domain/iampolicy/display-resolver.d.ts +6 -0
- package/dist/domain/iampolicy/display-resolver.d.ts.map +1 -0
- package/dist/domain/iampolicy/display-resolver.js +90 -0
- package/dist/domain/iampolicy/display-resolver.js.map +1 -0
- package/dist/domain/iampolicy/grant-path.d.ts +42 -0
- package/dist/domain/iampolicy/grant-path.d.ts.map +1 -0
- package/dist/domain/iampolicy/grant-path.js +183 -0
- package/dist/domain/iampolicy/grant-path.js.map +1 -0
- package/dist/domain/iampolicy/grant-scope.d.ts +4 -0
- package/dist/domain/iampolicy/grant-scope.d.ts.map +1 -0
- package/dist/domain/iampolicy/grant-scope.js +34 -0
- package/dist/domain/iampolicy/grant-scope.js.map +1 -0
- package/dist/domain/iampolicy/membership.d.ts +111 -0
- package/dist/domain/iampolicy/membership.d.ts.map +1 -0
- package/dist/domain/iampolicy/membership.js +152 -0
- package/dist/domain/iampolicy/membership.js.map +1 -0
- package/dist/domain/iampolicy/permissions.d.ts +19 -0
- package/dist/domain/iampolicy/permissions.d.ts.map +1 -0
- package/dist/domain/iampolicy/permissions.js +27 -0
- package/dist/domain/iampolicy/permissions.js.map +1 -0
- package/dist/domain/iampolicy/resource-store.d.ts +4 -0
- package/dist/domain/iampolicy/resource-store.d.ts.map +1 -0
- package/dist/domain/iampolicy/resource-store.js +135 -0
- package/dist/domain/iampolicy/resource-store.js.map +1 -0
- package/dist/domain/iampolicy/role-lifecycle.d.ts +9 -0
- package/dist/domain/iampolicy/role-lifecycle.d.ts.map +1 -0
- package/dist/domain/iampolicy/role-lifecycle.js +106 -0
- package/dist/domain/iampolicy/role-lifecycle.js.map +1 -0
- package/dist/domain/iampolicy/roles.d.ts +11 -0
- package/dist/domain/iampolicy/roles.d.ts.map +1 -0
- package/dist/domain/iampolicy/roles.js +78 -0
- package/dist/domain/iampolicy/roles.js.map +1 -0
- package/dist/domain/iampolicy/specs.d.ts +7 -0
- package/dist/domain/iampolicy/specs.d.ts.map +1 -0
- package/dist/domain/iampolicy/specs.js +45 -0
- package/dist/domain/iampolicy/specs.js.map +1 -0
- package/dist/domain/iampolicy/steps.d.ts +18 -0
- package/dist/domain/iampolicy/steps.d.ts.map +1 -0
- package/dist/domain/iampolicy/steps.js +117 -0
- package/dist/domain/iampolicy/steps.js.map +1 -0
- package/dist/domain/iampolicy/store-contract.d.ts +8 -0
- package/dist/domain/iampolicy/store-contract.d.ts.map +1 -0
- package/dist/domain/iampolicy/store-contract.js +258 -0
- package/dist/domain/iampolicy/store-contract.js.map +1 -0
- package/dist/domain/iampolicy/store.d.ts +78 -0
- package/dist/domain/iampolicy/store.d.ts.map +1 -0
- package/dist/domain/iampolicy/store.js +13 -0
- package/dist/domain/iampolicy/store.js.map +1 -0
- package/dist/domain/iampolicy/wire-refusals.d.ts +25 -0
- package/dist/domain/iampolicy/wire-refusals.d.ts.map +1 -0
- package/dist/domain/iampolicy/wire-refusals.js +76 -0
- package/dist/domain/iampolicy/wire-refusals.js.map +1 -0
- package/dist/domain/identityaccount/constants.d.ts +33 -0
- package/dist/domain/identityaccount/constants.d.ts.map +1 -1
- package/dist/domain/identityaccount/constants.js +2 -48
- package/dist/domain/identityaccount/constants.js.map +1 -1
- package/dist/domain/identityaccount/controller.d.ts +8 -1
- package/dist/domain/identityaccount/controller.d.ts.map +1 -1
- package/dist/domain/identityaccount/controller.js +54 -29
- package/dist/domain/identityaccount/controller.js.map +1 -1
- package/dist/domain/identityaccount/operator.d.ts.map +1 -1
- package/dist/domain/identityaccount/operator.js +2 -4
- package/dist/domain/identityaccount/operator.js.map +1 -1
- package/dist/domain/identityaccount/provisioning.d.ts +20 -1
- package/dist/domain/identityaccount/provisioning.d.ts.map +1 -1
- package/dist/domain/identityaccount/provisioning.js +17 -3
- package/dist/domain/identityaccount/provisioning.js.map +1 -1
- package/dist/domain/identityaccount/resolve.d.ts +23 -0
- package/dist/domain/identityaccount/resolve.d.ts.map +1 -1
- package/dist/domain/identityaccount/resolve.js +17 -0
- package/dist/domain/identityaccount/resolve.js.map +1 -1
- package/dist/domain/identityaccount/store-contract.d.ts +4 -18
- package/dist/domain/identityaccount/store-contract.d.ts.map +1 -1
- package/dist/domain/identityaccount/store-contract.js +12 -47
- package/dist/domain/identityaccount/store-contract.js.map +1 -1
- package/dist/extensions/authorization-queries.d.ts +81 -0
- package/dist/extensions/authorization-queries.d.ts.map +1 -0
- package/dist/extensions/authorization-queries.js +2 -0
- package/dist/extensions/authorization-queries.js.map +1 -0
- package/dist/extensions/drivers.d.ts +49 -2
- package/dist/extensions/drivers.d.ts.map +1 -1
- package/dist/extensions/identity.d.ts +14 -0
- package/dist/extensions/identity.d.ts.map +1 -1
- package/dist/extensions/identity.js +18 -1
- package/dist/extensions/identity.js.map +1 -1
- package/dist/extensions/policy-grant-scope.d.ts +50 -0
- package/dist/extensions/policy-grant-scope.d.ts.map +1 -0
- package/dist/extensions/policy-grant-scope.js +2 -0
- package/dist/extensions/policy-grant-scope.js.map +1 -0
- package/dist/extensions/registry.d.ts +23 -1
- package/dist/extensions/registry.d.ts.map +1 -1
- package/dist/extensions/registry.js +30 -0
- package/dist/extensions/registry.js.map +1 -1
- package/dist/extensions/resource-authorization.d.ts +63 -0
- package/dist/extensions/resource-authorization.d.ts.map +1 -1
- package/dist/index.d.ts +7 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/pipeline/apiresource-meta.d.ts +56 -1
- package/dist/pipeline/apiresource-meta.d.ts.map +1 -1
- package/dist/pipeline/apiresource-meta.js +155 -1
- package/dist/pipeline/apiresource-meta.js.map +1 -1
- package/dist/pipeline/interceptors/auth.d.ts +12 -0
- package/dist/pipeline/interceptors/auth.d.ts.map +1 -1
- package/dist/pipeline/interceptors/auth.js +13 -1
- package/dist/pipeline/interceptors/auth.js.map +1 -1
- package/dist/pipeline/steps/authorize.d.ts +20 -5
- package/dist/pipeline/steps/authorize.d.ts.map +1 -1
- package/dist/pipeline/steps/authorize.js +37 -17
- package/dist/pipeline/steps/authorize.js.map +1 -1
- package/dist/pipeline/steps/defaults.d.ts +10 -0
- package/dist/pipeline/steps/defaults.d.ts.map +1 -1
- package/dist/pipeline/steps/defaults.js +39 -0
- package/dist/pipeline/steps/defaults.js.map +1 -1
- package/dist/store/port-contract.d.ts +61 -0
- package/dist/store/port-contract.d.ts.map +1 -0
- package/dist/store/port-contract.js +67 -0
- package/dist/store/port-contract.js.map +1 -0
- package/package.json +4 -4
- package/src/boot/compose.ts +107 -10
- package/src/domain/iampolicy/__tests__/access-lists.test.ts +238 -0
- package/src/domain/iampolicy/__tests__/constants.test.ts +388 -0
- package/src/domain/iampolicy/__tests__/controller.test.ts +960 -0
- package/src/domain/iampolicy/__tests__/display-resolver.test.ts +138 -0
- package/src/domain/iampolicy/__tests__/grant-path.test.ts +474 -0
- package/src/domain/iampolicy/__tests__/grant-scope.test.ts +61 -0
- package/src/domain/iampolicy/__tests__/iampolicy.test.ts +532 -0
- package/src/domain/iampolicy/__tests__/membership.test.ts +447 -0
- package/src/domain/iampolicy/__tests__/permissions.test.ts +41 -0
- package/src/domain/iampolicy/__tests__/resource-store.test.ts +165 -0
- package/src/domain/iampolicy/__tests__/role-lifecycle.test.ts +361 -0
- package/src/domain/iampolicy/__tests__/roles.test.ts +53 -0
- package/src/domain/iampolicy/__tests__/specs.test.ts +41 -0
- package/src/domain/iampolicy/__tests__/steps.test.ts +278 -0
- package/src/domain/iampolicy/__tests__/support.ts +195 -0
- package/src/domain/iampolicy/__tests__/wire-refusals.test.ts +117 -0
- package/src/domain/iampolicy/access-lists.ts +191 -0
- package/src/domain/iampolicy/constants.ts +243 -0
- package/src/domain/iampolicy/controller.ts +798 -0
- package/src/domain/iampolicy/display-resolver.ts +123 -0
- package/src/domain/iampolicy/grant-path.ts +270 -0
- package/src/domain/iampolicy/grant-scope.ts +36 -0
- package/src/domain/iampolicy/membership.ts +298 -0
- package/src/domain/iampolicy/permissions.ts +35 -0
- package/src/domain/iampolicy/resource-store.ts +186 -0
- package/src/domain/iampolicy/role-lifecycle.ts +132 -0
- package/src/domain/iampolicy/roles.ts +95 -0
- package/src/domain/iampolicy/specs.ts +53 -0
- package/src/domain/iampolicy/steps.ts +178 -0
- package/src/domain/iampolicy/store-contract.ts +450 -0
- package/src/domain/iampolicy/store.ts +103 -0
- package/src/domain/iampolicy/wire-refusals.ts +110 -0
- package/src/domain/identityaccount/__tests__/provisioning.test.ts +19 -7
- package/src/domain/identityaccount/__tests__/resolve.test.ts +113 -2
- package/src/domain/identityaccount/constants.ts +16 -31
- package/src/domain/identityaccount/controller.ts +65 -35
- package/src/domain/identityaccount/operator.ts +5 -5
- package/src/domain/identityaccount/provisioning.ts +43 -5
- package/src/domain/identityaccount/resolve.ts +43 -0
- package/src/domain/identityaccount/store-contract.ts +22 -70
- package/src/extensions/__tests__/composed-support.ts +87 -0
- package/src/extensions/__tests__/iam-policy-composed.test.ts +373 -0
- package/src/extensions/__tests__/iam-policy-points.test.ts +169 -0
- package/src/extensions/__tests__/identity-account-composed.test.ts +12 -78
- package/src/extensions/__tests__/identity.test.ts +62 -0
- package/src/extensions/__tests__/tier-truthfulness.test.ts +8 -0
- package/src/extensions/authorization-queries.ts +97 -0
- package/src/extensions/drivers.ts +49 -2
- package/src/extensions/identity.ts +21 -0
- package/src/extensions/policy-grant-scope.ts +50 -0
- package/src/extensions/registry.ts +62 -1
- package/src/extensions/resource-authorization.ts +65 -0
- package/src/index.ts +20 -0
- package/src/pipeline/__tests__/grantable-roles-for.test.ts +87 -0
- package/src/pipeline/__tests__/kind-by-enum-name.test.ts +107 -0
- package/src/pipeline/__tests__/kind-served-by-edition.test.ts +107 -0
- package/src/pipeline/apiresource-meta.ts +181 -1
- package/src/pipeline/interceptors/auth.ts +14 -1
- package/src/pipeline/steps/__tests__/authorize-annotation-completeness.test.ts +73 -3
- package/src/pipeline/steps/__tests__/authorize.test.ts +134 -9
- package/src/pipeline/steps/__tests__/derived-id.test.ts +49 -0
- package/src/pipeline/steps/authorize.ts +46 -21
- package/src/pipeline/steps/defaults.ts +42 -0
- package/src/store/__tests__/port-contract.test.ts +123 -0
- package/src/store/port-contract.ts +102 -0
|
@@ -34,7 +34,10 @@
|
|
|
34
34
|
* points (20260911.11) at the identity-accounts stage (the store driver,
|
|
35
35
|
* ahead of the boot-time operator ensure) and the routes stage (the
|
|
36
36
|
* federation capability and the provision slot's steps, into the
|
|
37
|
-
* identity-account controller).
|
|
37
|
+
* identity-account controller); the IamPolicy points (20260913.01) at
|
|
38
|
+
* the same two stages — the store driver where the grant path is built,
|
|
39
|
+
* beside the identity-account store, and the grant scope and the query
|
|
40
|
+
* engine into the IamPolicy controller at the routes stage.
|
|
38
41
|
*/
|
|
39
42
|
import type { DescMessage } from "@bufbuild/protobuf";
|
|
40
43
|
import type { ConnectRouter } from "@connectrpc/connect";
|
|
@@ -45,10 +48,13 @@ import type { ArtifactStorageDriverFactory } from "../artifactstorage/artifact-s
|
|
|
45
48
|
import { BUILT_IN_STORAGE_TYPES } from "../artifactstorage/artifact-storage.js";
|
|
46
49
|
import type { ChannelRuntime } from "../domain/agentchannel/channel-runtime.js";
|
|
47
50
|
import type { IdentityAccountStore } from "../domain/identityaccount/store.js";
|
|
51
|
+
import type { IamPolicyStore } from "../domain/iampolicy/store.js";
|
|
48
52
|
import type { SecretCodec } from "../encryption/codec.js";
|
|
49
53
|
import { V1_VERSION } from "../encryption/v1-codec.js";
|
|
54
|
+
import type { AuthorizationQueryEngine } from "./authorization-queries.js";
|
|
50
55
|
import type { IdentityFederation } from "./identity-federation.js";
|
|
51
56
|
import type { ListReadScope } from "./list-read-scope.js";
|
|
57
|
+
import type { PolicyGrantScope } from "./policy-grant-scope.js";
|
|
52
58
|
import type { ModelCatalogProvider } from "../domain/workflow/registry/model-catalog-provider.js";
|
|
53
59
|
import type { VisitorErrorPolicy } from "../pipeline/interceptors/error-boundary.js";
|
|
54
60
|
import type { PipelineStep } from "../pipeline/pipeline.js";
|
|
@@ -250,6 +256,22 @@ export interface ResolvedExtensionDrivers {
|
|
|
250
256
|
* four federated RPCs refuse UNIMPLEMENTED with the edition reason.
|
|
251
257
|
*/
|
|
252
258
|
readonly identityFederation: IdentityFederation | undefined;
|
|
259
|
+
/**
|
|
260
|
+
* The 20260913.01 IamPolicy store driver — undefined = the compose.ts
|
|
261
|
+
* stage that builds the grant path installs the OSS adapter over the
|
|
262
|
+
* generic Store, OSS behavior byte-identical.
|
|
263
|
+
*/
|
|
264
|
+
readonly iamPolicyStore: IamPolicyStore | undefined;
|
|
265
|
+
/**
|
|
266
|
+
* The 20260913.01 policy grant scope — undefined = compose.ts installs
|
|
267
|
+
* open source's organization-only default.
|
|
268
|
+
*/
|
|
269
|
+
readonly policyGrantScope: PolicyGrantScope | undefined;
|
|
270
|
+
/**
|
|
271
|
+
* The 20260913.01 authorization-query engine — undefined = the
|
|
272
|
+
* tuple-half RPC arms refuse UNIMPLEMENTED with the edition reason.
|
|
273
|
+
*/
|
|
274
|
+
readonly authorizationQueries: AuthorizationQueryEngine | undefined;
|
|
253
275
|
}
|
|
254
276
|
|
|
255
277
|
/**
|
|
@@ -297,6 +319,12 @@ export function resolveExtensions(
|
|
|
297
319
|
let identityAccountStoreDeclaredBy: string | undefined;
|
|
298
320
|
let identityFederation: IdentityFederation | undefined;
|
|
299
321
|
let identityFederationDeclaredBy: string | undefined;
|
|
322
|
+
let iamPolicyStore: IamPolicyStore | undefined;
|
|
323
|
+
let iamPolicyStoreDeclaredBy: string | undefined;
|
|
324
|
+
let policyGrantScope: PolicyGrantScope | undefined;
|
|
325
|
+
let policyGrantScopeDeclaredBy: string | undefined;
|
|
326
|
+
let authorizationQueries: AuthorizationQueryEngine | undefined;
|
|
327
|
+
let authorizationQueriesDeclaredBy: string | undefined;
|
|
300
328
|
const artifactStorageDrivers = new Map<
|
|
301
329
|
string,
|
|
302
330
|
ArtifactStorageDriverFactory
|
|
@@ -458,6 +486,36 @@ export function resolveExtensions(
|
|
|
458
486
|
identityFederationDeclaredBy = unit.name;
|
|
459
487
|
}
|
|
460
488
|
|
|
489
|
+
if (unit.drivers?.iamPolicyStore !== undefined) {
|
|
490
|
+
if (iamPolicyStoreDeclaredBy !== undefined) {
|
|
491
|
+
throw new Error(
|
|
492
|
+
`extension '${unit.name}' registers an IamPolicyStore, but '${iamPolicyStoreDeclaredBy}' already did — exactly one may be composed`,
|
|
493
|
+
);
|
|
494
|
+
}
|
|
495
|
+
iamPolicyStore = unit.drivers.iamPolicyStore;
|
|
496
|
+
iamPolicyStoreDeclaredBy = unit.name;
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
if (unit.drivers?.policyGrantScope !== undefined) {
|
|
500
|
+
if (policyGrantScopeDeclaredBy !== undefined) {
|
|
501
|
+
throw new Error(
|
|
502
|
+
`extension '${unit.name}' registers a PolicyGrantScope, but '${policyGrantScopeDeclaredBy}' already did — exactly one may be composed`,
|
|
503
|
+
);
|
|
504
|
+
}
|
|
505
|
+
policyGrantScope = unit.drivers.policyGrantScope;
|
|
506
|
+
policyGrantScopeDeclaredBy = unit.name;
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
if (unit.drivers?.authorizationQueries !== undefined) {
|
|
510
|
+
if (authorizationQueriesDeclaredBy !== undefined) {
|
|
511
|
+
throw new Error(
|
|
512
|
+
`extension '${unit.name}' registers an AuthorizationQueryEngine, but '${authorizationQueriesDeclaredBy}' already did — exactly one may be composed`,
|
|
513
|
+
);
|
|
514
|
+
}
|
|
515
|
+
authorizationQueries = unit.drivers.authorizationQueries;
|
|
516
|
+
authorizationQueriesDeclaredBy = unit.name;
|
|
517
|
+
}
|
|
518
|
+
|
|
461
519
|
if (unit.drivers?.artifactStorageDrivers !== undefined) {
|
|
462
520
|
for (const [name, factory] of unit.drivers.artifactStorageDrivers) {
|
|
463
521
|
if ((BUILT_IN_STORAGE_TYPES as ReadonlyArray<string>).includes(name)) {
|
|
@@ -580,6 +638,9 @@ export function resolveExtensions(
|
|
|
580
638
|
scheduleFireCaller,
|
|
581
639
|
identityAccountStore,
|
|
582
640
|
identityFederation,
|
|
641
|
+
iamPolicyStore,
|
|
642
|
+
policyGrantScope,
|
|
643
|
+
authorizationQueries,
|
|
583
644
|
},
|
|
584
645
|
services,
|
|
585
646
|
workers,
|
|
@@ -26,9 +26,37 @@
|
|
|
26
26
|
* - onVisibilityChanged: SYNCHRONOUS, post-persist; a throw fails the
|
|
27
27
|
* request (metadata may be persisted with tuples lagging — retrying
|
|
28
28
|
* the same transition converges, the set-diff is idempotent).
|
|
29
|
+
*
|
|
30
|
+
* Corrected 2026-09-13 (20260913.01, T01_1_review.md Q-OR-6, Q-OR-10):
|
|
31
|
+
* "no driver composed = OSS behavior byte-identical" described the seam
|
|
32
|
+
* while the only writer of authorization records was the cloud's tuple
|
|
33
|
+
* driver. The seam now records authorization at lifecycle points in EVERY
|
|
34
|
+
* edition: open source's built-in role lifecycle
|
|
35
|
+
* (domain/iampolicy/role-lifecycle.ts, the entry's slice 4) writes
|
|
36
|
+
* IamPolicy rows (the organization creator's `owner` row; cleanup on
|
|
37
|
+
* delete), and a composed driver writes tuples too. The two POLICY hooks below are the
|
|
38
|
+
* other direction of the same seam — the IamPolicy domain's one grant and
|
|
39
|
+
* revoke path (domain/iampolicy/grant-path.ts) telling the driver that a
|
|
40
|
+
* row was written or is about to be deleted, so a composition can mirror
|
|
41
|
+
* the row as a tuple. Their order is the cloud#425 invariant, fixed by the
|
|
42
|
+
* path and stated on each event:
|
|
43
|
+
* - onPolicyGranted: SYNCHRONOUS, AFTER the row is persisted; fires on
|
|
44
|
+
* the duplicate arm too (the inline heal for a row whose tuple never
|
|
45
|
+
* landed). A throw fails the request with the row in place — the
|
|
46
|
+
* caller's retry or a boot backfill heals it.
|
|
47
|
+
* - onPolicyRevoked: SYNCHRONOUS, BEFORE the row is deleted. A throw
|
|
48
|
+
* leaves row and tuple, so a retry converges; the reverse order was
|
|
49
|
+
* the fail-open incident class ("revoke" silently becoming "keep").
|
|
50
|
+
* On the absent arm (no row for the spec) it fires with the spec and
|
|
51
|
+
* no policy, so a composition can still delete a bare tuple written
|
|
52
|
+
* before the row mirror existed.
|
|
53
|
+
* Absent method = no mirror is written (the OSS posture: rows are the
|
|
54
|
+
* record).
|
|
29
55
|
*/
|
|
30
56
|
import type { ApiResourceKind } from "@stigmer/protos/ai/stigmer/commons/apiresource/apiresourcekind/api_resource_kind_pb";
|
|
31
57
|
import type { OwnerAttributionType } from "@stigmer/protos/ai/stigmer/commons/apiresource/apiresourcekind/authorization_config_pb";
|
|
58
|
+
import type { IamPolicy } from "@stigmer/protos/ai/stigmer/iam/iampolicy/v1/api_pb";
|
|
59
|
+
import type { IamPolicySpec } from "@stigmer/protos/ai/stigmer/iam/iampolicy/v1/spec_pb";
|
|
32
60
|
|
|
33
61
|
import type { CallerIdentity } from "./identity.js";
|
|
34
62
|
|
|
@@ -125,6 +153,31 @@ export interface DefaultInstanceLinkedEvent {
|
|
|
125
153
|
readonly blueprintId: string;
|
|
126
154
|
}
|
|
127
155
|
|
|
156
|
+
/**
|
|
157
|
+
* Fired synchronously AFTER an IamPolicy row is persisted by the domain's
|
|
158
|
+
* grant path — and on the duplicate arm, when the triple was already held
|
|
159
|
+
* and no row was written, so a composition heals a row whose tuple never
|
|
160
|
+
* landed (cloud#425's inline heal). `policy` is the row as stored: on the
|
|
161
|
+
* duplicate arm the FIRST writer's row, whose id may be a legacy random
|
|
162
|
+
* one on a composition that predates derived ids.
|
|
163
|
+
*/
|
|
164
|
+
export interface PolicyGrantedEvent {
|
|
165
|
+
readonly policy: IamPolicy;
|
|
166
|
+
/** True when the triple was already held and this grant wrote nothing. */
|
|
167
|
+
readonly duplicate: boolean;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Fired synchronously BEFORE an IamPolicy row is deleted by the domain's
|
|
172
|
+
* grant path. `spec` is always the triple being revoked; `policy` is the
|
|
173
|
+
* row found for it, or `undefined` on the absent arm — the spec alone is
|
|
174
|
+
* enough to name the tuple a composition still deletes then.
|
|
175
|
+
*/
|
|
176
|
+
export interface PolicyRevokedEvent {
|
|
177
|
+
readonly spec: IamPolicySpec;
|
|
178
|
+
readonly policy: IamPolicy | undefined;
|
|
179
|
+
}
|
|
180
|
+
|
|
128
181
|
/**
|
|
129
182
|
* The driver interface (single-instance point, registered via
|
|
130
183
|
* ExtensionDrivers.resourceAuthorizationLifecycle). Implementations must
|
|
@@ -142,4 +195,16 @@ export interface ResourceAuthorizationLifecycle {
|
|
|
142
195
|
* (the OSS posture: local access control needs none).
|
|
143
196
|
*/
|
|
144
197
|
onDefaultInstanceLinked?(event: DefaultInstanceLinkedEvent): Promise<void>;
|
|
198
|
+
/**
|
|
199
|
+
* OPTIONAL (added 20260913.01 slice 2): synchronous, AFTER the row
|
|
200
|
+
* persist, on the duplicate arm too; a throw fails the grant with the
|
|
201
|
+
* row in place. Absent method = rows are the record (the OSS posture).
|
|
202
|
+
*/
|
|
203
|
+
onPolicyGranted?(event: PolicyGrantedEvent): Promise<void>;
|
|
204
|
+
/**
|
|
205
|
+
* OPTIONAL (added 20260913.01 slice 2): synchronous, BEFORE the row
|
|
206
|
+
* delete; a throw leaves row and tuple for the retry. Absent method =
|
|
207
|
+
* rows are the record (the OSS posture).
|
|
208
|
+
*/
|
|
209
|
+
onPolicyRevoked?(event: PolicyRevokedEvent): Promise<void>;
|
|
145
210
|
}
|
package/src/index.ts
CHANGED
|
@@ -100,6 +100,8 @@ export type { ResolvedExtensionDrivers } from "./extensions/registry.js";
|
|
|
100
100
|
// tests pin against.
|
|
101
101
|
export type {
|
|
102
102
|
DefaultInstanceLinkedEvent,
|
|
103
|
+
PolicyGrantedEvent,
|
|
104
|
+
PolicyRevokedEvent,
|
|
103
105
|
ResourceAuthorizationLifecycle,
|
|
104
106
|
ResourceCreatedEvent,
|
|
105
107
|
ResourceDeletedEvent,
|
|
@@ -125,6 +127,24 @@ export type {
|
|
|
125
127
|
IdentityAccountStoreContractFixture,
|
|
126
128
|
} from "./domain/identityaccount/store-contract.js";
|
|
127
129
|
export { identityAccountStoreContract } from "./domain/identityaccount/store-contract.js";
|
|
130
|
+
// The 20260913.01 IamPolicy seams (Q-OR-1, Q-OR-10): the store PORT a
|
|
131
|
+
// composition drives the domain's grant path through (drivers.iamPolicyStore;
|
|
132
|
+
// a driver throws DuplicatePolicyError for a held id) and its vitest-free
|
|
133
|
+
// contract kit; the grant scope (drivers.policyGrantScope — which kinds an
|
|
134
|
+
// edition grants on, with which roles; open source's default is the
|
|
135
|
+
// organization alone); and the tuple-half query engine
|
|
136
|
+
// (drivers.authorizationQueries — the three graph questions only an
|
|
137
|
+
// authorization backend answers). The grant path itself is NOT exported: a
|
|
138
|
+
// composition reaches it as in-process RPCs, the doctrine.
|
|
139
|
+
export type { IamPolicyStore } from "./domain/iampolicy/store.js";
|
|
140
|
+
export { DuplicatePolicyError } from "./domain/iampolicy/store.js";
|
|
141
|
+
export type {
|
|
142
|
+
IamPolicyStoreContractCase,
|
|
143
|
+
IamPolicyStoreContractFixture,
|
|
144
|
+
} from "./domain/iampolicy/store-contract.js";
|
|
145
|
+
export { iamPolicyStoreContract } from "./domain/iampolicy/store-contract.js";
|
|
146
|
+
export type { PolicyGrantScope } from "./extensions/policy-grant-scope.js";
|
|
147
|
+
export type { AuthorizationQueryEngine } from "./extensions/authorization-queries.js";
|
|
128
148
|
export type { IdentityFederation } from "./extensions/identity-federation.js";
|
|
129
149
|
export type { AccountsBySubject } from "./domain/identityaccount/resolve.js";
|
|
130
150
|
export { identityIdForSubject } from "./domain/identityaccount/resolve.js";
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pins `grantableRolesFor` (pipeline/apiresource-meta.ts), the one read of
|
|
3
|
+
* a kind's `kind_meta.authorization.grantable_roles` — what the IamPolicy
|
|
4
|
+
* domain's ValidateGrantableRole consults FIRST (20260913.01, Q-OR-3 as
|
|
5
|
+
* refined: the proto says what can be granted at all; the composed
|
|
6
|
+
* PolicyGrantScope only narrows by edition) and what open source's
|
|
7
|
+
* organization-only scope reads for the organization.
|
|
8
|
+
*
|
|
9
|
+
* Three properties are load-bearing and pinned here rather than assumed:
|
|
10
|
+
*
|
|
11
|
+
* - The ORGANIZATION lists exactly owner, admin, member, viewer, in the
|
|
12
|
+
* proto's order. This is the Q-OR-4 ruling made a test: the four roles
|
|
13
|
+
* every edition grants on an organization are the proto's, not a list
|
|
14
|
+
* in code, so a change to `api_resource_kind.proto` fails here and
|
|
15
|
+
* someone reads the ruling before the console shows a fifth word.
|
|
16
|
+
* - The five system-managed kinds list NO roles. ValidateGrantableRole's
|
|
17
|
+
* first arm refuses a grant on them with the cloud's byte-pinned
|
|
18
|
+
* INVALID_ARGUMENT in every edition; this is the data that arm reads.
|
|
19
|
+
* - TOTAL, never a throw. The reader is asked about the SECOND kind
|
|
20
|
+
* vocabulary — an `ApiResourceRef.kind` resolved through
|
|
21
|
+
* `kindByEnumName`, which yields the unknown kind for anything it does
|
|
22
|
+
* not recognise and never throws (the module header). The unknown kind
|
|
23
|
+
* is the one enum member without `kind_meta`; it answers the empty
|
|
24
|
+
* list, as the cloud's `grantableRolesFor` (iam/policy/roles.ts) did.
|
|
25
|
+
*/
|
|
26
|
+
import { getOption, hasOption } from "@bufbuild/protobuf";
|
|
27
|
+
import { describe, expect, it } from "vitest";
|
|
28
|
+
|
|
29
|
+
import {
|
|
30
|
+
ApiResourceKind,
|
|
31
|
+
ApiResourceKindSchema,
|
|
32
|
+
kind_meta,
|
|
33
|
+
} from "@stigmer/protos/ai/stigmer/commons/apiresource/apiresourcekind/api_resource_kind_pb";
|
|
34
|
+
import { IamRole } from "@stigmer/protos/ai/stigmer/iam/v1/enum_pb";
|
|
35
|
+
|
|
36
|
+
import { grantableRolesFor } from "../apiresource-meta.js";
|
|
37
|
+
|
|
38
|
+
describe("grantableRolesFor — the proto's grantable roles per kind", () => {
|
|
39
|
+
it("the organization grants exactly owner, admin, member, viewer, in proto order (Q-OR-4)", () => {
|
|
40
|
+
expect(grantableRolesFor(ApiResourceKind.organization)).toEqual([
|
|
41
|
+
IamRole.owner,
|
|
42
|
+
IamRole.admin,
|
|
43
|
+
IamRole.member,
|
|
44
|
+
IamRole.viewer,
|
|
45
|
+
]);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("a blueprint kind answers the descriptor's own list, not a copy", () => {
|
|
49
|
+
const agentValue = ApiResourceKindSchema.values.find(
|
|
50
|
+
(value) => value.number === ApiResourceKind.agent,
|
|
51
|
+
);
|
|
52
|
+
expect(agentValue).toBeDefined();
|
|
53
|
+
const fromDescriptor =
|
|
54
|
+
getOption(agentValue!, kind_meta).authorization?.grantableRoles ?? [];
|
|
55
|
+
expect(fromDescriptor.length).toBeGreaterThan(0);
|
|
56
|
+
expect(grantableRolesFor(ApiResourceKind.agent)).toEqual(fromDescriptor);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it.each([
|
|
60
|
+
["identity_account", ApiResourceKind.identity_account],
|
|
61
|
+
["api_key", ApiResourceKind.api_key],
|
|
62
|
+
["iam_policy", ApiResourceKind.iam_policy],
|
|
63
|
+
["invitation", ApiResourceKind.invitation],
|
|
64
|
+
["agent_execution", ApiResourceKind.agent_execution],
|
|
65
|
+
])(
|
|
66
|
+
"%s is system-managed: no grantable roles (Q-OR-3 arm 1)",
|
|
67
|
+
(_name, kind) => {
|
|
68
|
+
expect(grantableRolesFor(kind)).toEqual([]);
|
|
69
|
+
},
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
it("the unknown kind answers the empty list and never throws (the second vocabulary's doctrine)", () => {
|
|
73
|
+
expect(
|
|
74
|
+
grantableRolesFor(ApiResourceKind.api_resource_kind_unknown),
|
|
75
|
+
).toEqual([]);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it("is total over the descriptor: every member answers an array", () => {
|
|
79
|
+
for (const value of ApiResourceKindSchema.values) {
|
|
80
|
+
const roles = grantableRolesFor(value.number as ApiResourceKind);
|
|
81
|
+
expect(Array.isArray(roles), value.name).toBe(true);
|
|
82
|
+
if (!hasOption(value, kind_meta)) {
|
|
83
|
+
expect(roles, `${value.name} carries no kind_meta`).toEqual([]);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
});
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pins `kindByEnumName` (pipeline/apiresource-meta.ts), the lookup for the
|
|
3
|
+
* SECOND kind vocabulary the contract carries. A resource's own `kind`
|
|
4
|
+
* field is the `kind_meta.name` ("McpServer"; `getKindEnum`, the #545
|
|
5
|
+
* lesson). An `ApiResourceRef.kind` — the IamPolicy spec's principal and
|
|
6
|
+
* resource, the FGA object type — is the enum MEMBER name ("mcp_server"),
|
|
7
|
+
* which is what every SDK call site sends and what the cloud's
|
|
8
|
+
* `kindFromSpecString` resolved before this entry (20260913.01, Q-OR-2).
|
|
9
|
+
*
|
|
10
|
+
* Two properties are load-bearing and pinned here rather than assumed:
|
|
11
|
+
*
|
|
12
|
+
* - EXACT match, never canonicalised. The IamPolicy id is derived from
|
|
13
|
+
* the spec's text (Q-OR-9), so a lenient matcher that admitted
|
|
14
|
+
* "Organization" beside "organization" would mint two rows for one
|
|
15
|
+
* grant, or force the domain to rewrite caller input before storing
|
|
16
|
+
* it. The two spellings `getKindEnum` accepts are refused here on
|
|
17
|
+
* purpose; the contrast arm keeps the two lookups from being merged.
|
|
18
|
+
* - NEVER a throw. The Authorize step resolves `resource_kind_path`
|
|
19
|
+
* through this function at position 1 of every chain, and resolution
|
|
20
|
+
* never throws by doctrine (authorize.ts header): an unknown name is
|
|
21
|
+
* the unknown kind, and the Authorizer owns the decision.
|
|
22
|
+
*/
|
|
23
|
+
import { describe, expect, it } from "vitest";
|
|
24
|
+
|
|
25
|
+
import { ApiResourceKind } from "@stigmer/protos/ai/stigmer/commons/apiresource/apiresourcekind/api_resource_kind_pb";
|
|
26
|
+
|
|
27
|
+
import {
|
|
28
|
+
getKindEnum,
|
|
29
|
+
kindByEnumName,
|
|
30
|
+
kindEnumName,
|
|
31
|
+
} from "../apiresource-meta.js";
|
|
32
|
+
|
|
33
|
+
describe("kindByEnumName — an ApiResourceRef.kind is the enum member name", () => {
|
|
34
|
+
it.each([
|
|
35
|
+
["organization", ApiResourceKind.organization],
|
|
36
|
+
["identity_account", ApiResourceKind.identity_account],
|
|
37
|
+
["mcp_server", ApiResourceKind.mcp_server],
|
|
38
|
+
["agent", ApiResourceKind.agent],
|
|
39
|
+
["iam_policy", ApiResourceKind.iam_policy],
|
|
40
|
+
])("%s resolves to its kind", (name, kind) => {
|
|
41
|
+
expect(kindByEnumName(name)).toBe(kind);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it.each([
|
|
45
|
+
["Organization", "the kind_meta spelling, not a member name"],
|
|
46
|
+
["McpServer", "the kind_meta spelling, not a member name"],
|
|
47
|
+
["ORGANIZATION", "case differs"],
|
|
48
|
+
["organization ", "trailing whitespace"],
|
|
49
|
+
["", "empty"],
|
|
50
|
+
["spaceship", "not a kind"],
|
|
51
|
+
["5", "a number spelled as text is not a name"],
|
|
52
|
+
["constructor", "a prototype key is not a name"],
|
|
53
|
+
])("%s is the unknown kind (%s) — never a throw", (value) => {
|
|
54
|
+
expect(kindByEnumName(value)).toBe(
|
|
55
|
+
ApiResourceKind.api_resource_kind_unknown,
|
|
56
|
+
);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("the zero value's own name is the unknown kind too", () => {
|
|
60
|
+
expect(kindByEnumName("api_resource_kind_unknown")).toBe(
|
|
61
|
+
ApiResourceKind.api_resource_kind_unknown,
|
|
62
|
+
);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("is exact where getKindEnum is lenient — the two vocabularies stay two lookups", () => {
|
|
66
|
+
// A resource's `kind` field: canonical kind_meta.name matching,
|
|
67
|
+
// case-insensitive, underscores ignored, and a throw on unknown.
|
|
68
|
+
expect(getKindEnum("McpServer")).toBe(ApiResourceKind.mcp_server);
|
|
69
|
+
expect(getKindEnum("mcp_server")).toBe(ApiResourceKind.mcp_server);
|
|
70
|
+
expect(() => getKindEnum("spaceship")).toThrow();
|
|
71
|
+
// An ApiResourceRef.kind: the member name and nothing else.
|
|
72
|
+
expect(kindByEnumName("McpServer")).toBe(
|
|
73
|
+
ApiResourceKind.api_resource_kind_unknown,
|
|
74
|
+
);
|
|
75
|
+
expect(kindByEnumName("mcp_server")).toBe(ApiResourceKind.mcp_server);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it("every member of the enum round-trips through its own name", () => {
|
|
79
|
+
for (const [name, value] of Object.entries(ApiResourceKind)) {
|
|
80
|
+
if (typeof value !== "number") continue;
|
|
81
|
+
expect(kindByEnumName(name), name).toBe(value);
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
describe("kindEnumName — the inverse: a kind spelled as an ApiResourceRef.kind", () => {
|
|
87
|
+
it.each([
|
|
88
|
+
[ApiResourceKind.organization, "organization"],
|
|
89
|
+
[ApiResourceKind.identity_account, "identity_account"],
|
|
90
|
+
[ApiResourceKind.mcp_server, "mcp_server"],
|
|
91
|
+
])("%s spells as %s", (kind, name) => {
|
|
92
|
+
expect(kindEnumName(kind)).toBe(name);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it("the unknown kind spells itself — never a throw", () => {
|
|
96
|
+
expect(kindEnumName(ApiResourceKind.api_resource_kind_unknown)).toBe(
|
|
97
|
+
"api_resource_kind_unknown",
|
|
98
|
+
);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it("round-trips with kindByEnumName over every member — the two are inverses", () => {
|
|
102
|
+
for (const value of Object.values(ApiResourceKind)) {
|
|
103
|
+
if (typeof value !== "number") continue;
|
|
104
|
+
expect(kindByEnumName(kindEnumName(value)), String(value)).toBe(value);
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
});
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pins `tierServedByEdition` and `kindServedByEdition`
|
|
3
|
+
* (pipeline/apiresource-meta.ts), the server's twin of the SDK's tier-rank
|
|
4
|
+
* functions (sdk/typescript/src/resource-availability.ts: editions
|
|
5
|
+
* oss < enterprise < cloud; a tier admits its own edition and every edition
|
|
6
|
+
* above it; the enum NUMBERS are wire identifiers, never ranks —
|
|
7
|
+
* `enterprise` sits at 3 in both enums and ranks second). T01_1_review.md
|
|
8
|
+
* Q-OR-8 and claim check C5: the two implementations must agree on the
|
|
9
|
+
* whole matrix, because the console reads the SDK's answer to decide what
|
|
10
|
+
* to SHOW and `checkMyPermission` reads the server's to decide what is
|
|
11
|
+
* HELD; a disagreement is a surface that appears and then fails, the
|
|
12
|
+
* Q-EC-2b class.
|
|
13
|
+
*
|
|
14
|
+
* The matrix is spelled out rather than computed, so a change on either
|
|
15
|
+
* side is a visible diff here and the SDK's own test remains the SDK's.
|
|
16
|
+
*/
|
|
17
|
+
import { describe, expect, it } from "vitest";
|
|
18
|
+
|
|
19
|
+
import {
|
|
20
|
+
ApiResourceKind,
|
|
21
|
+
ResourceTier,
|
|
22
|
+
} from "@stigmer/protos/ai/stigmer/commons/apiresource/apiresourcekind/api_resource_kind_pb";
|
|
23
|
+
import { ServerEdition } from "@stigmer/protos/ai/stigmer/platform/v1/server_info_pb";
|
|
24
|
+
|
|
25
|
+
import {
|
|
26
|
+
kindServedByEdition,
|
|
27
|
+
tierServedByEdition,
|
|
28
|
+
} from "../apiresource-meta.js";
|
|
29
|
+
|
|
30
|
+
const EDITIONS = [
|
|
31
|
+
ServerEdition.oss,
|
|
32
|
+
ServerEdition.enterprise,
|
|
33
|
+
ServerEdition.cloud,
|
|
34
|
+
] as const;
|
|
35
|
+
|
|
36
|
+
/** tier → the editions that serve it, in the SDK's order. */
|
|
37
|
+
const MATRIX: ReadonlyArray<readonly [ResourceTier, readonly boolean[]]> = [
|
|
38
|
+
[ResourceTier.open_source, [true, true, true]],
|
|
39
|
+
[ResourceTier.enterprise, [false, true, true]],
|
|
40
|
+
[ResourceTier.cloud_only, [false, false, true]],
|
|
41
|
+
// A kind whose proto forgot the field is treated as core, as the SDK
|
|
42
|
+
// treats it: hiding a kind is the worse failure for a client, and holding
|
|
43
|
+
// a permission on a kind every edition serves is the truthful answer.
|
|
44
|
+
[ResourceTier.resource_tier_unspecified, [true, true, true]],
|
|
45
|
+
];
|
|
46
|
+
|
|
47
|
+
describe("tierServedByEdition — the SDK's rank table, spelled out", () => {
|
|
48
|
+
for (const [tier, served] of MATRIX) {
|
|
49
|
+
it(`${ResourceTier[tier]} is served by [oss, enterprise, cloud] = [${served.join(", ")}]`, () => {
|
|
50
|
+
expect(
|
|
51
|
+
EDITIONS.map((edition) => tierServedByEdition(tier, edition)),
|
|
52
|
+
).toEqual(served);
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
it("an edition the server does not know serves everything — an older server hides nothing, the SDK's own fallback", () => {
|
|
57
|
+
for (const [tier] of MATRIX) {
|
|
58
|
+
expect(
|
|
59
|
+
tierServedByEdition(tier, ServerEdition.server_edition_unspecified),
|
|
60
|
+
).toBe(true);
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
describe("kindServedByEdition — the kind's tier through the same table", () => {
|
|
66
|
+
it("reads the tier from kind_meta: agent everywhere, invitation from enterprise up, api_resource_version on cloud only", () => {
|
|
67
|
+
expect(
|
|
68
|
+
EDITIONS.map((edition) =>
|
|
69
|
+
kindServedByEdition(ApiResourceKind.agent, edition),
|
|
70
|
+
),
|
|
71
|
+
).toEqual([true, true, true]);
|
|
72
|
+
expect(
|
|
73
|
+
EDITIONS.map((edition) =>
|
|
74
|
+
kindServedByEdition(ApiResourceKind.invitation, edition),
|
|
75
|
+
),
|
|
76
|
+
).toEqual([false, true, true]);
|
|
77
|
+
expect(
|
|
78
|
+
EDITIONS.map((edition) =>
|
|
79
|
+
kindServedByEdition(ApiResourceKind.api_resource_version, edition),
|
|
80
|
+
),
|
|
81
|
+
).toEqual([false, false, true]);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it("platform is enterprise-tiered, so the operator seat's permissions are never held on open source (the settings-navigation arm)", () => {
|
|
85
|
+
expect(
|
|
86
|
+
kindServedByEdition(ApiResourceKind.platform, ServerEdition.oss),
|
|
87
|
+
).toBe(false);
|
|
88
|
+
expect(
|
|
89
|
+
kindServedByEdition(ApiResourceKind.platform, ServerEdition.enterprise),
|
|
90
|
+
).toBe(true);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it("iam_policy is served by every edition once this entry flips it", () => {
|
|
94
|
+
expect(
|
|
95
|
+
kindServedByEdition(ApiResourceKind.iam_policy, ServerEdition.oss),
|
|
96
|
+
).toBe(true);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it("the unknown kind is a programming error, not an answer", () => {
|
|
100
|
+
expect(() =>
|
|
101
|
+
kindServedByEdition(
|
|
102
|
+
ApiResourceKind.api_resource_kind_unknown,
|
|
103
|
+
ServerEdition.cloud,
|
|
104
|
+
),
|
|
105
|
+
).toThrow();
|
|
106
|
+
});
|
|
107
|
+
});
|