@stigmer/server 3.24.0 → 3.25.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/authorization/authorizer.d.ts +10 -4
- package/dist/authorization/authorizer.d.ts.map +1 -1
- package/dist/authorization/authorizer.js +11 -4
- package/dist/authorization/authorizer.js.map +1 -1
- package/dist/authorization/model/index.d.ts +4 -4
- package/dist/authorization/model/index.d.ts.map +1 -1
- package/dist/authorization/model/index.js +2 -0
- package/dist/authorization/model/index.js.map +1 -1
- package/dist/authorization/model/oauth_app.d.ts.map +1 -1
- package/dist/authorization/model/oauth_app.js +3 -2
- package/dist/authorization/model/oauth_app.js.map +1 -1
- package/dist/authorization/model/platform_client.d.ts +2 -0
- package/dist/authorization/model/platform_client.d.ts.map +1 -0
- package/dist/authorization/model/platform_client.js +31 -0
- package/dist/authorization/model/platform_client.js.map +1 -0
- package/dist/boot/compose.d.ts.map +1 -1
- package/dist/boot/compose.js +88 -5
- package/dist/boot/compose.js.map +1 -1
- package/dist/boot/route-shadowing.d.ts +25 -0
- package/dist/boot/route-shadowing.d.ts.map +1 -0
- package/dist/boot/route-shadowing.js +17 -0
- package/dist/boot/route-shadowing.js.map +1 -0
- package/dist/domain/iampolicy/membership.d.ts +6 -1
- package/dist/domain/iampolicy/membership.d.ts.map +1 -1
- package/dist/domain/iampolicy/membership.js +8 -2
- package/dist/domain/iampolicy/membership.js.map +1 -1
- package/dist/domain/identityaccount/constants.d.ts +13 -0
- package/dist/domain/identityaccount/constants.d.ts.map +1 -1
- package/dist/domain/identityaccount/constants.js +17 -0
- package/dist/domain/identityaccount/constants.js.map +1 -1
- package/dist/domain/identityaccount/controller.d.ts +3 -1
- package/dist/domain/identityaccount/controller.d.ts.map +1 -1
- package/dist/domain/identityaccount/controller.js +15 -6
- 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 +1 -0
- package/dist/domain/identityaccount/operator.js.map +1 -1
- package/dist/domain/identityaccount/provisioning.d.ts +18 -0
- package/dist/domain/identityaccount/provisioning.d.ts.map +1 -1
- package/dist/domain/identityaccount/provisioning.js +1 -0
- package/dist/domain/identityaccount/provisioning.js.map +1 -1
- package/dist/domain/identityaccount/resource-store.js +9 -5
- package/dist/domain/identityaccount/resource-store.js.map +1 -1
- package/dist/domain/identityaccount/steps.d.ts +11 -2
- package/dist/domain/identityaccount/steps.d.ts.map +1 -1
- package/dist/domain/identityaccount/steps.js +42 -4
- package/dist/domain/identityaccount/steps.js.map +1 -1
- package/dist/domain/identityaccount/store-contract.d.ts.map +1 -1
- package/dist/domain/identityaccount/store-contract.js +33 -0
- package/dist/domain/identityaccount/store-contract.js.map +1 -1
- package/dist/domain/identityaccount/store.d.ts +7 -5
- package/dist/domain/identityaccount/store.d.ts.map +1 -1
- package/dist/domain/identityaccount/store.js.map +1 -1
- package/dist/domain/platform/controller.d.ts +10 -0
- package/dist/domain/platform/controller.d.ts.map +1 -1
- package/dist/domain/platform/controller.js +2 -1
- package/dist/domain/platform/controller.js.map +1 -1
- package/dist/domain/platformclient/constants.d.ts +60 -0
- package/dist/domain/platformclient/constants.d.ts.map +1 -0
- package/dist/domain/platformclient/constants.js +78 -0
- package/dist/domain/platformclient/constants.js.map +1 -0
- package/dist/domain/platformclient/controller.d.ts +59 -0
- package/dist/domain/platformclient/controller.d.ts.map +1 -0
- package/dist/domain/platformclient/controller.js +191 -0
- package/dist/domain/platformclient/controller.js.map +1 -0
- package/dist/domain/platformclient/credentials.d.ts +13 -0
- package/dist/domain/platformclient/credentials.d.ts.map +1 -0
- package/dist/domain/platformclient/credentials.js +56 -0
- package/dist/domain/platformclient/credentials.js.map +1 -0
- package/dist/domain/platformclient/mint.d.ts +23 -0
- package/dist/domain/platformclient/mint.d.ts.map +1 -0
- package/dist/domain/platformclient/mint.js +254 -0
- package/dist/domain/platformclient/mint.js.map +1 -0
- package/dist/domain/platformclient/origin-guard.d.ts +9 -0
- package/dist/domain/platformclient/origin-guard.d.ts.map +1 -0
- package/dist/domain/platformclient/origin-guard.js +81 -0
- package/dist/domain/platformclient/origin-guard.js.map +1 -0
- package/dist/domain/platformclient/resource-store.d.ts +4 -0
- package/dist/domain/platformclient/resource-store.d.ts.map +1 -0
- package/dist/domain/platformclient/resource-store.js +117 -0
- package/dist/domain/platformclient/resource-store.js.map +1 -0
- package/dist/domain/platformclient/steps.d.ts +51 -0
- package/dist/domain/platformclient/steps.d.ts.map +1 -0
- package/dist/domain/platformclient/steps.js +339 -0
- package/dist/domain/platformclient/steps.js.map +1 -0
- package/dist/domain/platformclient/store-contract.d.ts +8 -0
- package/dist/domain/platformclient/store-contract.d.ts.map +1 -0
- package/dist/domain/platformclient/store-contract.js +208 -0
- package/dist/domain/platformclient/store-contract.js.map +1 -0
- package/dist/domain/platformclient/store.d.ts +58 -0
- package/dist/domain/platformclient/store.d.ts.map +1 -0
- package/dist/domain/platformclient/store.js +13 -0
- package/dist/domain/platformclient/store.js.map +1 -0
- package/dist/domain/platformclient/system-managed.d.ts +18 -0
- package/dist/domain/platformclient/system-managed.d.ts.map +1 -0
- package/dist/domain/platformclient/system-managed.js +80 -0
- package/dist/domain/platformclient/system-managed.js.map +1 -0
- package/dist/domain/platformclient/token-controller.d.ts +11 -0
- package/dist/domain/platformclient/token-controller.d.ts.map +1 -0
- package/dist/domain/platformclient/token-controller.js +32 -0
- package/dist/domain/platformclient/token-controller.js.map +1 -0
- package/dist/domain/platformclient/verifier.d.ts +10 -0
- package/dist/domain/platformclient/verifier.d.ts.map +1 -0
- package/dist/domain/platformclient/verifier.js +76 -0
- package/dist/domain/platformclient/verifier.js.map +1 -0
- package/dist/encryption/key-manager.d.ts +20 -3
- package/dist/encryption/key-manager.d.ts.map +1 -1
- package/dist/encryption/key-manager.js +64 -42
- package/dist/encryption/key-manager.js.map +1 -1
- package/dist/extensions/caller-guards.d.ts +8 -3
- package/dist/extensions/caller-guards.d.ts.map +1 -1
- package/dist/extensions/drivers.d.ts +37 -0
- package/dist/extensions/drivers.d.ts.map +1 -1
- package/dist/extensions/guest-token-minting.d.ts +26 -0
- package/dist/extensions/guest-token-minting.d.ts.map +1 -0
- package/dist/extensions/guest-token-minting.js +2 -0
- package/dist/extensions/guest-token-minting.js.map +1 -0
- package/dist/extensions/identity.d.ts +6 -4
- package/dist/extensions/identity.d.ts.map +1 -1
- package/dist/extensions/identity.js.map +1 -1
- package/dist/extensions/registry.d.ts +30 -1
- package/dist/extensions/registry.d.ts.map +1 -1
- package/dist/extensions/registry.js +34 -1
- package/dist/extensions/registry.js.map +1 -1
- package/dist/index.d.ts +13 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -0
- package/dist/index.js.map +1 -1
- package/dist/pipeline/interceptors/auth.d.ts +14 -0
- package/dist/pipeline/interceptors/auth.d.ts.map +1 -1
- package/dist/pipeline/interceptors/auth.js +24 -0
- package/dist/pipeline/interceptors/auth.js.map +1 -1
- package/dist/pipeline/steps/build-update-state.d.ts +11 -1
- package/dist/pipeline/steps/build-update-state.d.ts.map +1 -1
- package/dist/pipeline/steps/build-update-state.js +5 -2
- package/dist/pipeline/steps/build-update-state.js.map +1 -1
- package/dist/platformtoken/envelope.d.ts +100 -0
- package/dist/platformtoken/envelope.d.ts.map +1 -0
- package/dist/platformtoken/envelope.js +184 -0
- package/dist/platformtoken/envelope.js.map +1 -0
- package/dist/platformtoken/key-ring.d.ts +85 -0
- package/dist/platformtoken/key-ring.d.ts.map +1 -0
- package/dist/platformtoken/key-ring.js +164 -0
- package/dist/platformtoken/key-ring.js.map +1 -0
- package/package.json +6 -6
- package/src/authorization/README.md +1 -1
- package/src/authorization/__tests__/authorizer.test.ts +6 -2
- package/src/authorization/__tests__/wire-permissions.test.ts +1 -0
- package/src/authorization/authorizer.ts +11 -4
- package/src/authorization/model/__tests__/registry.test.ts +8 -1
- package/src/authorization/model/index.ts +6 -4
- package/src/authorization/model/oauth_app.ts +3 -2
- package/src/authorization/model/platform_client.ts +43 -0
- package/src/boot/__tests__/auth-enabled-composition.test.ts +2 -1
- package/src/boot/__tests__/route-shadowing.test.ts +67 -0
- package/src/boot/compose.ts +90 -5
- package/src/boot/route-shadowing.ts +47 -0
- package/src/domain/iampolicy/__tests__/membership.test.ts +7 -0
- package/src/domain/iampolicy/membership.ts +8 -2
- package/src/domain/identityaccount/__tests__/platform-client-accounts.test.ts +124 -0
- package/src/domain/identityaccount/__tests__/resource-store.test.ts +1 -0
- package/src/domain/identityaccount/constants.ts +20 -0
- package/src/domain/identityaccount/controller.ts +16 -4
- package/src/domain/identityaccount/operator.ts +1 -0
- package/src/domain/identityaccount/provisioning.ts +17 -0
- package/src/domain/identityaccount/resource-store.ts +9 -5
- package/src/domain/identityaccount/steps.ts +58 -3
- package/src/domain/identityaccount/store-contract.ts +51 -0
- package/src/domain/identityaccount/store.ts +7 -5
- package/src/domain/platform/__tests__/platform.test.ts +7 -1
- package/src/domain/platform/controller.ts +12 -1
- package/src/domain/platformclient/__tests__/controller.test.ts +397 -0
- package/src/domain/platformclient/__tests__/credentials.test.ts +46 -0
- package/src/domain/platformclient/__tests__/mint.test.ts +415 -0
- package/src/domain/platformclient/__tests__/resource-store.test.ts +140 -0
- package/src/domain/platformclient/__tests__/system-managed.test.ts +101 -0
- package/src/domain/platformclient/__tests__/verifier-and-guard.test.ts +207 -0
- package/src/domain/platformclient/constants.ts +120 -0
- package/src/domain/platformclient/controller.ts +449 -0
- package/src/domain/platformclient/credentials.ts +68 -0
- package/src/domain/platformclient/mint.ts +412 -0
- package/src/domain/platformclient/origin-guard.ts +117 -0
- package/src/domain/platformclient/resource-store.ts +150 -0
- package/src/domain/platformclient/steps.ts +472 -0
- package/src/domain/platformclient/store-contract.ts +318 -0
- package/src/domain/platformclient/store.ts +63 -0
- package/src/domain/platformclient/system-managed.ts +115 -0
- package/src/domain/platformclient/token-controller.ts +49 -0
- package/src/domain/platformclient/verifier.ts +106 -0
- package/src/encryption/key-manager.ts +87 -44
- package/src/extensions/__tests__/extension-composition.test.ts +3 -2
- package/src/extensions/__tests__/platform-client-composed.test.ts +288 -0
- package/src/extensions/__tests__/registry.test.ts +4 -1
- package/src/extensions/__tests__/tier-truthfulness.test.ts +8 -0
- package/src/extensions/caller-guards.ts +8 -3
- package/src/extensions/drivers.ts +37 -0
- package/src/extensions/guest-token-minting.ts +32 -0
- package/src/extensions/identity.ts +6 -4
- package/src/extensions/registry.ts +77 -3
- package/src/index.ts +55 -0
- package/src/pipeline/interceptors/auth.ts +27 -0
- package/src/pipeline/steps/__tests__/references.test.ts +2 -0
- package/src/pipeline/steps/build-update-state.ts +5 -2
- package/src/platformtoken/__tests__/envelope.test.ts +277 -0
- package/src/platformtoken/__tests__/key-ring.test.ts +205 -0
- package/src/platformtoken/envelope.ts +276 -0
- package/src/platformtoken/key-ring.ts +266 -0
- package/src/runnerauth/__tests__/runner-subject-composed.test.ts +9 -3
|
@@ -0,0 +1,449 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PlatformClient controller — the command and query sides, served by open
|
|
3
|
+
* source in every edition (the token side is token-controller.ts). A
|
|
4
|
+
* PlatformClient is an organization's inbound credential pair: its backend
|
|
5
|
+
* presents client_id + client_secret to mint short-lived user tokens for
|
|
6
|
+
* its own product's users (mint.ts). The inbound counterpart to OAuthApp,
|
|
7
|
+
* whose controller this one follows chain for chain.
|
|
8
|
+
*
|
|
9
|
+
* Every chain opens with Authorize and runs over the PlatformClientStore
|
|
10
|
+
* port (store.ts), so the cloud serves these same chains over its own
|
|
11
|
+
* table. What the domain adds to the OAuthApp shape:
|
|
12
|
+
* - the credential steps: generated on create and shown once in
|
|
13
|
+
* PlatformClientCreateResponse, kept from the row on update, replaced
|
|
14
|
+
* by rotateSecret (the client_id is permanent);
|
|
15
|
+
* - `system-share-client` refused on create, and the platform's
|
|
16
|
+
* system-managed clients refused on update, delete and rotate;
|
|
17
|
+
* - the reference rule on `environment_refs` (NormalizeReferences, then
|
|
18
|
+
* ValidateReferences) on create and update, which every chain that
|
|
19
|
+
* stores a spec reference runs; it reads the environments from the
|
|
20
|
+
* resource Store, not from the port, because a driver may keep client
|
|
21
|
+
* rows in a table of its own while environments stay in the Store;
|
|
22
|
+
* - the stored hash cleared on EVERY response, deletes and rotations
|
|
23
|
+
* included — the model promises it is never returned
|
|
24
|
+
* (fga/model/iam/platform_client.fga), and no reader needs it;
|
|
25
|
+
* - getByReference loads, then authorizes the loaded client exactly as
|
|
26
|
+
* `get` would (AuthorizeResolvedTarget), and listByOrg narrows through
|
|
27
|
+
* the list read scope, so a member who may not `get` a client does not
|
|
28
|
+
* see it listed either: the model gives organization members no access
|
|
29
|
+
* to a credential.
|
|
30
|
+
*
|
|
31
|
+
* There is no apply and no updateVisibility: clients are created through
|
|
32
|
+
* the console or the API and are always organization-private.
|
|
33
|
+
*
|
|
34
|
+
* Proven by platformclient.conformance.test.ts (local targets and the
|
|
35
|
+
* cloud) and __tests__/controller.test.ts.
|
|
36
|
+
*/
|
|
37
|
+
import type { ConnectRouter, HandlerContext } from "@connectrpc/connect";
|
|
38
|
+
import { create } from "@bufbuild/protobuf";
|
|
39
|
+
|
|
40
|
+
import { ApiResourceKind } from "@stigmer/protos/ai/stigmer/commons/apiresource/apiresourcekind/api_resource_kind_pb";
|
|
41
|
+
import type {
|
|
42
|
+
ApiResourceDeleteInput,
|
|
43
|
+
ApiResourceId,
|
|
44
|
+
ApiResourceReference,
|
|
45
|
+
} from "@stigmer/protos/ai/stigmer/commons/apiresource/io_pb";
|
|
46
|
+
import { PlatformClientSchema } from "@stigmer/protos/ai/stigmer/iam/platformclient/v1/api_pb";
|
|
47
|
+
import type { PlatformClient } from "@stigmer/protos/ai/stigmer/iam/platformclient/v1/api_pb";
|
|
48
|
+
import { PlatformClientCommandController } from "@stigmer/protos/ai/stigmer/iam/platformclient/v1/command_pb";
|
|
49
|
+
import {
|
|
50
|
+
PlatformClientCreateResponseSchema,
|
|
51
|
+
PlatformClientsSchema,
|
|
52
|
+
} from "@stigmer/protos/ai/stigmer/iam/platformclient/v1/io_pb";
|
|
53
|
+
import type {
|
|
54
|
+
ListPlatformClientsByOrgInput,
|
|
55
|
+
PlatformClientCreateResponse,
|
|
56
|
+
PlatformClientId,
|
|
57
|
+
PlatformClients,
|
|
58
|
+
} from "@stigmer/protos/ai/stigmer/iam/platformclient/v1/io_pb";
|
|
59
|
+
import { PlatformClientQueryController } from "@stigmer/protos/ai/stigmer/iam/platformclient/v1/query_pb";
|
|
60
|
+
|
|
61
|
+
import type { Logger } from "../../boot/logger.js";
|
|
62
|
+
import type { Authorizer } from "../../extensions/authorizer.js";
|
|
63
|
+
import type { ListReadScope } from "../../extensions/list-read-scope.js";
|
|
64
|
+
import { restrictListByReadScope } from "../../extensions/list-read-scope.js";
|
|
65
|
+
import type { ResourceAuthorizationLifecycle } from "../../extensions/resource-authorization.js";
|
|
66
|
+
import { internalError } from "../../pipeline/errors.js";
|
|
67
|
+
import { apiResourceKindKey } from "../../pipeline/interceptors/apiresource.js";
|
|
68
|
+
import { callerIdentityOf } from "../../pipeline/interceptors/auth.js";
|
|
69
|
+
import { newPipeline } from "../../pipeline/pipeline.js";
|
|
70
|
+
import { RequestContext } from "../../pipeline/request-context.js";
|
|
71
|
+
import { newAuthorizeStep } from "../../pipeline/steps/authorize.js";
|
|
72
|
+
import {
|
|
73
|
+
loadedTargetAsMethod,
|
|
74
|
+
newAuthorizeResolvedTargetStep,
|
|
75
|
+
} from "../../pipeline/steps/authorize-resolved-target.js";
|
|
76
|
+
import {
|
|
77
|
+
newCleanupIamPoliciesStep,
|
|
78
|
+
newCreateAuthorizationTuplesStep,
|
|
79
|
+
} from "../../pipeline/steps/authorization-tuples.js";
|
|
80
|
+
import { newBuildUpdateStateStep } from "../../pipeline/steps/build-update-state.js";
|
|
81
|
+
import { newBuildNewStateStep } from "../../pipeline/steps/defaults.js";
|
|
82
|
+
import { RESOURCE_ID_KEY } from "../../pipeline/steps/delete.js";
|
|
83
|
+
import { newGuardReservedLabelsStep } from "../../pipeline/steps/guard-reserved-labels.js";
|
|
84
|
+
import { compareCreatedAtDesc } from "../../pipeline/steps/helpers.js";
|
|
85
|
+
import { EXISTING_RESOURCE_KEY } from "../../pipeline/steps/load-existing.js";
|
|
86
|
+
import { TARGET_RESOURCE_KEY } from "../../pipeline/steps/load-target.js";
|
|
87
|
+
import {
|
|
88
|
+
newNormalizeReferencesStep,
|
|
89
|
+
newValidateReferencesStep,
|
|
90
|
+
} from "../../pipeline/steps/references.js";
|
|
91
|
+
import { newResolveSlugStep } from "../../pipeline/steps/slug.js";
|
|
92
|
+
import { newValidateVisibilityStep } from "../../pipeline/steps/validate-visibility.js";
|
|
93
|
+
import { newValidateProtoStep } from "../../pipeline/steps/validation.js";
|
|
94
|
+
import type { Store } from "../../store/interface.js";
|
|
95
|
+
import {
|
|
96
|
+
newCheckDuplicateStep,
|
|
97
|
+
newDeleteClientStep,
|
|
98
|
+
newGenerateClientCredentialsStep,
|
|
99
|
+
newLoadClientByReferenceStep,
|
|
100
|
+
newLoadExistingClientForDeleteStep,
|
|
101
|
+
newLoadExistingClientStep,
|
|
102
|
+
newLoadTargetClientStep,
|
|
103
|
+
newPersistNewClientStep,
|
|
104
|
+
newPersistTargetClientStep,
|
|
105
|
+
newPersistUpdatedClientStep,
|
|
106
|
+
newPreserveClientCredentialsStep,
|
|
107
|
+
newRefuseReservedSlugStep,
|
|
108
|
+
newRefuseSystemManagedStep,
|
|
109
|
+
newRotateClientCredentialsStep,
|
|
110
|
+
parkedClientSecret,
|
|
111
|
+
redactPlatformClient,
|
|
112
|
+
storedClientOf,
|
|
113
|
+
} from "./steps.js";
|
|
114
|
+
import type { PlatformClientStore } from "./store.js";
|
|
115
|
+
|
|
116
|
+
export interface PlatformClientControllerDeps {
|
|
117
|
+
/** The composed store — a driver's, or the OSS adapter over the generic Store. */
|
|
118
|
+
readonly clients: PlatformClientStore;
|
|
119
|
+
/** The resource store the reference rule reads the environments `environment_refs` names from. */
|
|
120
|
+
readonly store: Store;
|
|
121
|
+
readonly logger: Logger;
|
|
122
|
+
/** The composed authorization seam — the Authorize step at position 1 of every chain calls it. */
|
|
123
|
+
readonly authorizer: Authorizer;
|
|
124
|
+
/** The composed tuple-lifecycle driver — undefined = the shared steps no-op. */
|
|
125
|
+
readonly authorizationLifecycle: ResourceAuthorizationLifecycle | undefined;
|
|
126
|
+
/** The composed list read scope — listByOrg narrows through it; undefined = every client of the org. */
|
|
127
|
+
readonly listReadScope: ListReadScope | undefined;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** Registers the command and query services on the router (routes stage). */
|
|
131
|
+
export function registerPlatformClientServices(
|
|
132
|
+
router: ConnectRouter,
|
|
133
|
+
deps: PlatformClientControllerDeps,
|
|
134
|
+
): void {
|
|
135
|
+
router.service(PlatformClientCommandController, {
|
|
136
|
+
create: (client, ctx) => createClient(deps, client, ctx),
|
|
137
|
+
update: (client, ctx) => updateClient(deps, client, ctx),
|
|
138
|
+
delete: (input, ctx) => deleteClient(deps, input, ctx),
|
|
139
|
+
rotateSecret: (id, ctx) => rotateSecret(deps, id, ctx),
|
|
140
|
+
});
|
|
141
|
+
router.service(PlatformClientQueryController, {
|
|
142
|
+
get: (id, ctx) => get(deps, id, ctx),
|
|
143
|
+
getByReference: (ref, ctx) => getByReference(deps, ref, ctx),
|
|
144
|
+
listByOrg: (input, ctx) => listByOrg(deps, input, ctx),
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function kindOf(ctx: HandlerContext): ApiResourceKind {
|
|
149
|
+
return ctx.values.get(apiResourceKindKey);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Create — OAuthApp's chain with the reserved-slug refusal after the slug
|
|
154
|
+
* resolves, the reference rule where every sibling chain runs it, and the
|
|
155
|
+
* credentials generated only once the row is known to be storable; the
|
|
156
|
+
* response carries the redacted client and the one look at its secret.
|
|
157
|
+
*/
|
|
158
|
+
async function createClient(
|
|
159
|
+
deps: PlatformClientControllerDeps,
|
|
160
|
+
client: PlatformClient,
|
|
161
|
+
ctx: HandlerContext,
|
|
162
|
+
): Promise<PlatformClientCreateResponse> {
|
|
163
|
+
const reqCtx = new RequestContext(
|
|
164
|
+
PlatformClientSchema,
|
|
165
|
+
client,
|
|
166
|
+
callerIdentityOf(ctx),
|
|
167
|
+
kindOf(ctx),
|
|
168
|
+
);
|
|
169
|
+
await newPipeline<typeof PlatformClientSchema>(
|
|
170
|
+
"platformclient-create",
|
|
171
|
+
deps.logger,
|
|
172
|
+
)
|
|
173
|
+
.addStep(
|
|
174
|
+
newAuthorizeStep(
|
|
175
|
+
PlatformClientCommandController.method.create,
|
|
176
|
+
deps.authorizer,
|
|
177
|
+
),
|
|
178
|
+
)
|
|
179
|
+
.addStep(newResolveSlugStep())
|
|
180
|
+
.addStep(newValidateProtoStep())
|
|
181
|
+
.addStep(newValidateVisibilityStep())
|
|
182
|
+
.addStep(newRefuseReservedSlugStep())
|
|
183
|
+
.addStep(newCheckDuplicateStep(deps.clients))
|
|
184
|
+
.addStep(newBuildNewStateStep())
|
|
185
|
+
.addStep(newGuardReservedLabelsStep(deps.authorizer))
|
|
186
|
+
.addStep(newNormalizeReferencesStep())
|
|
187
|
+
.addStep(newValidateReferencesStep(deps.store))
|
|
188
|
+
.addStep(newGenerateClientCredentialsStep())
|
|
189
|
+
.addStep(newPersistNewClientStep(deps.clients))
|
|
190
|
+
.addStep(
|
|
191
|
+
newCreateAuthorizationTuplesStep(
|
|
192
|
+
deps.authorizationLifecycle,
|
|
193
|
+
deps.logger,
|
|
194
|
+
),
|
|
195
|
+
)
|
|
196
|
+
.build()
|
|
197
|
+
.execute(reqCtx);
|
|
198
|
+
return create(PlatformClientCreateResponseSchema, {
|
|
199
|
+
platformClient: redactPlatformClient(reqCtx.newState),
|
|
200
|
+
clientSecret: parkedClientSecret(reqCtx),
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Update — full spec replacement from the request (BuildUpdateState),
|
|
206
|
+
* addressed by id or org-scoped slug; the platform's own clients refused
|
|
207
|
+
* before any state is built; the reference rule as on create; the
|
|
208
|
+
* credential fields kept from the row.
|
|
209
|
+
*/
|
|
210
|
+
async function updateClient(
|
|
211
|
+
deps: PlatformClientControllerDeps,
|
|
212
|
+
client: PlatformClient,
|
|
213
|
+
ctx: HandlerContext,
|
|
214
|
+
): Promise<PlatformClient> {
|
|
215
|
+
const reqCtx = new RequestContext(
|
|
216
|
+
PlatformClientSchema,
|
|
217
|
+
client,
|
|
218
|
+
callerIdentityOf(ctx),
|
|
219
|
+
kindOf(ctx),
|
|
220
|
+
);
|
|
221
|
+
await newPipeline<typeof PlatformClientSchema>(
|
|
222
|
+
"platformclient-update",
|
|
223
|
+
deps.logger,
|
|
224
|
+
)
|
|
225
|
+
.addStep(
|
|
226
|
+
newAuthorizeStep(
|
|
227
|
+
PlatformClientCommandController.method.update,
|
|
228
|
+
deps.authorizer,
|
|
229
|
+
),
|
|
230
|
+
)
|
|
231
|
+
.addStep(newValidateProtoStep())
|
|
232
|
+
.addStep(newResolveSlugStep())
|
|
233
|
+
.addStep(newLoadExistingClientStep(deps.clients))
|
|
234
|
+
.addStep(
|
|
235
|
+
newRefuseSystemManagedStep<typeof PlatformClientSchema>(
|
|
236
|
+
"updated",
|
|
237
|
+
EXISTING_RESOURCE_KEY,
|
|
238
|
+
),
|
|
239
|
+
)
|
|
240
|
+
.addStep(newBuildUpdateStateStep())
|
|
241
|
+
.addStep(newGuardReservedLabelsStep(deps.authorizer))
|
|
242
|
+
.addStep(newNormalizeReferencesStep())
|
|
243
|
+
.addStep(newValidateReferencesStep(deps.store))
|
|
244
|
+
.addStep(newPreserveClientCredentialsStep())
|
|
245
|
+
.addStep(newPersistUpdatedClientStep(deps.clients))
|
|
246
|
+
.build()
|
|
247
|
+
.execute(reqCtx);
|
|
248
|
+
return redactPlatformClient(reqCtx.newState);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Delete — the platform's own clients refused before the delete; the
|
|
253
|
+
* client's policies cleaned up after. The client's outstanding tokens stop
|
|
254
|
+
* at their next request (the verifier's liveness read); the end-user
|
|
255
|
+
* accounts it provisioned stay, because an account belongs to the
|
|
256
|
+
* organization, not to one of its clients.
|
|
257
|
+
*/
|
|
258
|
+
async function deleteClient(
|
|
259
|
+
deps: PlatformClientControllerDeps,
|
|
260
|
+
input: ApiResourceDeleteInput,
|
|
261
|
+
ctx: HandlerContext,
|
|
262
|
+
): Promise<PlatformClient> {
|
|
263
|
+
type DeleteInput = typeof PlatformClientCommandController.method.delete.input;
|
|
264
|
+
const reqCtx = new RequestContext(
|
|
265
|
+
PlatformClientCommandController.method.delete.input,
|
|
266
|
+
input,
|
|
267
|
+
callerIdentityOf(ctx),
|
|
268
|
+
kindOf(ctx),
|
|
269
|
+
);
|
|
270
|
+
// ApiResourceDeleteInput carries resource_id, not the `value` field
|
|
271
|
+
// ExtractResourceId reads (OAuthApp's pattern).
|
|
272
|
+
reqCtx.set(RESOURCE_ID_KEY, input.resourceId);
|
|
273
|
+
await newPipeline<DeleteInput>("platformclient-delete", deps.logger)
|
|
274
|
+
.addStep(
|
|
275
|
+
newAuthorizeStep(
|
|
276
|
+
PlatformClientCommandController.method.delete,
|
|
277
|
+
deps.authorizer,
|
|
278
|
+
),
|
|
279
|
+
)
|
|
280
|
+
.addStep(newValidateProtoStep())
|
|
281
|
+
.addStep(newLoadExistingClientForDeleteStep<DeleteInput>(deps.clients))
|
|
282
|
+
.addStep(
|
|
283
|
+
newRefuseSystemManagedStep<DeleteInput>("deleted", EXISTING_RESOURCE_KEY),
|
|
284
|
+
)
|
|
285
|
+
.addStep(newDeleteClientStep<DeleteInput>(deps.clients))
|
|
286
|
+
.addStep(
|
|
287
|
+
newCleanupIamPoliciesStep(deps.authorizationLifecycle, deps.logger),
|
|
288
|
+
)
|
|
289
|
+
.build()
|
|
290
|
+
.execute(reqCtx);
|
|
291
|
+
return redactPlatformClient(storedClientOf(reqCtx, EXISTING_RESOURCE_KEY));
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* RotateSecret — a new secret for the loaded client (the client_id stays),
|
|
296
|
+
* shown once in the response. Tokens already minted stay valid until they
|
|
297
|
+
* expire; deleting the client is what revokes them.
|
|
298
|
+
*/
|
|
299
|
+
async function rotateSecret(
|
|
300
|
+
deps: PlatformClientControllerDeps,
|
|
301
|
+
id: PlatformClientId,
|
|
302
|
+
ctx: HandlerContext,
|
|
303
|
+
): Promise<PlatformClientCreateResponse> {
|
|
304
|
+
type RotateInput =
|
|
305
|
+
typeof PlatformClientCommandController.method.rotateSecret.input;
|
|
306
|
+
const reqCtx = new RequestContext(
|
|
307
|
+
PlatformClientCommandController.method.rotateSecret.input,
|
|
308
|
+
id,
|
|
309
|
+
callerIdentityOf(ctx),
|
|
310
|
+
kindOf(ctx),
|
|
311
|
+
);
|
|
312
|
+
await newPipeline<RotateInput>("platformclient-rotate-secret", deps.logger)
|
|
313
|
+
.addStep(
|
|
314
|
+
newAuthorizeStep(
|
|
315
|
+
PlatformClientCommandController.method.rotateSecret,
|
|
316
|
+
deps.authorizer,
|
|
317
|
+
),
|
|
318
|
+
)
|
|
319
|
+
.addStep(newValidateProtoStep())
|
|
320
|
+
.addStep(newLoadTargetClientStep<RotateInput>(deps.clients))
|
|
321
|
+
.addStep(
|
|
322
|
+
newRefuseSystemManagedStep<RotateInput>("rotated", TARGET_RESOURCE_KEY),
|
|
323
|
+
)
|
|
324
|
+
.addStep(newRotateClientCredentialsStep<RotateInput>())
|
|
325
|
+
.addStep(newPersistTargetClientStep<RotateInput>(deps.clients))
|
|
326
|
+
.build()
|
|
327
|
+
.execute(reqCtx);
|
|
328
|
+
return create(PlatformClientCreateResponseSchema, {
|
|
329
|
+
platformClient: redactPlatformClient(
|
|
330
|
+
storedClientOf(reqCtx, TARGET_RESOURCE_KEY),
|
|
331
|
+
),
|
|
332
|
+
clientSecret: parkedClientSecret(reqCtx),
|
|
333
|
+
});
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
async function get(
|
|
337
|
+
deps: PlatformClientControllerDeps,
|
|
338
|
+
id: ApiResourceId,
|
|
339
|
+
ctx: HandlerContext,
|
|
340
|
+
): Promise<PlatformClient> {
|
|
341
|
+
type GetInput = typeof PlatformClientQueryController.method.get.input;
|
|
342
|
+
const reqCtx = new RequestContext(
|
|
343
|
+
PlatformClientQueryController.method.get.input,
|
|
344
|
+
id,
|
|
345
|
+
callerIdentityOf(ctx),
|
|
346
|
+
kindOf(ctx),
|
|
347
|
+
);
|
|
348
|
+
await newPipeline<GetInput>("platformclient-get", deps.logger)
|
|
349
|
+
.addStep(
|
|
350
|
+
newAuthorizeStep(
|
|
351
|
+
PlatformClientQueryController.method.get,
|
|
352
|
+
deps.authorizer,
|
|
353
|
+
),
|
|
354
|
+
)
|
|
355
|
+
.addStep(newValidateProtoStep())
|
|
356
|
+
.addStep(newLoadTargetClientStep<GetInput>(deps.clients))
|
|
357
|
+
.build()
|
|
358
|
+
.execute(reqCtx);
|
|
359
|
+
return redactPlatformClient(storedClientOf(reqCtx, TARGET_RESOURCE_KEY));
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
/** GetByReference — load by org/slug, then authorize the loaded client as `get` would. */
|
|
363
|
+
async function getByReference(
|
|
364
|
+
deps: PlatformClientControllerDeps,
|
|
365
|
+
ref: ApiResourceReference,
|
|
366
|
+
ctx: HandlerContext,
|
|
367
|
+
): Promise<PlatformClient> {
|
|
368
|
+
const reqCtx = new RequestContext(
|
|
369
|
+
PlatformClientQueryController.method.getByReference.input,
|
|
370
|
+
ref,
|
|
371
|
+
callerIdentityOf(ctx),
|
|
372
|
+
kindOf(ctx),
|
|
373
|
+
);
|
|
374
|
+
await newPipeline<
|
|
375
|
+
typeof PlatformClientQueryController.method.getByReference.input
|
|
376
|
+
>("platformclient-get-by-reference", deps.logger)
|
|
377
|
+
.addStep(
|
|
378
|
+
newAuthorizeStep(
|
|
379
|
+
PlatformClientQueryController.method.getByReference,
|
|
380
|
+
deps.authorizer,
|
|
381
|
+
),
|
|
382
|
+
)
|
|
383
|
+
.addStep(newValidateProtoStep())
|
|
384
|
+
.addStep(newLoadClientByReferenceStep(deps.clients))
|
|
385
|
+
.addStep(
|
|
386
|
+
newAuthorizeResolvedTargetStep(
|
|
387
|
+
deps.authorizer,
|
|
388
|
+
loadedTargetAsMethod(PlatformClientQueryController.method.get),
|
|
389
|
+
),
|
|
390
|
+
)
|
|
391
|
+
.build()
|
|
392
|
+
.execute(reqCtx);
|
|
393
|
+
return redactPlatformClient(storedClientOf(reqCtx, TARGET_RESOURCE_KEY));
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
/**
|
|
397
|
+
* ListByOrg — the organization's clients the caller may view, newest
|
|
398
|
+
* first, each redacted. The annotation admits anyone who can view the
|
|
399
|
+
* organization; the list read scope then keeps only the clients a `get`
|
|
400
|
+
* would return.
|
|
401
|
+
*/
|
|
402
|
+
async function listByOrg(
|
|
403
|
+
deps: PlatformClientControllerDeps,
|
|
404
|
+
input: ListPlatformClientsByOrgInput,
|
|
405
|
+
ctx: HandlerContext,
|
|
406
|
+
): Promise<PlatformClients> {
|
|
407
|
+
const caller = callerIdentityOf(ctx);
|
|
408
|
+
const reqCtx = new RequestContext(
|
|
409
|
+
PlatformClientQueryController.method.listByOrg.input,
|
|
410
|
+
input,
|
|
411
|
+
caller,
|
|
412
|
+
kindOf(ctx),
|
|
413
|
+
);
|
|
414
|
+
await newPipeline<
|
|
415
|
+
typeof PlatformClientQueryController.method.listByOrg.input
|
|
416
|
+
>("platformclient-list-by-org", deps.logger)
|
|
417
|
+
.addStep(
|
|
418
|
+
newAuthorizeStep(
|
|
419
|
+
PlatformClientQueryController.method.listByOrg,
|
|
420
|
+
deps.authorizer,
|
|
421
|
+
),
|
|
422
|
+
)
|
|
423
|
+
.addStep(newValidateProtoStep())
|
|
424
|
+
.build()
|
|
425
|
+
.execute(reqCtx);
|
|
426
|
+
|
|
427
|
+
let candidates: ReadonlyArray<PlatformClient>;
|
|
428
|
+
try {
|
|
429
|
+
candidates = await deps.clients.findByOrg(input.org);
|
|
430
|
+
} catch (error) {
|
|
431
|
+
throw internalError(error, "failed to list platform clients");
|
|
432
|
+
}
|
|
433
|
+
const visible = await restrictListByReadScope(
|
|
434
|
+
deps.listReadScope,
|
|
435
|
+
caller,
|
|
436
|
+
ApiResourceKind.platform_client,
|
|
437
|
+
candidates,
|
|
438
|
+
input.org,
|
|
439
|
+
);
|
|
440
|
+
const entries = [...visible]
|
|
441
|
+
.sort((a, b) =>
|
|
442
|
+
compareCreatedAtDesc(
|
|
443
|
+
a.status?.audit?.specAudit?.createdAt,
|
|
444
|
+
b.status?.audit?.specAudit?.createdAt,
|
|
445
|
+
),
|
|
446
|
+
)
|
|
447
|
+
.map(redactPlatformClient);
|
|
448
|
+
return create(PlatformClientsSchema, { entries });
|
|
449
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PlatformClient credential material — byte-compatible with every client
|
|
3
|
+
* Stigmer Cloud has issued since the Java service, because the rows (and
|
|
4
|
+
* the secrets integrators hold) move under this code unchanged:
|
|
5
|
+
*
|
|
6
|
+
* - client_id: "stgm_cid_" + 32 random bytes, base64url (43 characters).
|
|
7
|
+
* Public by design — safe in logs and client-side code — so its
|
|
8
|
+
* existence is no secret and the mint may answer an unknown id and a
|
|
9
|
+
* wrong secret with the same copy at different speeds.
|
|
10
|
+
* - client_secret: "stgm_cs_" + 48 random bytes, base64url (64
|
|
11
|
+
* characters): 384 bits, returned once and never stored.
|
|
12
|
+
* - the stored hash: SHA-256 over the whole prefixed secret, base64url
|
|
13
|
+
* (43 characters). A slow KDF defends low-entropy passwords, not
|
|
14
|
+
* 384-bit random tokens — ApiKey's precedent (domain/apikey/keymaterial.ts).
|
|
15
|
+
* - the fingerprint: the secret's last six characters, for display.
|
|
16
|
+
*
|
|
17
|
+
* The comparison hashes the presented secret and compares the two hashes in
|
|
18
|
+
* constant time, so neither the secret nor the stored hash leaks through
|
|
19
|
+
* timing.
|
|
20
|
+
*/
|
|
21
|
+
import { createHash, randomBytes, timingSafeEqual } from "node:crypto";
|
|
22
|
+
|
|
23
|
+
export const CLIENT_ID_PREFIX = "stgm_cid_";
|
|
24
|
+
export const CLIENT_SECRET_PREFIX = "stgm_cs_";
|
|
25
|
+
|
|
26
|
+
/** Random bytes behind a client_id. */
|
|
27
|
+
const CLIENT_ID_BYTES = 32;
|
|
28
|
+
|
|
29
|
+
/** Random bytes behind a client_secret: 384 bits. */
|
|
30
|
+
const CLIENT_SECRET_BYTES = 48;
|
|
31
|
+
|
|
32
|
+
/** Characters of the secret kept for display. */
|
|
33
|
+
export const SECRET_FINGERPRINT_LENGTH = 6;
|
|
34
|
+
|
|
35
|
+
export function generateClientId(): string {
|
|
36
|
+
return CLIENT_ID_PREFIX + randomBytes(CLIENT_ID_BYTES).toString("base64url");
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function generateClientSecret(): string {
|
|
40
|
+
return (
|
|
41
|
+
CLIENT_SECRET_PREFIX +
|
|
42
|
+
randomBytes(CLIENT_SECRET_BYTES).toString("base64url")
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** SHA-256 over the whole prefixed secret, base64url — the stored form. */
|
|
47
|
+
export function hashClientSecret(secret: string): string {
|
|
48
|
+
return createHash("sha256").update(secret, "utf8").digest("base64url");
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** The secret's last six characters (the whole secret when shorter). */
|
|
52
|
+
export function secretFingerprint(secret: string): string {
|
|
53
|
+
return secret.length <= SECRET_FINGERPRINT_LENGTH
|
|
54
|
+
? secret
|
|
55
|
+
: secret.slice(-SECRET_FINGERPRINT_LENGTH);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Whether `secret` hashes to `storedHash`, compared in constant time. */
|
|
59
|
+
export function secretMatchesHash(secret: string, storedHash: string): boolean {
|
|
60
|
+
if (storedHash === "") {
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
const presented = Buffer.from(hashClientSecret(secret), "utf8");
|
|
64
|
+
const stored = Buffer.from(storedHash, "utf8");
|
|
65
|
+
return (
|
|
66
|
+
presented.length === stored.length && timingSafeEqual(presented, stored)
|
|
67
|
+
);
|
|
68
|
+
}
|