@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
|
@@ -18,6 +18,11 @@
|
|
|
18
18
|
* provisioning slot fire for both); a first-login race resolves the
|
|
19
19
|
* winner by subject whatever the failure's shape.
|
|
20
20
|
*
|
|
21
|
+
* The answer says whether THIS call created the row (`created`; 20260913.01
|
|
22
|
+
* slice 4): the early return and the race loser answer false, so a rule
|
|
23
|
+
* keyed on first provisioning (the membership rules) runs exactly once per
|
|
24
|
+
* account and never on the idempotent path.
|
|
25
|
+
*
|
|
21
26
|
* Step 4 of the cloud's flow (the personal organization) is not core: it
|
|
22
27
|
* fires on `identity-account-provision:post-persist` in the composition.
|
|
23
28
|
*/
|
|
@@ -126,7 +131,9 @@ describe("provisionDirectAccount", () => {
|
|
|
126
131
|
oidcCaller("auth0|alice"),
|
|
127
132
|
);
|
|
128
133
|
|
|
129
|
-
expect(second).toEqual(first);
|
|
134
|
+
expect(second.account).toEqual(first.account);
|
|
135
|
+
expect(first.created, "the first call created the row").toBe(true);
|
|
136
|
+
expect(second.created, "the second call found it").toBe(false);
|
|
130
137
|
expect(path.calls).toHaveLength(1);
|
|
131
138
|
expect(userInfo.tokens).toEqual(["token-for-auth0|alice"]);
|
|
132
139
|
});
|
|
@@ -140,7 +147,7 @@ describe("provisionDirectAccount", () => {
|
|
|
140
147
|
createAccount: path.createAccount,
|
|
141
148
|
userInfo,
|
|
142
149
|
});
|
|
143
|
-
const account = await provisioner.provisionDirectAccount(
|
|
150
|
+
const { account } = await provisioner.provisionDirectAccount(
|
|
144
151
|
"auth0|alice",
|
|
145
152
|
oidcCaller("auth0|alice"),
|
|
146
153
|
);
|
|
@@ -186,7 +193,7 @@ describe("provisionDirectAccount", () => {
|
|
|
186
193
|
createAccount: path.createAccount,
|
|
187
194
|
userInfo,
|
|
188
195
|
});
|
|
189
|
-
const account = await provisioner.provisionDirectAccount(
|
|
196
|
+
const { account } = await provisioner.provisionDirectAccount(
|
|
190
197
|
"local|operator@example.com",
|
|
191
198
|
operatorCaller,
|
|
192
199
|
);
|
|
@@ -205,7 +212,7 @@ describe("provisionDirectAccount", () => {
|
|
|
205
212
|
createAccount: path.createAccount,
|
|
206
213
|
userInfo: fakeUserInfo({ ...PROFILE, email: "" }),
|
|
207
214
|
});
|
|
208
|
-
const account = await provisioner.provisionDirectAccount(
|
|
215
|
+
const { account } = await provisioner.provisionDirectAccount(
|
|
209
216
|
"abc123@clients",
|
|
210
217
|
oidcCaller("abc123@clients"),
|
|
211
218
|
);
|
|
@@ -233,7 +240,8 @@ describe("provisionDirectAccount", () => {
|
|
|
233
240
|
"auth0|carol",
|
|
234
241
|
oidcCaller("auth0|carol"),
|
|
235
242
|
);
|
|
236
|
-
expect(resolved).toEqual(winner);
|
|
243
|
+
expect(resolved.account).toEqual(winner);
|
|
244
|
+
expect(resolved.created, "the loser did not create the row").toBe(false);
|
|
237
245
|
expect(accounts.rows.size).toBe(1);
|
|
238
246
|
});
|
|
239
247
|
|
|
@@ -274,8 +282,12 @@ describe("provisionDirectAccount", () => {
|
|
|
274
282
|
),
|
|
275
283
|
]);
|
|
276
284
|
|
|
277
|
-
expect(a.metadata?.id).toBe(accountIdFor("auth0|erin"));
|
|
278
|
-
expect(b.metadata?.id).toBe(a.metadata?.id);
|
|
285
|
+
expect(a.account.metadata?.id).toBe(accountIdFor("auth0|erin"));
|
|
286
|
+
expect(b.account.metadata?.id).toBe(a.account.metadata?.id);
|
|
279
287
|
expect(accounts.rows.size).toBe(1);
|
|
288
|
+
expect(
|
|
289
|
+
[a.created, b.created].filter(Boolean),
|
|
290
|
+
"exactly one of the two created the row",
|
|
291
|
+
).toHaveLength(1);
|
|
280
292
|
});
|
|
281
293
|
});
|
|
@@ -16,6 +16,20 @@
|
|
|
16
16
|
* - a hit whose row carries no id is an infrastructure fault naming the
|
|
17
17
|
* subject, never a `""` principal — the one deliberate divergence
|
|
18
18
|
* from the cloud's inline `?? ""`.
|
|
19
|
+
*
|
|
20
|
+
* And the read in the other direction, `accountForCaller` (20260913.01
|
|
21
|
+
* slice 4, Q-S4-1): the account a CallerIdentity stands for, the two
|
|
22
|
+
* primary-key reads whoAmI has always made, stated once so the built-in
|
|
23
|
+
* role lifecycle and whoAmI cannot disagree:
|
|
24
|
+
*
|
|
25
|
+
* - an identityId that IS an account id resolves by that id (the
|
|
26
|
+
* verifier already re-stamped);
|
|
27
|
+
* - otherwise the caller's subject (idpIdOf) resolves through the direct
|
|
28
|
+
* lookup — the trusted-local operator's `local|<email>` and a
|
|
29
|
+
* composition verifier that stamps the raw subject both land here;
|
|
30
|
+
* - a caller whose credential names no subject (an opaque token) is
|
|
31
|
+
* `undefined` with NO second read — the store is never asked about "";
|
|
32
|
+
* - an idp-shaped caller with no account is `undefined`.
|
|
19
33
|
*/
|
|
20
34
|
import { create } from "@bufbuild/protobuf";
|
|
21
35
|
import { ConnectError } from "@connectrpc/connect";
|
|
@@ -24,8 +38,9 @@ import { describe, expect, it, vi } from "vitest";
|
|
|
24
38
|
import { IdentityAccountSchema } from "@stigmer/protos/ai/stigmer/iam/identityaccount/v1/api_pb";
|
|
25
39
|
import { IdentityAccountProvisioningMode } from "@stigmer/protos/ai/stigmer/iam/identityaccount/v1/enum_pb";
|
|
26
40
|
|
|
27
|
-
import {
|
|
28
|
-
import {
|
|
41
|
+
import type { CallerIdentity } from "../../../extensions/identity.js";
|
|
42
|
+
import { accountIdFor, localIdpIdFor } from "../constants.js";
|
|
43
|
+
import { accountForCaller, identityIdForSubject } from "../resolve.js";
|
|
29
44
|
import type { AccountsBySubject } from "../resolve.js";
|
|
30
45
|
import { fakeIdentityAccountStore } from "./support.js";
|
|
31
46
|
|
|
@@ -112,3 +127,99 @@ describe("identityIdForSubject", () => {
|
|
|
112
127
|
expect(String(error)).toContain("auth0|corrupt");
|
|
113
128
|
});
|
|
114
129
|
});
|
|
130
|
+
|
|
131
|
+
/** An unsigned JWT-shaped token whose payload carries `sub` — what idpIdOf reads. */
|
|
132
|
+
function tokenFor(sub: string): string {
|
|
133
|
+
const segment = (value: unknown): string =>
|
|
134
|
+
Buffer.from(JSON.stringify(value)).toString("base64url");
|
|
135
|
+
return `${segment({ alg: "none" })}.${segment({ sub })}.unsigned`;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function caller(overrides: Partial<CallerIdentity>): CallerIdentity {
|
|
139
|
+
return {
|
|
140
|
+
identityId: "",
|
|
141
|
+
callerClass: "user",
|
|
142
|
+
issuer: "",
|
|
143
|
+
rawToken: "",
|
|
144
|
+
...overrides,
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
describe("accountForCaller — the account a caller stands for", () => {
|
|
149
|
+
it("an identityId that is an account id resolves by that id, with no subject read", async () => {
|
|
150
|
+
const accounts = seeded("auth0|known");
|
|
151
|
+
const bySubject = vi.spyOn(accounts, "findDirectByIdpId");
|
|
152
|
+
|
|
153
|
+
const account = await accountForCaller(
|
|
154
|
+
accounts,
|
|
155
|
+
caller({
|
|
156
|
+
identityId: accountIdFor("auth0|known"),
|
|
157
|
+
issuer: "https://issuer.test",
|
|
158
|
+
rawToken: tokenFor("auth0|known"),
|
|
159
|
+
}),
|
|
160
|
+
);
|
|
161
|
+
|
|
162
|
+
expect(account?.metadata?.id).toBe(accountIdFor("auth0|known"));
|
|
163
|
+
expect(bySubject).not.toHaveBeenCalled();
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it("a raw-subject identityId resolves through the token's sub — the composition-verifier shape", async () => {
|
|
167
|
+
const accounts = seeded("auth0|raw");
|
|
168
|
+
|
|
169
|
+
const account = await accountForCaller(
|
|
170
|
+
accounts,
|
|
171
|
+
caller({
|
|
172
|
+
identityId: "auth0|raw",
|
|
173
|
+
issuer: "",
|
|
174
|
+
rawToken: tokenFor("auth0|raw"),
|
|
175
|
+
}),
|
|
176
|
+
);
|
|
177
|
+
|
|
178
|
+
expect(account?.metadata?.id).toBe(accountIdFor("auth0|raw"));
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
it("the trusted-local operator resolves through local|<identityId>", async () => {
|
|
182
|
+
const accounts = seeded(localIdpIdFor("operator@example.com"));
|
|
183
|
+
|
|
184
|
+
const account = await accountForCaller(
|
|
185
|
+
accounts,
|
|
186
|
+
caller({ identityId: "operator@example.com" }),
|
|
187
|
+
);
|
|
188
|
+
|
|
189
|
+
expect(account?.metadata?.id).toBe(
|
|
190
|
+
accountIdFor(localIdpIdFor("operator@example.com")),
|
|
191
|
+
);
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
it("a credential naming no subject is undefined and the store is never asked about ''", async () => {
|
|
195
|
+
const accounts = seeded("auth0|someone");
|
|
196
|
+
const bySubject = vi.spyOn(accounts, "findDirectByIdpId");
|
|
197
|
+
|
|
198
|
+
const account = await accountForCaller(
|
|
199
|
+
accounts,
|
|
200
|
+
caller({
|
|
201
|
+
identityId: "opaque",
|
|
202
|
+
issuer: "https://issuer.test",
|
|
203
|
+
rawToken: "not-a-jwt",
|
|
204
|
+
}),
|
|
205
|
+
);
|
|
206
|
+
|
|
207
|
+
expect(account).toBeUndefined();
|
|
208
|
+
expect(bySubject).not.toHaveBeenCalled();
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
it("an idp-shaped caller with no account is undefined", async () => {
|
|
212
|
+
const accounts = seeded("auth0|someone-else");
|
|
213
|
+
|
|
214
|
+
const account = await accountForCaller(
|
|
215
|
+
accounts,
|
|
216
|
+
caller({
|
|
217
|
+
identityId: "auth0|stranger",
|
|
218
|
+
issuer: "https://issuer.test",
|
|
219
|
+
rawToken: tokenFor("auth0|stranger"),
|
|
220
|
+
}),
|
|
221
|
+
);
|
|
222
|
+
|
|
223
|
+
expect(account).toBeUndefined();
|
|
224
|
+
});
|
|
225
|
+
});
|
|
@@ -5,17 +5,20 @@
|
|
|
5
5
|
*
|
|
6
6
|
* The derived id (A1). A direct account's id is a pure function of its
|
|
7
7
|
* subject: `ida_` + the top 130 bits of sha256(idp_id) as 26 lowercase
|
|
8
|
-
* Crockford-base32 characters
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
8
|
+
* Crockford-base32 characters, through `derivedId` in
|
|
9
|
+
* pipeline/steps/defaults.ts (the one encoder every derived kind shares
|
|
10
|
+
* since 20260913.01 slice 2; the IamPolicy id is the other). The shape is
|
|
11
|
+
* indistinguishable from a minted `{prefix}_{ulid}`, so nothing downstream
|
|
12
|
+
* learns a second id grammar, and the PRIMARY KEY becomes the one home of
|
|
13
|
+
* "one account per subject": the generic store has no secondary uniqueness
|
|
14
|
+
* and its save is an upsert (`store/sqlite/store.ts`), so a
|
|
15
|
+
* lookup-then-insert on a scan could never close the first-login race. Two
|
|
16
|
+
* replicas deriving the same id converge by primary key with no migration
|
|
17
|
+
* and no second copy of any field. The hash is one-way, so an issuer
|
|
18
|
+
* subject never leaks into the `created_by.id` stamps that ride on every
|
|
19
|
+
* resource. The golden vectors in __tests__/constants.test.ts are
|
|
20
|
+
* wire-adjacent contract: a change here re-addresses every direct account
|
|
21
|
+
* open source ever created.
|
|
19
22
|
*
|
|
20
23
|
* `idpIdOf` (A2) is the ONE place a CallerIdentity becomes a subject. An
|
|
21
24
|
* issuer-vouched token is read for its `sub` (already verified at
|
|
@@ -28,19 +31,12 @@
|
|
|
28
31
|
* The copy moved from the cloud's iam/account/handlers.ts as-is: the CLI,
|
|
29
32
|
* console and SDK show these sentences verbatim, so they are contract.
|
|
30
33
|
*/
|
|
31
|
-
import { createHash } from "node:crypto";
|
|
32
|
-
|
|
33
34
|
import type { CallerIdentity } from "../../extensions/identity.js";
|
|
35
|
+
import { derivedId } from "../../pipeline/steps/defaults.js";
|
|
34
36
|
|
|
35
37
|
/** The kind's id prefix (kind_meta `identity_account.id_prefix`). */
|
|
36
38
|
export const ACCOUNT_ID_PREFIX = "ida";
|
|
37
39
|
|
|
38
|
-
/** Crockford base32, lowercased — the alphabet every minted ULID id uses. */
|
|
39
|
-
const CROCKFORD_ALPHABET = "0123456789abcdefghjkmnpqrstvwxyz";
|
|
40
|
-
const DERIVED_ID_CHARS = 26;
|
|
41
|
-
const DERIVED_ID_BITS = BigInt(DERIVED_ID_CHARS * 5);
|
|
42
|
-
const SHA256_BITS = 256n;
|
|
43
|
-
|
|
44
40
|
/**
|
|
45
41
|
* The derived account id of a direct account with subject `idpId`. Pure,
|
|
46
42
|
* total over non-empty input; an empty subject has no principal and so
|
|
@@ -50,18 +46,7 @@ export function accountIdFor(idpId: string): string {
|
|
|
50
46
|
if (idpId === "") {
|
|
51
47
|
throw new Error("idp_id must not be empty");
|
|
52
48
|
}
|
|
53
|
-
|
|
54
|
-
let bits = 0n;
|
|
55
|
-
for (const byte of digest) {
|
|
56
|
-
bits = (bits << 8n) | BigInt(byte);
|
|
57
|
-
}
|
|
58
|
-
let top = bits >> (SHA256_BITS - DERIVED_ID_BITS);
|
|
59
|
-
let encoded = "";
|
|
60
|
-
for (let i = 0; i < DERIVED_ID_CHARS; i++) {
|
|
61
|
-
encoded = CROCKFORD_ALPHABET[Number(top & 31n)] + encoded;
|
|
62
|
-
top >>= 5n;
|
|
63
|
-
}
|
|
64
|
-
return `${ACCOUNT_ID_PREFIX}_${encoded}`;
|
|
49
|
+
return derivedId(ACCOUNT_ID_PREFIX, idpId);
|
|
65
50
|
}
|
|
66
51
|
|
|
67
52
|
/**
|
|
@@ -27,13 +27,16 @@
|
|
|
27
27
|
* ensure runs it as the trusted-local user.
|
|
28
28
|
*
|
|
29
29
|
* Direct handlers (no pipeline): whoAmI (the cloud's two primary-key
|
|
30
|
-
* reads
|
|
31
|
-
* getByIdpId (lookup FIRST, then
|
|
32
|
-
* cloud's order; the annotation's
|
|
33
|
-
* hand the Authorizer an email as a
|
|
34
|
-
* provisionMyAccount (the provisioner,
|
|
35
|
-
*
|
|
36
|
-
*
|
|
30
|
+
* reads, stated once as resolve.ts accountForCaller; NOT_FOUND for the
|
|
31
|
+
* unprovisioned), getByEmail and getByIdpId (lookup FIRST, then
|
|
32
|
+
* authorizeDirect on the FOUND id — the cloud's order; the annotation's
|
|
33
|
+
* `field_path = "value"` would otherwise hand the Authorizer an email as a
|
|
34
|
+
* resource id), getActorInfo, and provisionMyAccount (the provisioner,
|
|
35
|
+
* then — on the call that CREATED the row — the core AccountCreatedHook
|
|
36
|
+
* (open source's membership rules, 20260913.01 slice 4), then the composed
|
|
37
|
+
* post-persist gates; UNAUTHENTICATED with the cloud's copy for a
|
|
38
|
+
* credential naming no subject; UNAVAILABLE with the cloud's copy when
|
|
39
|
+
* userinfo fails).
|
|
37
40
|
*
|
|
38
41
|
* The four federation RPCs refuse UNIMPLEMENTED with the edition reason
|
|
39
42
|
* when no unit composes the capability (never INTERNAL; the organization
|
|
@@ -69,6 +72,7 @@ import type { Logger } from "../../boot/logger.js";
|
|
|
69
72
|
import type { Authorizer } from "../../extensions/authorizer.js";
|
|
70
73
|
import { stepsForSlot } from "../../extensions/gate-slots.js";
|
|
71
74
|
import type { ResolvedGateSteps } from "../../extensions/gate-slots.js";
|
|
75
|
+
import { isPlatformPipelineCaller } from "../../extensions/identity.js";
|
|
72
76
|
import type { CallerIdentity } from "../../extensions/identity.js";
|
|
73
77
|
import type { IdentityFederation } from "../../extensions/identity-federation.js";
|
|
74
78
|
import type { ResourceAuthorizationLifecycle } from "../../extensions/resource-authorization.js";
|
|
@@ -102,11 +106,13 @@ import {
|
|
|
102
106
|
idpIdOf,
|
|
103
107
|
} from "./constants.js";
|
|
104
108
|
import type {
|
|
109
|
+
AccountCreatedHook,
|
|
105
110
|
CreateAccount,
|
|
106
111
|
CreateAccountInput,
|
|
107
112
|
DirectAccountProvisioner,
|
|
108
113
|
} from "./provisioning.js";
|
|
109
114
|
import { UserInfoFetchError } from "./provisioning.js";
|
|
115
|
+
import { accountForCaller } from "./resolve.js";
|
|
110
116
|
import {
|
|
111
117
|
accountNotFoundError,
|
|
112
118
|
newAssignBackendFieldsStep,
|
|
@@ -140,6 +146,13 @@ export interface IdentityAccountControllerDeps extends CreateAccountPathDeps {
|
|
|
140
146
|
readonly federation: IdentityFederation | undefined;
|
|
141
147
|
/** The composed slot registrations — this domain's provision slot (Q-IA-9, A10). */
|
|
142
148
|
readonly gateSteps: ResolvedGateSteps;
|
|
149
|
+
/**
|
|
150
|
+
* The core first-provisioning rule — open source's membership rules
|
|
151
|
+
* under the built-in authorization posture; undefined when a
|
|
152
|
+
* composition brings its own Authorizer and so its own onboarding
|
|
153
|
+
* (20260913.01, Q-OR-6).
|
|
154
|
+
*/
|
|
155
|
+
readonly membership: AccountCreatedHook | undefined;
|
|
143
156
|
}
|
|
144
157
|
|
|
145
158
|
/** Registers both identityaccount services on the router (routes stage). */
|
|
@@ -294,11 +307,9 @@ function createRpc(
|
|
|
294
307
|
}
|
|
295
308
|
|
|
296
309
|
function guardInternalRpc(caller: CallerIdentity): void {
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
caller.origin === "in-process"
|
|
301
|
-
) {
|
|
310
|
+
// The one definition of "the platform's own pipelines"
|
|
311
|
+
// (extensions/identity.ts), shared with the IamPolicy system RPCs.
|
|
312
|
+
if (isPlatformPipelineCaller(caller)) {
|
|
302
313
|
return;
|
|
303
314
|
}
|
|
304
315
|
throw new ConnectError(CREATE_IS_INTERNAL_MESSAGE, Code.PermissionDenied);
|
|
@@ -458,16 +469,12 @@ async function whoAmI(
|
|
|
458
469
|
ctx: HandlerContext,
|
|
459
470
|
): Promise<IdentityAccount> {
|
|
460
471
|
const caller = callerIdentityOf(ctx);
|
|
461
|
-
|
|
462
|
-
|
|
472
|
+
// The domain's one statement of "the caller's account" (resolve.ts):
|
|
473
|
+
// by id, then by subject — shared with the built-in role lifecycle.
|
|
474
|
+
const account = await read(
|
|
475
|
+
() => accountForCaller(deps.accounts, caller),
|
|
463
476
|
"failed to load identity account",
|
|
464
477
|
);
|
|
465
|
-
if (account === undefined) {
|
|
466
|
-
account = await read(
|
|
467
|
-
() => deps.accounts.findDirectByIdpId(idpIdOf(caller)),
|
|
468
|
-
"failed to load identity account",
|
|
469
|
-
);
|
|
470
|
-
}
|
|
471
478
|
if (account === undefined) {
|
|
472
479
|
throw new ConnectError(ACCOUNT_NOT_FOUND_FOR_CALLER_MESSAGE, Code.NotFound);
|
|
473
480
|
}
|
|
@@ -530,16 +537,28 @@ async function lookupThenAuthorize(
|
|
|
530
537
|
|
|
531
538
|
/**
|
|
532
539
|
* provisionMyAccount — the subject from the caller's credential, the
|
|
533
|
-
* provisioner's flow, then
|
|
534
|
-
*
|
|
535
|
-
*
|
|
536
|
-
*
|
|
537
|
-
*
|
|
538
|
-
*
|
|
539
|
-
*
|
|
540
|
-
*
|
|
541
|
-
* the
|
|
542
|
-
*
|
|
540
|
+
* provisioner's flow, then two post-persist consumers with the caller
|
|
541
|
+
* RE-STAMPED as the account: the position-1 identity was idp-shaped
|
|
542
|
+
* because no row existed when the verifier ran, and anything that
|
|
543
|
+
* attributes ownership must name the account, never a principal no later
|
|
544
|
+
* request carries.
|
|
545
|
+
*
|
|
546
|
+
* 1. The core AccountCreatedHook (open source's membership rules), on
|
|
547
|
+
* the call that CREATED the row only (`created`): the slot below
|
|
548
|
+
* fires on the idempotent path by design, and a rule keyed on "holds
|
|
549
|
+
* no row" there would re-admit a member an administrator had just
|
|
550
|
+
* revoked (20260913.01 plan finding 2). A throw fails the request
|
|
551
|
+
* INTERNAL with the row in place and the rule does not run again for
|
|
552
|
+
* that account — the ratified store-fault mapping, and the one loud
|
|
553
|
+
* record of the window slice 4's Q-S4-4 accepted: the rule's writes
|
|
554
|
+
* are idempotent, so a partial run is harmless, and the recovery is
|
|
555
|
+
* an administrator's grant.
|
|
556
|
+
* 2. The composed post-persist gates (the cloud's personal
|
|
557
|
+
* organization), on EVERY call — the cloud's step backfills accounts
|
|
558
|
+
* that predate personal organizations there, which is why Q-IA-9
|
|
559
|
+
* chose a slot over onResourceCreated. Non-transactional in the
|
|
560
|
+
* `org-create:post-persist` sense: a gate failure fails the request,
|
|
561
|
+
* the row survives, the next call heals it.
|
|
543
562
|
*/
|
|
544
563
|
async function provisionMyAccount(
|
|
545
564
|
deps: IdentityAccountControllerDeps,
|
|
@@ -552,8 +571,12 @@ async function provisionMyAccount(
|
|
|
552
571
|
throw new ConnectError(NO_IDP_ID_MESSAGE, Code.Unauthenticated);
|
|
553
572
|
}
|
|
554
573
|
let account: IdentityAccount;
|
|
574
|
+
let created: boolean;
|
|
555
575
|
try {
|
|
556
|
-
account = await deps.provisioner.provisionDirectAccount(
|
|
576
|
+
({ account, created } = await deps.provisioner.provisionDirectAccount(
|
|
577
|
+
idpId,
|
|
578
|
+
caller,
|
|
579
|
+
));
|
|
557
580
|
} catch (error) {
|
|
558
581
|
if (error instanceof UserInfoFetchError) {
|
|
559
582
|
throw new ConnectError(
|
|
@@ -563,6 +586,17 @@ async function provisionMyAccount(
|
|
|
563
586
|
}
|
|
564
587
|
throw error;
|
|
565
588
|
}
|
|
589
|
+
const asAccount: CallerIdentity = {
|
|
590
|
+
...caller,
|
|
591
|
+
identityId: account.metadata?.id ?? "",
|
|
592
|
+
};
|
|
593
|
+
if (created && deps.membership !== undefined) {
|
|
594
|
+
try {
|
|
595
|
+
await deps.membership.onAccountCreated(account, asAccount);
|
|
596
|
+
} catch (error) {
|
|
597
|
+
throw internalError(error, "failed to record organization membership");
|
|
598
|
+
}
|
|
599
|
+
}
|
|
566
600
|
// The ratified provision slot (Q-IA-9; the gate-slots.ts header carries
|
|
567
601
|
// its semantics). Empty in OSS — no pipeline is built for zero steps.
|
|
568
602
|
const gates = stepsForSlot<typeof IdentityAccountSchema>(
|
|
@@ -570,10 +604,6 @@ async function provisionMyAccount(
|
|
|
570
604
|
"identity-account-provision:post-persist",
|
|
571
605
|
);
|
|
572
606
|
if (gates.length > 0) {
|
|
573
|
-
const asAccount: CallerIdentity = {
|
|
574
|
-
...caller,
|
|
575
|
-
identityId: account.metadata?.id ?? "",
|
|
576
|
-
};
|
|
577
607
|
const reqCtx = new RequestContext(
|
|
578
608
|
IdentityAccountSchema,
|
|
579
609
|
account,
|
|
@@ -34,7 +34,10 @@ import { create } from "@bufbuild/protobuf";
|
|
|
34
34
|
import type { IdentityAccount } from "@stigmer/protos/ai/stigmer/iam/identityaccount/v1/api_pb";
|
|
35
35
|
import { IdentityAccountSpecSchema } from "@stigmer/protos/ai/stigmer/iam/identityaccount/v1/spec_pb";
|
|
36
36
|
|
|
37
|
-
import {
|
|
37
|
+
import {
|
|
38
|
+
SYSTEM_OPERATOR_IDENTITY_ID,
|
|
39
|
+
trustedLocalIdentityFor,
|
|
40
|
+
} from "../../pipeline/interceptors/auth.js";
|
|
38
41
|
import { accountIdFor, localIdpIdFor } from "./constants.js";
|
|
39
42
|
import type { CreateAccount } from "./provisioning.js";
|
|
40
43
|
import { resolveCreateRace } from "./provisioning.js";
|
|
@@ -52,9 +55,6 @@ export interface OperatorAccountDeps {
|
|
|
52
55
|
readonly createAccount: CreateAccount;
|
|
53
56
|
}
|
|
54
57
|
|
|
55
|
-
/** The unconfigured laptop's operator: what the interceptor stamps as identityId. */
|
|
56
|
-
const SYSTEM_OPERATOR = "system";
|
|
57
|
-
|
|
58
58
|
export async function ensureOperatorAccount(
|
|
59
59
|
deps: OperatorAccountDeps,
|
|
60
60
|
operator: OperatorIdentity,
|
|
@@ -72,7 +72,7 @@ export async function ensureOperatorAccount(
|
|
|
72
72
|
? operator.displayName
|
|
73
73
|
: operator.email !== ""
|
|
74
74
|
? operator.email
|
|
75
|
-
:
|
|
75
|
+
: SYSTEM_OPERATOR_IDENTITY_ID;
|
|
76
76
|
try {
|
|
77
77
|
return await deps.createAccount(
|
|
78
78
|
{
|
|
@@ -28,6 +28,16 @@
|
|
|
28
28
|
* fires on the `identity-account-provision:post-persist` slot in the
|
|
29
29
|
* composition (A10), wired by the controller.
|
|
30
30
|
*
|
|
31
|
+
* The answer says whether THIS call created the row (`ProvisionedAccount.
|
|
32
|
+
* created`; 20260913.01 slice 4): the idempotent early return and the
|
|
33
|
+
* race loser answer false. The slot above fires on every call by design
|
|
34
|
+
* (the cloud's backfill needs that), so a CORE rule that must run exactly
|
|
35
|
+
* once per person — the open-source membership rules, which hand out
|
|
36
|
+
* organization roles at first sign-in — keys on `created` instead, through
|
|
37
|
+
* the AccountCreatedHook this domain offers and the controller runs. The
|
|
38
|
+
* hook is declared here, in the domain that fires it, so the domain that
|
|
39
|
+
* implements it (iampolicy) depends on this one and never the reverse.
|
|
40
|
+
*
|
|
31
41
|
* This module holds the domain flow over two PORTS — the store and the
|
|
32
42
|
* userinfo client — and no network code; identity/oidc-userinfo.ts is the
|
|
33
43
|
* OIDC implementation the composition root wires in.
|
|
@@ -88,22 +98,46 @@ export interface DirectAccountProvisionerDeps {
|
|
|
88
98
|
readonly userInfo: UserInfoClient;
|
|
89
99
|
}
|
|
90
100
|
|
|
101
|
+
/** The provisioner's answer: the account, and whether this call created its row. */
|
|
102
|
+
export interface ProvisionedAccount {
|
|
103
|
+
readonly account: IdentityAccount;
|
|
104
|
+
/** False on the idempotent early return and for a first-login race's loser. */
|
|
105
|
+
readonly created: boolean;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* What the identity-account domain offers a core rule keyed on a person's
|
|
110
|
+
* FIRST provisioning (the open-source membership rules,
|
|
111
|
+
* domain/iampolicy/membership.ts). Runs once per account, on the call
|
|
112
|
+
* whose `created` is true, with the caller RE-STAMPED as the account, so
|
|
113
|
+
* whatever the hook writes attributes to the account and never to the
|
|
114
|
+
* idp-shaped principal the verifier admitted before the row existed. Its
|
|
115
|
+
* failure semantics are the controller's: a throw fails the request with
|
|
116
|
+
* the account in place, and the hook does not run again for that account.
|
|
117
|
+
*/
|
|
118
|
+
export interface AccountCreatedHook {
|
|
119
|
+
onAccountCreated(
|
|
120
|
+
account: IdentityAccount,
|
|
121
|
+
caller: CallerIdentity,
|
|
122
|
+
): Promise<void>;
|
|
123
|
+
}
|
|
124
|
+
|
|
91
125
|
export interface DirectAccountProvisioner {
|
|
92
126
|
/** provisionMyAccount's domain flow; `idpId` is the caller's verified subject. */
|
|
93
127
|
provisionDirectAccount(
|
|
94
128
|
idpId: string,
|
|
95
129
|
caller: CallerIdentity,
|
|
96
|
-
): Promise<
|
|
130
|
+
): Promise<ProvisionedAccount>;
|
|
97
131
|
}
|
|
98
132
|
|
|
99
133
|
export function newDirectAccountProvisioner(
|
|
100
134
|
deps: DirectAccountProvisionerDeps,
|
|
101
135
|
): DirectAccountProvisioner {
|
|
102
136
|
return {
|
|
103
|
-
async provisionDirectAccount(idpId, caller): Promise<
|
|
137
|
+
async provisionDirectAccount(idpId, caller): Promise<ProvisionedAccount> {
|
|
104
138
|
const existing = await deps.accounts.findDirectByIdpId(idpId);
|
|
105
139
|
if (existing !== undefined) {
|
|
106
|
-
return existing;
|
|
140
|
+
return { account: existing, created: false };
|
|
107
141
|
}
|
|
108
142
|
|
|
109
143
|
const profile = await profileOf(deps.userInfo, caller);
|
|
@@ -121,11 +155,15 @@ export function newDirectAccountProvisioner(
|
|
|
121
155
|
};
|
|
122
156
|
|
|
123
157
|
try {
|
|
124
|
-
return
|
|
158
|
+
return {
|
|
159
|
+
account: await deps.createAccount(input, caller),
|
|
160
|
+
created: true,
|
|
161
|
+
};
|
|
125
162
|
} catch (error) {
|
|
126
|
-
|
|
163
|
+
const winner = await resolveCreateRace(error, () =>
|
|
127
164
|
deps.accounts.findDirectByIdpId(idpId),
|
|
128
165
|
);
|
|
166
|
+
return { account: winner, created: false };
|
|
129
167
|
}
|
|
130
168
|
},
|
|
131
169
|
};
|
|
@@ -28,12 +28,35 @@
|
|
|
28
28
|
* no id is the one deliberate divergence from the cloud's inline
|
|
29
29
|
* `metadata?.id ?? ""`: a `""` principal is the silent-junk failure the
|
|
30
30
|
* oss#405 doctrine forbids, so it is a loud fault naming the subject.
|
|
31
|
+
*
|
|
32
|
+
* The same knowledge read in the caller's direction is `accountForCaller`
|
|
33
|
+
* (20260913.01 slice 4, Q-S4-1): the account a stamped CallerIdentity
|
|
34
|
+
* stands for. Two primary-key reads, the cloud's whoAmI order — the
|
|
35
|
+
* identityId AS an account id (a verifier that resolved), then the
|
|
36
|
+
* caller's subject through the direct lookup (a verifier that did not:
|
|
37
|
+
* the trusted-local interceptor, which stamps the operator's email and
|
|
38
|
+
* never consults the store; a composition verifier that runs before any
|
|
39
|
+
* row exists). It lives here so whoAmI and the built-in role lifecycle,
|
|
40
|
+
* which asks "whose organization is this" from the creating caller,
|
|
41
|
+
* cannot answer the same question two ways. A credential naming no
|
|
42
|
+
* subject is `undefined` with no second read: the store is never asked
|
|
43
|
+
* about "".
|
|
31
44
|
*/
|
|
45
|
+
import type { IdentityAccount } from "@stigmer/protos/ai/stigmer/iam/identityaccount/v1/api_pb";
|
|
46
|
+
|
|
47
|
+
import type { CallerIdentity } from "../../extensions/identity.js";
|
|
48
|
+
import { idpIdOf } from "./constants.js";
|
|
32
49
|
import type { IdentityAccountStore } from "./store.js";
|
|
33
50
|
|
|
34
51
|
/** The one read a verifier needs from the domain: the direct account for a subject. */
|
|
35
52
|
export type AccountsBySubject = Pick<IdentityAccountStore, "findDirectByIdpId">;
|
|
36
53
|
|
|
54
|
+
/** The two reads the caller-direction resolution needs: by id, then by subject. */
|
|
55
|
+
export type AccountsByCaller = Pick<
|
|
56
|
+
IdentityAccountStore,
|
|
57
|
+
"findById" | "findDirectByIdpId"
|
|
58
|
+
>;
|
|
59
|
+
|
|
37
60
|
export async function identityIdForSubject(
|
|
38
61
|
accounts: AccountsBySubject,
|
|
39
62
|
subject: string,
|
|
@@ -50,3 +73,23 @@ export async function identityIdForSubject(
|
|
|
50
73
|
}
|
|
51
74
|
return id;
|
|
52
75
|
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* The account `caller` stands for, or `undefined` when there is none (an
|
|
79
|
+
* idp-shaped caller before provisioning; a credential naming no subject).
|
|
80
|
+
* Faults propagate as they are — the caller decides the wire shape.
|
|
81
|
+
*/
|
|
82
|
+
export async function accountForCaller(
|
|
83
|
+
accounts: AccountsByCaller,
|
|
84
|
+
caller: CallerIdentity,
|
|
85
|
+
): Promise<IdentityAccount | undefined> {
|
|
86
|
+
const byId = await accounts.findById(caller.identityId);
|
|
87
|
+
if (byId !== undefined) {
|
|
88
|
+
return byId;
|
|
89
|
+
}
|
|
90
|
+
const subject = idpIdOf(caller);
|
|
91
|
+
if (subject === "") {
|
|
92
|
+
return undefined;
|
|
93
|
+
}
|
|
94
|
+
return accounts.findDirectByIdpId(subject);
|
|
95
|
+
}
|