@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,318 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The PlatformClientStore PORT-CONTRACT KIT: every behavior an
|
|
3
|
+
* implementation of store.ts must satisfy identically, as cases a driver's
|
|
4
|
+
* test iterates. Open source runs them over its own adapter
|
|
5
|
+
* (resource-store.ts) on sqlite and Postgres in
|
|
6
|
+
* __tests__/resource-store.test.ts; a composition runs the SAME cases over
|
|
7
|
+
* the store it registers as `drivers.platformClientStore` (the cloud's
|
|
8
|
+
* `cloud.iam_platform_client` store), so "the port holds" is one statement
|
|
9
|
+
* proven per driver, never restated per repository.
|
|
10
|
+
*
|
|
11
|
+
* Shape: declarations over the port-contract runner (store/port-contract.ts),
|
|
12
|
+
* which owns the fresh fixture per case, the cleanup, and the reason the
|
|
13
|
+
* kit returns cases instead of calling vitest's `describe`. The
|
|
14
|
+
* disconnected-store case matters most here: the verifier reads this port
|
|
15
|
+
* on every request a minted token bears, so an outage that read as "no
|
|
16
|
+
* client" would revoke every live token instead of failing loudly.
|
|
17
|
+
*
|
|
18
|
+
* The duplicate arms check `instanceof DuplicatePlatformClientError` on the
|
|
19
|
+
* exported class, because that is how the create chain's Persist step
|
|
20
|
+
* catches it: a driver throwing a lookalike fails here for the reason it
|
|
21
|
+
* would fail a create.
|
|
22
|
+
*
|
|
23
|
+
* Case names are the contract lines, in the port's words. The OSS test pins
|
|
24
|
+
* the full list, so a case cannot drop out of the kit unnoticed.
|
|
25
|
+
*/
|
|
26
|
+
import assert from "node:assert/strict";
|
|
27
|
+
|
|
28
|
+
import { create, equals } from "@bufbuild/protobuf";
|
|
29
|
+
|
|
30
|
+
import type { PlatformClient } from "@stigmer/protos/ai/stigmer/iam/platformclient/v1/api_pb";
|
|
31
|
+
import { PlatformClientSchema } from "@stigmer/protos/ai/stigmer/iam/platformclient/v1/api_pb";
|
|
32
|
+
|
|
33
|
+
import { portContractCases } from "../../store/port-contract.js";
|
|
34
|
+
import type {
|
|
35
|
+
PortContractCase,
|
|
36
|
+
PortContractDeclaration,
|
|
37
|
+
PortContractFixture,
|
|
38
|
+
} from "../../store/port-contract.js";
|
|
39
|
+
import { DuplicatePlatformClientError } from "./store.js";
|
|
40
|
+
import type { PlatformClientStore } from "./store.js";
|
|
41
|
+
|
|
42
|
+
/** The runner's fixture over this port — the name a driver's test and the barrel know it by. */
|
|
43
|
+
export type PlatformClientStoreContractFixture =
|
|
44
|
+
PortContractFixture<PlatformClientStore>;
|
|
45
|
+
|
|
46
|
+
export type PlatformClientStoreContractCase = PortContractCase;
|
|
47
|
+
|
|
48
|
+
/** A client as the domain writes it; each fixture names its own id, org, slug and client_id. */
|
|
49
|
+
function platformClient(fields: {
|
|
50
|
+
id: string;
|
|
51
|
+
org: string;
|
|
52
|
+
slug: string;
|
|
53
|
+
clientId: string;
|
|
54
|
+
allowedOrigins?: string[];
|
|
55
|
+
}): PlatformClient {
|
|
56
|
+
return create(PlatformClientSchema, {
|
|
57
|
+
apiVersion: "iam.stigmer.ai/v1",
|
|
58
|
+
kind: "PlatformClient",
|
|
59
|
+
metadata: {
|
|
60
|
+
id: fields.id,
|
|
61
|
+
name: fields.slug,
|
|
62
|
+
slug: fields.slug,
|
|
63
|
+
org: fields.org,
|
|
64
|
+
},
|
|
65
|
+
spec: {
|
|
66
|
+
clientId: fields.clientId,
|
|
67
|
+
clientSecretHash: `hash-of-${fields.clientId}`,
|
|
68
|
+
secretFingerprint: "abcdef",
|
|
69
|
+
autoProvisionAccounts: true,
|
|
70
|
+
allowedOrigins: fields.allowedOrigins ?? [],
|
|
71
|
+
},
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function assertSameClient(
|
|
76
|
+
actual: PlatformClient | undefined,
|
|
77
|
+
expected: PlatformClient,
|
|
78
|
+
message: string,
|
|
79
|
+
): void {
|
|
80
|
+
assert.ok(actual !== undefined, `${message}: no client was answered`);
|
|
81
|
+
assert.ok(equals(PlatformClientSchema, actual, expected), message);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async function assertDuplicate(
|
|
85
|
+
write: Promise<void>,
|
|
86
|
+
message: string,
|
|
87
|
+
): Promise<void> {
|
|
88
|
+
await assert.rejects(
|
|
89
|
+
write,
|
|
90
|
+
(error: unknown) => error instanceof DuplicatePlatformClientError,
|
|
91
|
+
message,
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const ACME_DASHBOARD = platformClient({
|
|
96
|
+
id: "pcl_contract_acme_dashboard",
|
|
97
|
+
org: "acme",
|
|
98
|
+
slug: "dashboard",
|
|
99
|
+
clientId: "stgm_cid_contract_acme_dashboard",
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
const CASES: ReadonlyArray<PortContractDeclaration<PlatformClientStore>> = [
|
|
103
|
+
[
|
|
104
|
+
"save then findById, findByClientId and findByOrgAndSlug answer the saved client",
|
|
105
|
+
async ({ store }) => {
|
|
106
|
+
await store.save(ACME_DASHBOARD);
|
|
107
|
+
assertSameClient(
|
|
108
|
+
await store.findById("pcl_contract_acme_dashboard"),
|
|
109
|
+
ACME_DASHBOARD,
|
|
110
|
+
"findById must answer the saved row",
|
|
111
|
+
);
|
|
112
|
+
assertSameClient(
|
|
113
|
+
await store.findByClientId("stgm_cid_contract_acme_dashboard"),
|
|
114
|
+
ACME_DASHBOARD,
|
|
115
|
+
"findByClientId must answer the saved row",
|
|
116
|
+
);
|
|
117
|
+
assertSameClient(
|
|
118
|
+
await store.findByOrgAndSlug("acme", "dashboard"),
|
|
119
|
+
ACME_DASHBOARD,
|
|
120
|
+
"findByOrgAndSlug must answer the saved row",
|
|
121
|
+
);
|
|
122
|
+
},
|
|
123
|
+
],
|
|
124
|
+
[
|
|
125
|
+
"lookups of a client that is not held answer undefined",
|
|
126
|
+
async ({ store }) => {
|
|
127
|
+
await store.save(ACME_DASHBOARD);
|
|
128
|
+
assert.equal(await store.findById("pcl_contract_nobody"), undefined);
|
|
129
|
+
assert.equal(
|
|
130
|
+
await store.findByClientId("stgm_cid_contract_nobody"),
|
|
131
|
+
undefined,
|
|
132
|
+
);
|
|
133
|
+
assert.equal(await store.findByOrgAndSlug("acme", "nobody"), undefined);
|
|
134
|
+
assert.equal(
|
|
135
|
+
await store.findByOrgAndSlug("globex", "dashboard"),
|
|
136
|
+
undefined,
|
|
137
|
+
"a slug is scoped to its organization: another org's lookup must miss",
|
|
138
|
+
);
|
|
139
|
+
},
|
|
140
|
+
],
|
|
141
|
+
[
|
|
142
|
+
"save under a held id raises DuplicatePlatformClientError",
|
|
143
|
+
async ({ store }) => {
|
|
144
|
+
await store.save(ACME_DASHBOARD);
|
|
145
|
+
await assertDuplicate(
|
|
146
|
+
store.save(
|
|
147
|
+
platformClient({
|
|
148
|
+
id: "pcl_contract_acme_dashboard",
|
|
149
|
+
org: "acme",
|
|
150
|
+
slug: "other",
|
|
151
|
+
clientId: "stgm_cid_contract_other",
|
|
152
|
+
}),
|
|
153
|
+
),
|
|
154
|
+
"a held id must be refused, never overwritten",
|
|
155
|
+
);
|
|
156
|
+
},
|
|
157
|
+
],
|
|
158
|
+
[
|
|
159
|
+
"save under a held (org, slug) raises DuplicatePlatformClientError",
|
|
160
|
+
async ({ store }) => {
|
|
161
|
+
await store.save(ACME_DASHBOARD);
|
|
162
|
+
await assertDuplicate(
|
|
163
|
+
store.save(
|
|
164
|
+
platformClient({
|
|
165
|
+
id: "pcl_contract_acme_second",
|
|
166
|
+
org: "acme",
|
|
167
|
+
slug: "dashboard",
|
|
168
|
+
clientId: "stgm_cid_contract_acme_second",
|
|
169
|
+
}),
|
|
170
|
+
),
|
|
171
|
+
"a held (org, slug) must be refused",
|
|
172
|
+
);
|
|
173
|
+
},
|
|
174
|
+
],
|
|
175
|
+
[
|
|
176
|
+
"save under a held client_id raises DuplicatePlatformClientError",
|
|
177
|
+
async ({ store }) => {
|
|
178
|
+
await store.save(ACME_DASHBOARD);
|
|
179
|
+
await assertDuplicate(
|
|
180
|
+
store.save(
|
|
181
|
+
platformClient({
|
|
182
|
+
id: "pcl_contract_acme_third",
|
|
183
|
+
org: "acme",
|
|
184
|
+
slug: "third",
|
|
185
|
+
clientId: "stgm_cid_contract_acme_dashboard",
|
|
186
|
+
}),
|
|
187
|
+
),
|
|
188
|
+
"a held client_id must be refused: the mint resolves a client by it",
|
|
189
|
+
);
|
|
190
|
+
},
|
|
191
|
+
],
|
|
192
|
+
[
|
|
193
|
+
"the same slug in another organization is a different client",
|
|
194
|
+
async ({ store }) => {
|
|
195
|
+
await store.save(ACME_DASHBOARD);
|
|
196
|
+
const globex = platformClient({
|
|
197
|
+
id: "pcl_contract_globex_dashboard",
|
|
198
|
+
org: "globex",
|
|
199
|
+
slug: "dashboard",
|
|
200
|
+
clientId: "stgm_cid_contract_globex_dashboard",
|
|
201
|
+
});
|
|
202
|
+
await store.save(globex);
|
|
203
|
+
assertSameClient(
|
|
204
|
+
await store.findByOrgAndSlug("globex", "dashboard"),
|
|
205
|
+
globex,
|
|
206
|
+
"each organization answers its own client for the shared slug",
|
|
207
|
+
);
|
|
208
|
+
},
|
|
209
|
+
],
|
|
210
|
+
[
|
|
211
|
+
"update replaces the row in place",
|
|
212
|
+
async ({ store }) => {
|
|
213
|
+
await store.save(ACME_DASHBOARD);
|
|
214
|
+
const edited = platformClient({
|
|
215
|
+
id: "pcl_contract_acme_dashboard",
|
|
216
|
+
org: "acme",
|
|
217
|
+
slug: "dashboard",
|
|
218
|
+
clientId: "stgm_cid_contract_acme_dashboard",
|
|
219
|
+
allowedOrigins: ["https://app.acme.example"],
|
|
220
|
+
});
|
|
221
|
+
await store.update(edited);
|
|
222
|
+
assertSameClient(
|
|
223
|
+
await store.findById("pcl_contract_acme_dashboard"),
|
|
224
|
+
edited,
|
|
225
|
+
"findById must answer the replaced row",
|
|
226
|
+
);
|
|
227
|
+
assertSameClient(
|
|
228
|
+
await store.findByClientId("stgm_cid_contract_acme_dashboard"),
|
|
229
|
+
edited,
|
|
230
|
+
"findByClientId must answer the replaced row",
|
|
231
|
+
);
|
|
232
|
+
},
|
|
233
|
+
],
|
|
234
|
+
[
|
|
235
|
+
"update of an unknown id writes nothing",
|
|
236
|
+
async ({ store }) => {
|
|
237
|
+
await store.update(ACME_DASHBOARD);
|
|
238
|
+
assert.equal(
|
|
239
|
+
await store.findById("pcl_contract_acme_dashboard"),
|
|
240
|
+
undefined,
|
|
241
|
+
"update must replace, never create",
|
|
242
|
+
);
|
|
243
|
+
},
|
|
244
|
+
],
|
|
245
|
+
[
|
|
246
|
+
"deleteById frees the id, the client_id and the slug; deleting an unknown id resolves",
|
|
247
|
+
async ({ store }) => {
|
|
248
|
+
await store.save(ACME_DASHBOARD);
|
|
249
|
+
await store.deleteById("pcl_contract_acme_dashboard");
|
|
250
|
+
assert.equal(
|
|
251
|
+
await store.findById("pcl_contract_acme_dashboard"),
|
|
252
|
+
undefined,
|
|
253
|
+
);
|
|
254
|
+
assert.equal(
|
|
255
|
+
await store.findByClientId("stgm_cid_contract_acme_dashboard"),
|
|
256
|
+
undefined,
|
|
257
|
+
"a deleted client's client_id must stop resolving: the mint and the verifier read it",
|
|
258
|
+
);
|
|
259
|
+
assert.equal(
|
|
260
|
+
await store.findByOrgAndSlug("acme", "dashboard"),
|
|
261
|
+
undefined,
|
|
262
|
+
);
|
|
263
|
+
await store.save(ACME_DASHBOARD);
|
|
264
|
+
await store.deleteById("pcl_contract_nobody");
|
|
265
|
+
},
|
|
266
|
+
],
|
|
267
|
+
[
|
|
268
|
+
"findByOrg answers every client of the organization and no other",
|
|
269
|
+
async ({ store }) => {
|
|
270
|
+
await store.save(ACME_DASHBOARD);
|
|
271
|
+
await store.save(
|
|
272
|
+
platformClient({
|
|
273
|
+
id: "pcl_contract_acme_mobile",
|
|
274
|
+
org: "acme",
|
|
275
|
+
slug: "mobile",
|
|
276
|
+
clientId: "stgm_cid_contract_acme_mobile",
|
|
277
|
+
}),
|
|
278
|
+
);
|
|
279
|
+
await store.save(
|
|
280
|
+
platformClient({
|
|
281
|
+
id: "pcl_contract_globex_dashboard",
|
|
282
|
+
org: "globex",
|
|
283
|
+
slug: "dashboard",
|
|
284
|
+
clientId: "stgm_cid_contract_globex_dashboard",
|
|
285
|
+
}),
|
|
286
|
+
);
|
|
287
|
+
const ids = (await store.findByOrg("acme"))
|
|
288
|
+
.map((client) => client.metadata?.id ?? "")
|
|
289
|
+
.sort();
|
|
290
|
+
assert.deepEqual(ids, [
|
|
291
|
+
"pcl_contract_acme_dashboard",
|
|
292
|
+
"pcl_contract_acme_mobile",
|
|
293
|
+
]);
|
|
294
|
+
assert.deepEqual(await store.findByOrg("initech"), []);
|
|
295
|
+
},
|
|
296
|
+
],
|
|
297
|
+
[
|
|
298
|
+
"a disconnected store is an infrastructure fault, never 'not found'",
|
|
299
|
+
async (fixture) => {
|
|
300
|
+
await fixture.disconnect();
|
|
301
|
+
await assert.rejects(
|
|
302
|
+
fixture.store.findById("pcl_contract_acme_dashboard"),
|
|
303
|
+
"an id read on a disconnected store must reject: on the verifier's path 'not found' revokes every token",
|
|
304
|
+
);
|
|
305
|
+
await assert.rejects(
|
|
306
|
+
fixture.store.findByClientId("stgm_cid_contract_acme_dashboard"),
|
|
307
|
+
"a client_id read on a disconnected store must reject, never read as undefined",
|
|
308
|
+
);
|
|
309
|
+
},
|
|
310
|
+
],
|
|
311
|
+
];
|
|
312
|
+
|
|
313
|
+
/** The port's cases over `makeFixture`, one fresh fixture per case. */
|
|
314
|
+
export function platformClientStoreContract(
|
|
315
|
+
makeFixture: () => Promise<PlatformClientStoreContractFixture>,
|
|
316
|
+
): ReadonlyArray<PlatformClientStoreContractCase> {
|
|
317
|
+
return portContractCases(CASES, makeFixture);
|
|
318
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The PlatformClientStore PORT: the storage contract the platform-client
|
|
3
|
+
* domain writes and reads through, so the cloud serves the same chains
|
|
4
|
+
* over its own `cloud.iam_platform_client` table by registering an
|
|
5
|
+
* implementation as `drivers.platformClientStore` (the identity-account
|
|
6
|
+
* and IamPolicy precedent: a port for an IAM kind whose rows an edition
|
|
7
|
+
* keeps in its own table). Open source's implementation is
|
|
8
|
+
* resource-store.ts over the generic Store.
|
|
9
|
+
*
|
|
10
|
+
* The methods are exactly the lookups the domain asks: by id (every
|
|
11
|
+
* chain, and the verifier's liveness read), by client_id (the mint), by
|
|
12
|
+
* org-scoped slug (getByReference, update addressed by slug, the
|
|
13
|
+
* duplicate check), by organization (listByOrg). A port carries nothing
|
|
14
|
+
* only one edition calls.
|
|
15
|
+
*
|
|
16
|
+
* Contract every implementation must satisfy — proven by the port-contract
|
|
17
|
+
* kit (store-contract.ts, exported), which the OSS adapter's test iterates
|
|
18
|
+
* on both drivers and a composition's driver test iterates too:
|
|
19
|
+
* - `save` is create-only in effect: a held id, a held (org, slug) or a
|
|
20
|
+
* held client_id raises DuplicatePlatformClientError, never a silent
|
|
21
|
+
* overwrite;
|
|
22
|
+
* - `update` replaces by id and never creates: an unknown id writes
|
|
23
|
+
* nothing;
|
|
24
|
+
* - `deleteById` of an unknown id resolves;
|
|
25
|
+
* - `findByOrg` answers every client of the organization and no other,
|
|
26
|
+
* in no promised order (the chain sorts);
|
|
27
|
+
* - a typed not-found reads as `undefined`; any other storage failure
|
|
28
|
+
* propagates as the infrastructure fault it is (the ratified
|
|
29
|
+
* store-fault mapping — an outage must never read as "no client", which
|
|
30
|
+
* on the verifier's path would revoke every live token).
|
|
31
|
+
*/
|
|
32
|
+
import type { PlatformClient } from "@stigmer/protos/ai/stigmer/iam/platformclient/v1/api_pb";
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Raised by `save` when the id, the (org, slug) pair or the client_id is
|
|
36
|
+
* already held, so the create chain answers ALREADY_EXISTS. `cause`
|
|
37
|
+
* carries a driver's own error (the cloud's SQLSTATE 23505) for the logs;
|
|
38
|
+
* the message is the port's.
|
|
39
|
+
*/
|
|
40
|
+
export class DuplicatePlatformClientError extends Error {
|
|
41
|
+
constructor(message: string, options?: { cause?: unknown }) {
|
|
42
|
+
super(message, options);
|
|
43
|
+
this.name = "DuplicatePlatformClientError";
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface PlatformClientStore {
|
|
48
|
+
/** Insert; a held id, (org, slug) or client_id raises DuplicatePlatformClientError. */
|
|
49
|
+
save(client: PlatformClient): Promise<void>;
|
|
50
|
+
/** Full-row replace by id; an unknown id writes nothing. */
|
|
51
|
+
update(client: PlatformClient): Promise<void>;
|
|
52
|
+
/** Removes the row; no error when it does not exist. */
|
|
53
|
+
deleteById(id: string): Promise<void>;
|
|
54
|
+
findById(id: string): Promise<PlatformClient | undefined>;
|
|
55
|
+
/** The client holding this public identifier — the mint's lookup. */
|
|
56
|
+
findByClientId(clientId: string): Promise<PlatformClient | undefined>;
|
|
57
|
+
findByOrgAndSlug(
|
|
58
|
+
org: string,
|
|
59
|
+
slug: string,
|
|
60
|
+
): Promise<PlatformClient | undefined>;
|
|
61
|
+
/** Every client of the organization, in no promised order. */
|
|
62
|
+
findByOrg(org: string): Promise<ReadonlyArray<PlatformClient>>;
|
|
63
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The one builder of a system-managed PlatformClient: the row an edition
|
|
3
|
+
* creates for itself, never through the create chain, so the tokens it
|
|
4
|
+
* signs on the platform's own behalf (a guest's, a schedule fire's) name a
|
|
5
|
+
* client that exists. Stigmer Cloud keeps one per organization under
|
|
6
|
+
* SYSTEM_SHARE_CLIENT_SLUG and persists it through its store driver.
|
|
7
|
+
*
|
|
8
|
+
* The domain already owns what such a client is — the create chain refuses
|
|
9
|
+
* its reserved slug (RefuseReservedSlug) and every mutation refuses its
|
|
10
|
+
* label (RefuseSystemManaged) — so the row is built here too, and the
|
|
11
|
+
* rules that make it safe hold by construction:
|
|
12
|
+
* - it lives only under a reserved slug, which no user can create, so a
|
|
13
|
+
* user client can never be mistaken for one;
|
|
14
|
+
* - it carries the reserved `stigmer.ai/system-managed` label, so update,
|
|
15
|
+
* delete and rotate refuse it;
|
|
16
|
+
* - its credentials are real (a `client_id` unique like any other, a
|
|
17
|
+
* hash, a fingerprint), but the secret is discarded here: nobody ever
|
|
18
|
+
* authenticates as the platform's own client;
|
|
19
|
+
* - its id is spelled like every other (`pcl_` + ULID, generateId);
|
|
20
|
+
* - its audit names no actor, because no caller made it: the platform did.
|
|
21
|
+
*
|
|
22
|
+
* Who decides when one exists, and where it is stored, is the edition's:
|
|
23
|
+
* this function writes nothing. It writes no authorization tuples either —
|
|
24
|
+
* nothing authorizes against a system-managed client, and with none it
|
|
25
|
+
* stays out of every list and reference read a person makes.
|
|
26
|
+
*/
|
|
27
|
+
import { create } from "@bufbuild/protobuf";
|
|
28
|
+
import { timestampFromDate } from "@bufbuild/protobuf/wkt";
|
|
29
|
+
|
|
30
|
+
import { ApiResourceKind } from "@stigmer/protos/ai/stigmer/commons/apiresource/apiresourcekind/api_resource_kind_pb";
|
|
31
|
+
import {
|
|
32
|
+
ApiResourceAuditInfoSchema,
|
|
33
|
+
ApiResourceAuditSchema,
|
|
34
|
+
} from "@stigmer/protos/ai/stigmer/commons/apiresource/status_pb";
|
|
35
|
+
import { PlatformClientSchema } from "@stigmer/protos/ai/stigmer/iam/platformclient/v1/api_pb";
|
|
36
|
+
import type { PlatformClient } from "@stigmer/protos/ai/stigmer/iam/platformclient/v1/api_pb";
|
|
37
|
+
|
|
38
|
+
import {
|
|
39
|
+
RESERVED_LABEL_TRUE,
|
|
40
|
+
SYSTEM_MANAGED_LABEL,
|
|
41
|
+
} from "../../pipeline/apiresource-labels.js";
|
|
42
|
+
import {
|
|
43
|
+
defaultVisibilityFor,
|
|
44
|
+
getIdPrefix,
|
|
45
|
+
} from "../../pipeline/apiresource-meta.js";
|
|
46
|
+
import { generateId } from "../../pipeline/steps/defaults.js";
|
|
47
|
+
import {
|
|
48
|
+
PLATFORM_CLIENT_KIND_NAME,
|
|
49
|
+
RESERVED_PLATFORM_CLIENT_SLUGS,
|
|
50
|
+
} from "./constants.js";
|
|
51
|
+
import {
|
|
52
|
+
generateClientId,
|
|
53
|
+
generateClientSecret,
|
|
54
|
+
hashClientSecret,
|
|
55
|
+
secretFingerprint,
|
|
56
|
+
} from "./credentials.js";
|
|
57
|
+
|
|
58
|
+
export interface SystemManagedPlatformClientInput {
|
|
59
|
+
/** The owning organization. */
|
|
60
|
+
readonly org: string;
|
|
61
|
+
/** A reserved slug (RESERVED_PLATFORM_CLIENT_SLUGS); anything else throws. */
|
|
62
|
+
readonly slug: string;
|
|
63
|
+
/** The display name a console would show. */
|
|
64
|
+
readonly name: string;
|
|
65
|
+
/** The creation instant stamped on both audit slots; defaults to the wall clock. */
|
|
66
|
+
readonly now?: Date;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* A new system-managed client, ready for a store's `save`. Throws on an
|
|
71
|
+
* empty organization or a slug that is not reserved — an edition's
|
|
72
|
+
* programming error, never a request's.
|
|
73
|
+
*/
|
|
74
|
+
export function newSystemManagedPlatformClient(
|
|
75
|
+
input: SystemManagedPlatformClientInput,
|
|
76
|
+
): PlatformClient {
|
|
77
|
+
if (input.org === "") {
|
|
78
|
+
throw new Error("a system-managed platform client needs an organization");
|
|
79
|
+
}
|
|
80
|
+
if (!RESERVED_PLATFORM_CLIENT_SLUGS.has(input.slug)) {
|
|
81
|
+
throw new Error(
|
|
82
|
+
`'${input.slug}' is not a reserved platform-client slug; a system-managed client lives only under one (${[...RESERVED_PLATFORM_CLIENT_SLUGS].join(", ")})`,
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
const secret = generateClientSecret();
|
|
86
|
+
const created = timestampFromDate(input.now ?? new Date());
|
|
87
|
+
const audit = create(ApiResourceAuditInfoSchema, {
|
|
88
|
+
createdAt: created,
|
|
89
|
+
updatedAt: created,
|
|
90
|
+
event: "created",
|
|
91
|
+
});
|
|
92
|
+
return create(PlatformClientSchema, {
|
|
93
|
+
apiVersion: "iam.stigmer.ai/v1",
|
|
94
|
+
kind: PLATFORM_CLIENT_KIND_NAME,
|
|
95
|
+
metadata: {
|
|
96
|
+
id: generateId(getIdPrefix(ApiResourceKind.platform_client)),
|
|
97
|
+
org: input.org,
|
|
98
|
+
name: input.name,
|
|
99
|
+
slug: input.slug,
|
|
100
|
+
labels: { [SYSTEM_MANAGED_LABEL]: RESERVED_LABEL_TRUE },
|
|
101
|
+
visibility: defaultVisibilityFor(ApiResourceKind.platform_client),
|
|
102
|
+
},
|
|
103
|
+
spec: {
|
|
104
|
+
clientId: generateClientId(),
|
|
105
|
+
clientSecretHash: hashClientSecret(secret),
|
|
106
|
+
secretFingerprint: secretFingerprint(secret),
|
|
107
|
+
},
|
|
108
|
+
status: {
|
|
109
|
+
audit: create(ApiResourceAuditSchema, {
|
|
110
|
+
specAudit: audit,
|
|
111
|
+
statusAudit: audit,
|
|
112
|
+
}),
|
|
113
|
+
},
|
|
114
|
+
});
|
|
115
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PlatformClientTokenController — the two public token RPCs, registered in
|
|
3
|
+
* every composition so the service has one owner (the routes stage refuses
|
|
4
|
+
* an extension that registers it again, boot/route-shadowing.ts):
|
|
5
|
+
*
|
|
6
|
+
* - mintUserToken: the PlatformClient mint (mint.ts). Public on the wire
|
|
7
|
+
* — the client authenticates with its credentials in the body, not a
|
|
8
|
+
* bearer token — so there is no annotation to evaluate; the mint never
|
|
9
|
+
* reads the chain's caller.
|
|
10
|
+
* - mintGuestToken: the credential-free mint for anonymous visitors of a
|
|
11
|
+
* shared agent's hosted page, dispatched to the composed guest-token
|
|
12
|
+
* capability (extensions/guest-token-minting.ts). Without one — open
|
|
13
|
+
* source hosts no shared-agent pages — it answers UNIMPLEMENTED with
|
|
14
|
+
* the edition sentence, the identityFederation absent shape.
|
|
15
|
+
*/
|
|
16
|
+
import { Code, ConnectError } from "@connectrpc/connect";
|
|
17
|
+
import type { ConnectRouter } from "@connectrpc/connect";
|
|
18
|
+
|
|
19
|
+
import { PlatformClientTokenController } from "@stigmer/protos/ai/stigmer/iam/platformclient/v1/token_pb";
|
|
20
|
+
|
|
21
|
+
import type { GuestTokenMinting } from "../../extensions/guest-token-minting.js";
|
|
22
|
+
import { GUEST_MINT_UNIMPLEMENTED_MESSAGE } from "./constants.js";
|
|
23
|
+
import { mintUserToken } from "./mint.js";
|
|
24
|
+
import type { PlatformClientMintDeps } from "./mint.js";
|
|
25
|
+
|
|
26
|
+
export interface PlatformClientTokenControllerDeps {
|
|
27
|
+
readonly mint: PlatformClientMintDeps;
|
|
28
|
+
/** The composed capability; undefined = mintGuestToken refuses UNIMPLEMENTED. */
|
|
29
|
+
readonly guestTokenMinting: GuestTokenMinting | undefined;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Registers the token service on the router (routes stage). */
|
|
33
|
+
export function registerPlatformClientTokenService(
|
|
34
|
+
router: ConnectRouter,
|
|
35
|
+
deps: PlatformClientTokenControllerDeps,
|
|
36
|
+
): void {
|
|
37
|
+
router.service(PlatformClientTokenController, {
|
|
38
|
+
mintUserToken: (request) => mintUserToken(deps.mint, request),
|
|
39
|
+
mintGuestToken: (request, ctx) => {
|
|
40
|
+
if (deps.guestTokenMinting === undefined) {
|
|
41
|
+
throw new ConnectError(
|
|
42
|
+
GUEST_MINT_UNIMPLEMENTED_MESSAGE,
|
|
43
|
+
Code.Unimplemented,
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
return deps.guestTokenMinting.mintGuestToken(request, ctx.requestHeader);
|
|
47
|
+
},
|
|
48
|
+
});
|
|
49
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The PlatformClient user-token verifier: open source's one platform-token
|
|
3
|
+
* lane. It claims a token with `iss: "stigmer"` and NO `token_type` — the
|
|
4
|
+
* user token the mint signs — and passes every other token on:
|
|
5
|
+
* - a token that is not a platform token is another verifier's (the
|
|
6
|
+
* OIDC verifier's, an API key's);
|
|
7
|
+
* - a typed platform token (guest, schedule, channel, the sandbox
|
|
8
|
+
* family) belongs to the edition that mints it; open source verifies
|
|
9
|
+
* only its own vocabulary, and on a server with no verifier for that
|
|
10
|
+
* lane the chain refuses it as unclaimed.
|
|
11
|
+
* A platform token whose signature, expiry, audience or subject fails is
|
|
12
|
+
* refused here with the envelope's pinned copy, whichever lane it names —
|
|
13
|
+
* the refusal the cloud's verifier has always answered for it.
|
|
14
|
+
*
|
|
15
|
+
* Liveness is the verifier's (not a guard's), so every lane that
|
|
16
|
+
* authenticates — the serving chain and the extension HTTP edges that run
|
|
17
|
+
* the verifier chain without guards — refuses a deleted client's tokens on
|
|
18
|
+
* the next request, with the cloud's copy. A store fault propagates as the
|
|
19
|
+
* infrastructure fault it is, never as a revocation.
|
|
20
|
+
*
|
|
21
|
+
* Composed between `apikey` and `oidc`, only under an authentication
|
|
22
|
+
* posture (compose.ts): the OIDC verifier claims every JWT-shaped token and
|
|
23
|
+
* throws on one it cannot verify, so a platform token must be claimed
|
|
24
|
+
* before it gets there (stigmer#1137's lesson).
|
|
25
|
+
*
|
|
26
|
+
* The identity is the account the token was minted for: `sub` IS the
|
|
27
|
+
* account id (the mint signs it), so no account read is needed; class
|
|
28
|
+
* `user`, issuer "stigmer", the token carried (the memory-capture gate and
|
|
29
|
+
* the origin guard read its `platform_client_id`), and the email and name
|
|
30
|
+
* the platform asserted, for display only.
|
|
31
|
+
*/
|
|
32
|
+
import { Code, ConnectError } from "@connectrpc/connect";
|
|
33
|
+
|
|
34
|
+
import type {
|
|
35
|
+
CallerIdentity,
|
|
36
|
+
IdentityVerifier,
|
|
37
|
+
} from "../../extensions/identity.js";
|
|
38
|
+
import {
|
|
39
|
+
PLATFORM_TOKEN_ISSUER,
|
|
40
|
+
platformTokenRefusalError,
|
|
41
|
+
stringClaim,
|
|
42
|
+
verifyPlatformToken,
|
|
43
|
+
} from "../../platformtoken/envelope.js";
|
|
44
|
+
import type { PlatformTokenKeyRing } from "../../platformtoken/key-ring.js";
|
|
45
|
+
import {
|
|
46
|
+
DELETED_CLIENT_MESSAGE,
|
|
47
|
+
TOKEN_NAMES_NO_CLIENT_MESSAGE,
|
|
48
|
+
USER_TOKEN_CLAIMS,
|
|
49
|
+
} from "./constants.js";
|
|
50
|
+
import type { PlatformClientStore } from "./store.js";
|
|
51
|
+
|
|
52
|
+
export interface PlatformClientTokenVerifierDeps {
|
|
53
|
+
readonly keys: PlatformTokenKeyRing;
|
|
54
|
+
readonly clients: Pick<PlatformClientStore, "findById">;
|
|
55
|
+
readonly now?: () => Date;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function newPlatformClientTokenVerifier(
|
|
59
|
+
deps: PlatformClientTokenVerifierDeps,
|
|
60
|
+
): IdentityVerifier {
|
|
61
|
+
return {
|
|
62
|
+
name: "platform-client",
|
|
63
|
+
async verify(token: string): Promise<CallerIdentity | null> {
|
|
64
|
+
const result = verifyPlatformToken(deps.keys, token, deps.now?.());
|
|
65
|
+
switch (result.outcome) {
|
|
66
|
+
case "foreign":
|
|
67
|
+
return null;
|
|
68
|
+
case "refused":
|
|
69
|
+
throw platformTokenRefusalError(result.refusal);
|
|
70
|
+
case "verified":
|
|
71
|
+
break;
|
|
72
|
+
default: {
|
|
73
|
+
const exhaustive: never = result;
|
|
74
|
+
throw new Error(`unknown verification ${JSON.stringify(exhaustive)}`);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
const verified = result.token;
|
|
78
|
+
if (verified.tokenType !== undefined) {
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
const platformClientId = stringClaim(
|
|
82
|
+
verified.payload,
|
|
83
|
+
USER_TOKEN_CLAIMS.platformClientId,
|
|
84
|
+
);
|
|
85
|
+
if (platformClientId === undefined) {
|
|
86
|
+
throw new ConnectError(
|
|
87
|
+
TOKEN_NAMES_NO_CLIENT_MESSAGE,
|
|
88
|
+
Code.Unauthenticated,
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
if ((await deps.clients.findById(platformClientId)) === undefined) {
|
|
92
|
+
throw new ConnectError(DELETED_CLIENT_MESSAGE, Code.Unauthenticated);
|
|
93
|
+
}
|
|
94
|
+
const email = stringClaim(verified.payload, USER_TOKEN_CLAIMS.email);
|
|
95
|
+
const displayName = stringClaim(verified.payload, USER_TOKEN_CLAIMS.name);
|
|
96
|
+
return {
|
|
97
|
+
identityId: verified.subject,
|
|
98
|
+
callerClass: "user",
|
|
99
|
+
issuer: PLATFORM_TOKEN_ISSUER,
|
|
100
|
+
rawToken: token,
|
|
101
|
+
...(email !== undefined ? { email } : {}),
|
|
102
|
+
...(displayName !== undefined ? { displayName } : {}),
|
|
103
|
+
};
|
|
104
|
+
},
|
|
105
|
+
};
|
|
106
|
+
}
|