@stigmer/server 3.15.2-dev.20260913223433 → 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
|
@@ -8,6 +8,39 @@
|
|
|
8
8
|
* Both editions resolve the same strings from the same proto metadata; the
|
|
9
9
|
* visibility helpers mirror Cloud's VisibilityConfigResolver so the
|
|
10
10
|
* cross-edition error contract is identical by construction.
|
|
11
|
+
*
|
|
12
|
+
* The contract carries TWO kind vocabularies, and this module holds one
|
|
13
|
+
* lookup for each so they are never merged:
|
|
14
|
+
*
|
|
15
|
+
* - A resource's own `kind` field is the `kind_meta.name` ("McpServer").
|
|
16
|
+
* `getKindEnum` reads it by canonical name (case-insensitive,
|
|
17
|
+
* underscores ignored) and throws on an unknown value.
|
|
18
|
+
* - An `ApiResourceRef.kind` — the IamPolicy spec's principal and
|
|
19
|
+
* resource, the FGA object type — is the enum MEMBER name
|
|
20
|
+
* ("mcp_server"). `kindByEnumName` reads it by the descriptor's exact
|
|
21
|
+
* proto name and never throws (20260913.01, Q-OR-2). Exact, because the
|
|
22
|
+
* IamPolicy id is derived from the spec's text (Q-OR-9): a lenient
|
|
23
|
+
* match would let "Organization" and "organization" mint two rows for
|
|
24
|
+
* one grant. Reading the descriptor's name is not deriving a spelling;
|
|
25
|
+
* the #545 rule is about recovering kind_meta.name from an enum, which
|
|
26
|
+
* neither lookup does. A reader asked about a kind that arrived
|
|
27
|
+
* through this vocabulary follows its doctrine: `grantableRolesFor`
|
|
28
|
+
* answers the empty list for the unknown kind instead of throwing,
|
|
29
|
+
* because the kind it is asked about may have come off the wire.
|
|
30
|
+
* `kindEnumName` is the inverse — how server code SPELLS a kind when
|
|
31
|
+
* it builds an `ApiResourceRef` itself (the built-in role lifecycle's
|
|
32
|
+
* cleanup refs; the organization-role specs) — so no caller reaches
|
|
33
|
+
* for the enum's reverse index by hand.
|
|
34
|
+
*
|
|
35
|
+
* A kind's TIER meets the served EDITION in exactly one place on the
|
|
36
|
+
* server, `tierServedByEdition` / `kindServedByEdition` (20260913.01
|
|
37
|
+
* Q-OR-8; claim check C5) — the twin of the SDK's rank functions
|
|
38
|
+
* (sdk/typescript/src/resource-availability.ts), pinned equal on the whole
|
|
39
|
+
* matrix by __tests__/kind-served-by-edition.test.ts. The console reads
|
|
40
|
+
* the SDK's answer to decide what to SHOW; `checkMyPermission` reads the
|
|
41
|
+
* server's to decide what is HELD; a disagreement is a surface that
|
|
42
|
+
* appears and then fails. The enum NUMBERS are wire identifiers, never
|
|
43
|
+
* ranks (`enterprise` sits at 3 in both enums and ranks second).
|
|
11
44
|
*/
|
|
12
45
|
import { getOption, hasOption } from "@bufbuild/protobuf";
|
|
13
46
|
import type { DescEnumValue } from "@bufbuild/protobuf";
|
|
@@ -16,9 +49,12 @@ import { ApiResourceVisibility } from "@stigmer/protos/ai/stigmer/commons/apires
|
|
|
16
49
|
import {
|
|
17
50
|
ApiResourceKind,
|
|
18
51
|
ApiResourceKindSchema,
|
|
52
|
+
ResourceTier,
|
|
19
53
|
kind_meta,
|
|
20
54
|
} from "@stigmer/protos/ai/stigmer/commons/apiresource/apiresourcekind/api_resource_kind_pb";
|
|
21
55
|
import type { ApiResourceKindMeta } from "@stigmer/protos/ai/stigmer/commons/apiresource/apiresourcekind/api_resource_kind_pb";
|
|
56
|
+
import type { IamRole } from "@stigmer/protos/ai/stigmer/iam/v1/enum_pb";
|
|
57
|
+
import { ServerEdition } from "@stigmer/protos/ai/stigmer/platform/v1/server_info_pb";
|
|
22
58
|
|
|
23
59
|
/** Go GetKindMeta: the kind_meta extension of the enum value. */
|
|
24
60
|
export function getKindMeta(kind: ApiResourceKind): ApiResourceKindMeta {
|
|
@@ -55,19 +91,143 @@ export function getKindEnum(kindValue: string): ApiResourceKind {
|
|
|
55
91
|
return kind;
|
|
56
92
|
}
|
|
57
93
|
|
|
94
|
+
/**
|
|
95
|
+
* Resolves an `ApiResourceRef.kind` — a kind's enum MEMBER name, exactly
|
|
96
|
+
* as the proto spells it ("organization", "identity_account") — to the
|
|
97
|
+
* enum. Anything else, including the kind_meta spelling, a different case,
|
|
98
|
+
* the empty string and the zero value's own name, is
|
|
99
|
+
* `api_resource_kind_unknown`; never a throw, because the Authorize step
|
|
100
|
+
* resolves `resource_kind_path` through this at position 1 of every chain
|
|
101
|
+
* and resolution never fails a request (authorize.ts header). See the
|
|
102
|
+
* module header for why the match is exact.
|
|
103
|
+
*/
|
|
104
|
+
export function kindByEnumName(value: string): ApiResourceKind {
|
|
105
|
+
return (
|
|
106
|
+
kindsByEnumName().get(value) ?? ApiResourceKind.api_resource_kind_unknown
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* The inverse of `kindByEnumName`: a kind spelled as an `ApiResourceRef.kind`
|
|
112
|
+
* — the descriptor's exact proto name ("organization", "identity_account").
|
|
113
|
+
* Total over the enum: the unknown kind spells itself, so a ref built from
|
|
114
|
+
* it round-trips to the unknown kind and the gate that reads it refuses
|
|
115
|
+
* the way it refuses a wire caller's garbage; never a throw.
|
|
116
|
+
*/
|
|
117
|
+
export function kindEnumName(kind: ApiResourceKind): string {
|
|
118
|
+
// A number that is no member (a cast from outside the enum) spells as
|
|
119
|
+
// the unknown kind, which every descriptor carries as its zero value.
|
|
120
|
+
const value =
|
|
121
|
+
kindValueDescriptor(kind) ??
|
|
122
|
+
kindValueDescriptor(ApiResourceKind.api_resource_kind_unknown);
|
|
123
|
+
return value?.name ?? "";
|
|
124
|
+
}
|
|
125
|
+
|
|
58
126
|
/**
|
|
59
127
|
* Go DefaultVisibilityFor: blueprint kinds flagged defaults_to_org_visibility
|
|
60
128
|
* get visibility_org (blueprints are shared org assets — a private default
|
|
61
129
|
* would silently hide new blueprints from teammates); all others get
|
|
62
130
|
* visibility_private.
|
|
63
131
|
*/
|
|
64
|
-
export function defaultVisibilityFor(
|
|
132
|
+
export function defaultVisibilityFor(
|
|
133
|
+
kind: ApiResourceKind,
|
|
134
|
+
): ApiResourceVisibility {
|
|
65
135
|
const config = getKindMeta(kind).authorization?.visibility;
|
|
66
136
|
return config?.defaultsToOrgVisibility === true
|
|
67
137
|
? ApiResourceVisibility.visibility_org
|
|
68
138
|
: ApiResourceVisibility.visibility_private;
|
|
69
139
|
}
|
|
70
140
|
|
|
141
|
+
/**
|
|
142
|
+
* The roles a user may be granted on a resource of `kind` according to the
|
|
143
|
+
* CONTRACT — `kind_meta.authorization.grantable_roles`, the one source of
|
|
144
|
+
* "what can be granted at all" (Java AuthorizationConfigResolver
|
|
145
|
+
* .getGrantableRoles; the cloud's iam/policy/roles.ts grantableRolesFor,
|
|
146
|
+
* which this replaces at the 20260913.01 re-point). A kind that lists none
|
|
147
|
+
* is system-managed in every edition (identity_account, api_key,
|
|
148
|
+
* iam_policy, invitation, agent_execution); an edition's PolicyGrantScope
|
|
149
|
+
* (extensions/policy-grant-scope.ts) only narrows this list.
|
|
150
|
+
*
|
|
151
|
+
* Total, never a throw: the kind is an `ApiResourceRef.kind` resolved by
|
|
152
|
+
* `kindByEnumName`, so the unknown kind — the one member without
|
|
153
|
+
* `kind_meta` — is a legitimate argument and answers the empty list
|
|
154
|
+
* (module header, the second vocabulary).
|
|
155
|
+
*/
|
|
156
|
+
export function grantableRolesFor(
|
|
157
|
+
kind: ApiResourceKind,
|
|
158
|
+
): ReadonlyArray<IamRole> {
|
|
159
|
+
const valueDesc = kindValueDescriptor(kind);
|
|
160
|
+
if (valueDesc === undefined || !hasOption(valueDesc, kind_meta)) {
|
|
161
|
+
return [];
|
|
162
|
+
}
|
|
163
|
+
return getOption(valueDesc, kind_meta).authorization?.grantableRoles ?? [];
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Whether an edition serves a tier: the tier names the MINIMUM edition,
|
|
168
|
+
* the editions are ordered oss < enterprise < cloud (each composes the
|
|
169
|
+
* previous one's units), so a tier admits its own edition and every one
|
|
170
|
+
* above. An edition the server does not know (`server_edition_unspecified`)
|
|
171
|
+
* serves everything — the SDK's own fallback: an older client against a
|
|
172
|
+
* newer server hides nothing. A tier the proto forgot is treated as core
|
|
173
|
+
* for the same reason the SDK does: hiding a served kind is the worse
|
|
174
|
+
* failure.
|
|
175
|
+
*/
|
|
176
|
+
export function tierServedByEdition(
|
|
177
|
+
tier: ResourceTier,
|
|
178
|
+
edition: ServerEdition,
|
|
179
|
+
): boolean {
|
|
180
|
+
if (edition === ServerEdition.server_edition_unspecified) {
|
|
181
|
+
return true;
|
|
182
|
+
}
|
|
183
|
+
return editionRank(edition) >= tierRank(tier);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Whether this edition serves a kind: its `kind_meta.tier` through the
|
|
188
|
+
* table above. Throws for a kind without `kind_meta` (the unknown kind):
|
|
189
|
+
* a caller that reaches this with wire input has skipped the kind
|
|
190
|
+
* refusals, and "served" for it would hide that bug.
|
|
191
|
+
*/
|
|
192
|
+
export function kindServedByEdition(
|
|
193
|
+
kind: ApiResourceKind,
|
|
194
|
+
edition: ServerEdition,
|
|
195
|
+
): boolean {
|
|
196
|
+
return tierServedByEdition(getKindMeta(kind).tier, edition);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function editionRank(edition: ServerEdition): number {
|
|
200
|
+
switch (edition) {
|
|
201
|
+
case ServerEdition.oss:
|
|
202
|
+
return 1;
|
|
203
|
+
case ServerEdition.enterprise:
|
|
204
|
+
return 2;
|
|
205
|
+
case ServerEdition.cloud:
|
|
206
|
+
case ServerEdition.server_edition_unspecified:
|
|
207
|
+
return 3;
|
|
208
|
+
default: {
|
|
209
|
+
const exhaustive: never = edition;
|
|
210
|
+
throw new Error(`unknown server edition: ${String(exhaustive)}`);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function tierRank(tier: ResourceTier): number {
|
|
216
|
+
switch (tier) {
|
|
217
|
+
case ResourceTier.open_source:
|
|
218
|
+
case ResourceTier.resource_tier_unspecified:
|
|
219
|
+
return 1;
|
|
220
|
+
case ResourceTier.enterprise:
|
|
221
|
+
return 2;
|
|
222
|
+
case ResourceTier.cloud_only:
|
|
223
|
+
return 3;
|
|
224
|
+
default: {
|
|
225
|
+
const exhaustive: never = tier;
|
|
226
|
+
throw new Error(`unknown resource tier: ${String(exhaustive)}`);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
71
231
|
/**
|
|
72
232
|
* Go SupportsVisibility: PRIVATE and UNSPECIFIED are always supported (no
|
|
73
233
|
* visibility grant); every other level requires the matching supports_*
|
|
@@ -134,3 +294,23 @@ function kindsByCanonicalName(): Map<string, ApiResourceKind> {
|
|
|
134
294
|
}
|
|
135
295
|
return canonicalNameCache;
|
|
136
296
|
}
|
|
297
|
+
|
|
298
|
+
let enumNameCache: Map<string, ApiResourceKind> | undefined;
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* Every member by its descriptor name, the zero value included (it maps to
|
|
302
|
+
* itself, which is the unknown kind either way). A Map rather than the TS
|
|
303
|
+
* enum's reverse mapping, so a prototype key or a number spelled as text
|
|
304
|
+
* cannot resolve to anything.
|
|
305
|
+
*/
|
|
306
|
+
function kindsByEnumName(): Map<string, ApiResourceKind> {
|
|
307
|
+
if (enumNameCache === undefined) {
|
|
308
|
+
enumNameCache = new Map(
|
|
309
|
+
ApiResourceKindSchema.values.map((value) => [
|
|
310
|
+
value.name,
|
|
311
|
+
value.number as ApiResourceKind,
|
|
312
|
+
]),
|
|
313
|
+
);
|
|
314
|
+
}
|
|
315
|
+
return enumNameCache;
|
|
316
|
+
}
|
|
@@ -175,6 +175,19 @@ export function trustedLocalIdentity(): CallerIdentity {
|
|
|
175
175
|
return trustedLocalIdentityFor(operatorIdentitySnapshot());
|
|
176
176
|
}
|
|
177
177
|
|
|
178
|
+
/**
|
|
179
|
+
* The identityId of the UNCONFIGURED laptop's operator — no
|
|
180
|
+
* STIGMER_OPERATOR_EMAIL, so the trust domain's one principal has no
|
|
181
|
+
* email to be named by. It is what the interceptor stamps and therefore
|
|
182
|
+
* what every audit actor on such a laptop carries as `created_by.id`; the
|
|
183
|
+
* operator account derives its subject from it (`local|system`) and the
|
|
184
|
+
* membership rules read it as "not a person" when they classify creator
|
|
185
|
+
* stamps (domain/iampolicy/membership.ts). One home (20260913.01 slice 4,
|
|
186
|
+
* Q-S4-6): a second spelling anywhere would make a stamp and its reader
|
|
187
|
+
* disagree silently.
|
|
188
|
+
*/
|
|
189
|
+
export const SYSTEM_OPERATOR_IDENTITY_ID = "system";
|
|
190
|
+
|
|
178
191
|
/**
|
|
179
192
|
* The trusted-local identity for a given operator — the one construction
|
|
180
193
|
* of that principal, shared by the per-request stamp above and by the
|
|
@@ -187,7 +200,7 @@ export function trustedLocalIdentityFor(operator: {
|
|
|
187
200
|
}): CallerIdentity {
|
|
188
201
|
if (operator.email === "") {
|
|
189
202
|
return {
|
|
190
|
-
identityId:
|
|
203
|
+
identityId: SYSTEM_OPERATOR_IDENTITY_ID,
|
|
191
204
|
callerClass: "user",
|
|
192
205
|
issuer: "",
|
|
193
206
|
rawToken: "",
|
|
@@ -20,9 +20,32 @@
|
|
|
20
20
|
* never reach the annotation's resolution (the skip lanes resolve their
|
|
21
21
|
* target in the handler through `authorizeDirect`'s override). A config
|
|
22
22
|
* that names NO kind (static or by path) is outside this invariant: the
|
|
23
|
-
* check is then about the kind, not the id
|
|
24
|
-
*
|
|
25
|
-
*
|
|
23
|
+
* check is then about the kind, not the id.
|
|
24
|
+
*
|
|
25
|
+
* The IamPolicy contract is the one place that shape exists, and it is
|
|
26
|
+
* pinned here by descriptor, not by served route, so the arm turns green
|
|
27
|
+
* when the ANNOTATIONS land (sp.oss-organization-roles slice 1) and not
|
|
28
|
+
* when the service is first served (slice 5). Of its nine `rpc.config`
|
|
29
|
+
* methods: six name their target — `create`, `delete`,
|
|
30
|
+
* `listResourceAccessByPrincipal`, `getPrincipalResourceRoles` by
|
|
31
|
+
* `resource_kind_path` (the spec's `resource.kind`, a STRING the resolver
|
|
32
|
+
* reads by enum name) + `field_path`; `revokeOrgAccess` and
|
|
33
|
+
* `getPrincipalsCount` statically. `get(IamPolicyId)` names no kind BY
|
|
34
|
+
* DESIGN: its target is inside the row it loads, so it is the one
|
|
35
|
+
* load-then-authorize lane (`authorizeDirect` with the override's
|
|
36
|
+
* resourceKind + resourceId, the getByEmail precedent). The two
|
|
37
|
+
* `listAuthorized*Ids` were the open question of S1 (finding 7): an
|
|
38
|
+
* annotation with a permission and no kind resolves to an EMPTY target,
|
|
39
|
+
* which the open-source permissive Authorizer allows and the cloud's
|
|
40
|
+
* OpenFGA Authorizer denies for every caller — the #1073 class this
|
|
41
|
+
* invariant cannot see. Ruled at slice 1 (2026-09-13, T01_1_review.md
|
|
42
|
+
* Q-OR-2): `listAuthorizedPrincipalIds` HAS a resource and names it (the
|
|
43
|
+
* same `can_view_access` target `listResourceAccessByPrincipal` enforces
|
|
44
|
+
* for the same question); `listAuthorizedResourceIds` has a principal and
|
|
45
|
+
* no resource, so it is an `is_skip_authorization` lane whose handler
|
|
46
|
+
* enforces `checkAuthorization`'s principal-trust rule (a user only for
|
|
47
|
+
* themselves; machine and internal for anyone) — a dead `can_view_access`
|
|
48
|
+
* config would have been exactly the kind-less shape this file guards.
|
|
26
49
|
*
|
|
27
50
|
* The mutation arms prove the check bites over the same function.
|
|
28
51
|
*/
|
|
@@ -41,6 +64,8 @@ import {
|
|
|
41
64
|
is_public,
|
|
42
65
|
is_skip_authorization,
|
|
43
66
|
} from "@stigmer/protos/ai/stigmer/commons/rpc/method_options_pb";
|
|
67
|
+
import { IamPolicyCommandController } from "@stigmer/protos/ai/stigmer/iam/iampolicy/v1/command_pb";
|
|
68
|
+
import { IamPolicyQueryController } from "@stigmer/protos/ai/stigmer/iam/iampolicy/v1/query_pb";
|
|
44
69
|
|
|
45
70
|
import { loadConfig } from "../../../boot/config.js";
|
|
46
71
|
import { composeServer } from "../../../boot/compose.js";
|
|
@@ -151,6 +176,51 @@ describe("authorization annotations name where the resource id comes from", () =
|
|
|
151
176
|
});
|
|
152
177
|
});
|
|
153
178
|
|
|
179
|
+
describe("the IamPolicy contract names its targets (sp.oss-organization-roles slice 1, read by descriptor)", () => {
|
|
180
|
+
const targets = annotationTargets([
|
|
181
|
+
IamPolicyCommandController,
|
|
182
|
+
IamPolicyQueryController,
|
|
183
|
+
]);
|
|
184
|
+
const byName = new Map(
|
|
185
|
+
targets.map((t) => [t.method.split("/")[1] ?? t.method, t]),
|
|
186
|
+
);
|
|
187
|
+
|
|
188
|
+
it.each([
|
|
189
|
+
"create",
|
|
190
|
+
"delete",
|
|
191
|
+
"revokeOrgAccess",
|
|
192
|
+
"listResourceAccessByPrincipal",
|
|
193
|
+
"getPrincipalResourceRoles",
|
|
194
|
+
"getPrincipalsCount",
|
|
195
|
+
"listAuthorizedPrincipalIds",
|
|
196
|
+
])("%s names a resource kind AND where its id comes from", (method) => {
|
|
197
|
+
const target = byName.get(method);
|
|
198
|
+
expect(target, `${method} carries rpc.config`).toBeDefined();
|
|
199
|
+
expect(target?.namesKind, `${method} names a kind`).toBe(true);
|
|
200
|
+
expect(target?.namesId, `${method} names its id`).toBe(true);
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
it("get names no kind — the one load-then-authorize lane, its target inside the row", () => {
|
|
204
|
+
const get = byName.get("get");
|
|
205
|
+
expect(get, "get carries rpc.config").toBeDefined();
|
|
206
|
+
expect(get?.namesKind).toBe(false);
|
|
207
|
+
expect(get?.skipsResolution).toBe(false);
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
it("listAuthorizedResourceIds carries NO rpc.config and skips — its trust rule is the handler's (finding 7)", () => {
|
|
211
|
+
// Read the descriptor directly: `annotationTargets` lists only
|
|
212
|
+
// config-carrying methods, and this one must have left that set.
|
|
213
|
+
const method = IamPolicyQueryController.method.listAuthorizedResourceIds;
|
|
214
|
+
expect(byName.has("listAuthorizedResourceIds")).toBe(false);
|
|
215
|
+
expect(hasOption(method, rpcAuthorizationConfig)).toBe(false);
|
|
216
|
+
expect(getOption(method, is_skip_authorization)).toBe(true);
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
it("the invariant reports none of them once slice 1 lands", () => {
|
|
220
|
+
expect(unresolvableTargets(targets)).toEqual([]);
|
|
221
|
+
});
|
|
222
|
+
});
|
|
223
|
+
|
|
154
224
|
describe("the check bites (mutation proofs over the same function)", () => {
|
|
155
225
|
const shaped = (overrides: Partial<AnnotationTarget>): AnnotationTarget => ({
|
|
156
226
|
method: "x.v1.Query/probe",
|
|
@@ -4,9 +4,11 @@
|
|
|
4
4
|
* annotation's byte-pinned error_msg; unavailable → INTERNAL, never a
|
|
5
5
|
* softened denial), the skip arms (internal caller class, is_public,
|
|
6
6
|
* is_skip_authorization, no-config methods), check-target resolution
|
|
7
|
-
* (field_path, static resource_id, absent field = empty id
|
|
8
|
-
*
|
|
9
|
-
*
|
|
7
|
+
* (field_path, static resource_id, absent field = empty id, a string
|
|
8
|
+
* resource_kind_path resolved by enum name with an unknown name = the
|
|
9
|
+
* unknown kind — never a throw), the direct-handler override in both its
|
|
10
|
+
* shapes (a server-side id; a server-side kind AND id), and the mid-chain
|
|
11
|
+
* resolved-id pattern the traced ListVersions handlers port onto.
|
|
10
12
|
*/
|
|
11
13
|
import { describe, expect, it } from "vitest";
|
|
12
14
|
import { create } from "@bufbuild/protobuf";
|
|
@@ -17,6 +19,7 @@ import { AgentSchema } from "@stigmer/protos/ai/stigmer/agentic/agent/v1/api_pb"
|
|
|
17
19
|
import { AgentExecutionCommandController } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/command_pb";
|
|
18
20
|
import { AgentExecutionSchema } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/api_pb";
|
|
19
21
|
import { IamPolicyCommandController } from "@stigmer/protos/ai/stigmer/iam/iampolicy/v1/command_pb";
|
|
22
|
+
import { IamPolicyQueryController } from "@stigmer/protos/ai/stigmer/iam/iampolicy/v1/query_pb";
|
|
20
23
|
import { IamPermission } from "@stigmer/protos/ai/stigmer/iam/v1/enum_pb";
|
|
21
24
|
import { PlatformQueryController } from "@stigmer/protos/ai/stigmer/platform/v1/server_info_pb";
|
|
22
25
|
import { ApiResourceKind } from "@stigmer/protos/ai/stigmer/commons/apiresource/apiresourcekind/api_resource_kind_pb";
|
|
@@ -37,9 +40,7 @@ import {
|
|
|
37
40
|
} from "../authorize.js";
|
|
38
41
|
|
|
39
42
|
/** Awaits the rejection and returns it as a ConnectError. */
|
|
40
|
-
async function captureError(
|
|
41
|
-
run: () => Promise<void>,
|
|
42
|
-
): Promise<ConnectError> {
|
|
43
|
+
async function captureError(run: () => Promise<void>): Promise<ConnectError> {
|
|
43
44
|
try {
|
|
44
45
|
await run();
|
|
45
46
|
} catch (error) {
|
|
@@ -290,6 +291,75 @@ describe("check-target resolution (never a throw — byte-identity)", () => {
|
|
|
290
291
|
},
|
|
291
292
|
]);
|
|
292
293
|
});
|
|
294
|
+
|
|
295
|
+
// 20260913.01 (Q-OR-2): the IamPolicy RPCs name their target inside the
|
|
296
|
+
// request as a STRING kind (`ApiResourceRef.kind`, "organization"), so
|
|
297
|
+
// `resource_kind_path` resolves a string through the kind enum's names.
|
|
298
|
+
// The option had no user before this entry; a numeric field still works.
|
|
299
|
+
it("resource_kind_path over a string field resolves through the kind enum's names (IamPolicy create)", async () => {
|
|
300
|
+
const { authorizer, checks } = fakeAuthorizer({ kind: "allow" });
|
|
301
|
+
const method = IamPolicyCommandController.method.create;
|
|
302
|
+
const step = newAuthorizeStep(method, authorizer);
|
|
303
|
+
const ctx = new RequestContext(
|
|
304
|
+
method.input,
|
|
305
|
+
create(method.input, {
|
|
306
|
+
principal: { kind: "identity_account", id: "ida_alice" },
|
|
307
|
+
relation: "member",
|
|
308
|
+
resource: { kind: "organization", id: "acme" },
|
|
309
|
+
}),
|
|
310
|
+
testCallerIdentity(),
|
|
311
|
+
);
|
|
312
|
+
await step.execute(ctx);
|
|
313
|
+
expect(checks).toEqual([
|
|
314
|
+
{
|
|
315
|
+
permission: IamPermission.can_grant_access,
|
|
316
|
+
resourceKind: ApiResourceKind.organization,
|
|
317
|
+
resourceId: "acme",
|
|
318
|
+
},
|
|
319
|
+
]);
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
it("an unknown kind name resolves to the unknown kind — never a throw; the Authorizer owns the decision", async () => {
|
|
323
|
+
const { authorizer, checks } = fakeAuthorizer({ kind: "allow" });
|
|
324
|
+
const method = IamPolicyCommandController.method.create;
|
|
325
|
+
const step = newAuthorizeStep(method, authorizer);
|
|
326
|
+
const ctx = new RequestContext(
|
|
327
|
+
method.input,
|
|
328
|
+
create(method.input, {
|
|
329
|
+
principal: { kind: "identity_account", id: "ida_alice" },
|
|
330
|
+
relation: "member",
|
|
331
|
+
resource: { kind: "spaceship", id: "acme" },
|
|
332
|
+
}),
|
|
333
|
+
testCallerIdentity(),
|
|
334
|
+
);
|
|
335
|
+
await expect(step.execute(ctx)).resolves.toBeUndefined();
|
|
336
|
+
expect(checks[0]?.resourceKind).toBe(
|
|
337
|
+
ApiResourceKind.api_resource_kind_unknown,
|
|
338
|
+
);
|
|
339
|
+
expect(checks[0]?.resourceId).toBe("acme");
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
it("revokeOrgAccess names the organization statically and its id by field_path (organization_id)", async () => {
|
|
343
|
+
const { authorizer, checks } = fakeAuthorizer({ kind: "allow" });
|
|
344
|
+
const method = IamPolicyCommandController.method.revokeOrgAccess;
|
|
345
|
+
const step = newAuthorizeStep(method, authorizer);
|
|
346
|
+
const ctx = new RequestContext(
|
|
347
|
+
method.input,
|
|
348
|
+
create(method.input, {
|
|
349
|
+
identityAccountId: "ida_alice",
|
|
350
|
+
organizationId: "acme",
|
|
351
|
+
}),
|
|
352
|
+
testCallerIdentity(),
|
|
353
|
+
);
|
|
354
|
+
await step.execute(ctx);
|
|
355
|
+
expect(checks).toEqual([
|
|
356
|
+
{
|
|
357
|
+
permission: IamPermission.can_grant_access,
|
|
358
|
+
resourceKind: ApiResourceKind.organization,
|
|
359
|
+
resourceId: "acme",
|
|
360
|
+
},
|
|
361
|
+
]);
|
|
362
|
+
});
|
|
293
363
|
});
|
|
294
364
|
|
|
295
365
|
describe("authorizeDirect (the direct-handler arm, C2 Stage 4)", () => {
|
|
@@ -348,6 +418,49 @@ describe("authorizeDirect (the direct-handler arm, C2 Stage 4)", () => {
|
|
|
348
418
|
]);
|
|
349
419
|
});
|
|
350
420
|
|
|
421
|
+
// 20260913.01 (Q-OR-2): the IamPolicy `get(IamPolicyId)` lane. Its
|
|
422
|
+
// annotation names a permission and NO kind, because the target is the
|
|
423
|
+
// loaded row's resource — kind AND id are server-side state. The
|
|
424
|
+
// override carries both; the annotation still owns the permission and
|
|
425
|
+
// the copy.
|
|
426
|
+
it("the target override may carry the resource KIND too — the IamPolicy get lane", async () => {
|
|
427
|
+
const { authorizer, checks } = fakeAuthorizer({ kind: "allow" });
|
|
428
|
+
const method = IamPolicyQueryController.method.get;
|
|
429
|
+
await authorizeDirect(
|
|
430
|
+
method,
|
|
431
|
+
authorizer,
|
|
432
|
+
testCallerIdentity(),
|
|
433
|
+
create(method.input, { value: "iamp_01row" }),
|
|
434
|
+
{ resourceKind: ApiResourceKind.organization, resourceId: "acme" },
|
|
435
|
+
);
|
|
436
|
+
expect(checks).toEqual([
|
|
437
|
+
{
|
|
438
|
+
permission: IamPermission.can_view_access,
|
|
439
|
+
resourceKind: ApiResourceKind.organization,
|
|
440
|
+
resourceId: "acme",
|
|
441
|
+
},
|
|
442
|
+
]);
|
|
443
|
+
});
|
|
444
|
+
|
|
445
|
+
it("an override with the id alone leaves the annotation's kind in place — the completeOAuthConnect lane is unchanged", async () => {
|
|
446
|
+
const { authorizer, checks } = fakeAuthorizer({ kind: "allow" });
|
|
447
|
+
const method = IamPolicyQueryController.method.get;
|
|
448
|
+
// A kind-less annotation plus an id-only override: the kind stays
|
|
449
|
+
// unknown, exactly what the annotation says. The lane that needs a
|
|
450
|
+
// kind must say so; nothing is inferred.
|
|
451
|
+
await authorizeDirect(
|
|
452
|
+
method,
|
|
453
|
+
authorizer,
|
|
454
|
+
testCallerIdentity(),
|
|
455
|
+
create(method.input, { value: "iamp_01row" }),
|
|
456
|
+
{ resourceId: "acme" },
|
|
457
|
+
);
|
|
458
|
+
expect(checks[0]?.resourceKind).toBe(
|
|
459
|
+
ApiResourceKind.api_resource_kind_unknown,
|
|
460
|
+
);
|
|
461
|
+
expect(checks[0]?.resourceId).toBe("acme");
|
|
462
|
+
});
|
|
463
|
+
|
|
351
464
|
it("the ruling-Q1 not-found arm maps identically from the direct entry", async () => {
|
|
352
465
|
const { authorizer } = fakeAuthorizer({ kind: "not-found" });
|
|
353
466
|
const error = await authorizeDirect(
|
|
@@ -438,18 +551,30 @@ describe("authorizeResolvedResource (the mid-chain resolved-id pattern, 20260830
|
|
|
438
551
|
),
|
|
439
552
|
);
|
|
440
553
|
expect(err.code).toBe(Code.PermissionDenied);
|
|
441
|
-
expect(err.rawMessage).toBe(
|
|
554
|
+
expect(err.rawMessage).toBe(
|
|
555
|
+
"unauthorized to view workflow version history",
|
|
556
|
+
);
|
|
442
557
|
});
|
|
443
558
|
|
|
444
559
|
it("deny without lane copy falls back to the reason, then the shared fallback", async () => {
|
|
445
560
|
const reasoned = fakeAuthorizer({ kind: "deny", reason: "because" });
|
|
446
561
|
const err1 = await captureError(() =>
|
|
447
|
-
authorizeResolvedResource(
|
|
562
|
+
authorizeResolvedResource(
|
|
563
|
+
reasoned.authorizer,
|
|
564
|
+
testCallerIdentity(),
|
|
565
|
+
check,
|
|
566
|
+
"",
|
|
567
|
+
),
|
|
448
568
|
);
|
|
449
569
|
expect(err1.rawMessage).toBe("because");
|
|
450
570
|
const bare = fakeAuthorizer({ kind: "deny", reason: "" });
|
|
451
571
|
const err2 = await captureError(() =>
|
|
452
|
-
authorizeResolvedResource(
|
|
572
|
+
authorizeResolvedResource(
|
|
573
|
+
bare.authorizer,
|
|
574
|
+
testCallerIdentity(),
|
|
575
|
+
check,
|
|
576
|
+
"",
|
|
577
|
+
),
|
|
453
578
|
);
|
|
454
579
|
expect(err2.rawMessage).toBe("permission denied");
|
|
455
580
|
});
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pins `derivedId`, the one encoder behind every id a kind derives from its
|
|
3
|
+
* natural key instead of minting (metadata.proto's `id` comment: a direct
|
|
4
|
+
* IdentityAccount from its subject, an IamPolicy from its triple; the
|
|
5
|
+
* Organization's slug needs no encoding). Lifted from the identity-account
|
|
6
|
+
* domain in 20260913.01 slice 2 so the second derived id did not carry a
|
|
7
|
+
* second copy of the grammar.
|
|
8
|
+
*
|
|
9
|
+
* The vectors here are the two domains' golden vectors restated through
|
|
10
|
+
* the shared function: `accountIdFor` and `policyIdFor` must equal
|
|
11
|
+
* `derivedId(prefix, text)` over their canonical text, and those domain
|
|
12
|
+
* tests pin the wire-adjacent constants. A change to the encoding
|
|
13
|
+
* re-addresses every derived row open source ever wrote.
|
|
14
|
+
*/
|
|
15
|
+
import { describe, expect, it } from "vitest";
|
|
16
|
+
|
|
17
|
+
import { derivedId } from "../defaults.js";
|
|
18
|
+
|
|
19
|
+
const CROCKFORD_26 = /^[0-9abcdefghjkmnpqrstvwxyz]{26}$/;
|
|
20
|
+
|
|
21
|
+
describe("derivedId — prefix, underscore, the top 130 bits of sha256 as 26 Crockford chars", () => {
|
|
22
|
+
it("matches the identity-account golden vector", () => {
|
|
23
|
+
// accountIdFor("auth0|user123") as pinned in identityaccount/__tests__/constants.test.ts.
|
|
24
|
+
expect(derivedId("ida", "auth0|user123")).toBe(
|
|
25
|
+
"ida_wtr3jcf281yfk9xx61kj59fsme",
|
|
26
|
+
);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("matches the IamPolicy golden vector over the canonical triple text", () => {
|
|
30
|
+
expect(
|
|
31
|
+
derivedId(
|
|
32
|
+
"iamp",
|
|
33
|
+
"identity_account:ida_wtr3jcf281yfk9xx61kj59fsme#@organization:acme#admin",
|
|
34
|
+
),
|
|
35
|
+
).toBe("iamp_vkbjb6nvqm77vtwnr2bj0x95f9");
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it("has the minted id's shape whatever the prefix, so nothing downstream learns a second grammar", () => {
|
|
39
|
+
const id = derivedId("xyz", "anything");
|
|
40
|
+
expect(id.startsWith("xyz_")).toBe(true);
|
|
41
|
+
expect(id.slice("xyz_".length)).toMatch(CROCKFORD_26);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it("is a pure function of prefix and text: equal inputs, equal ids; a changed byte, a changed id", () => {
|
|
45
|
+
expect(derivedId("ida", "same")).toBe(derivedId("ida", "same"));
|
|
46
|
+
expect(derivedId("ida", "same")).not.toBe(derivedId("ida", "same "));
|
|
47
|
+
expect(derivedId("ida", "same")).not.toBe(derivedId("iamp", "same"));
|
|
48
|
+
});
|
|
49
|
+
});
|