@amerged/ohmyhost-mcp 0.1.10
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/LICENSE +202 -0
- package/NOTICE +4 -0
- package/README.md +11 -0
- package/THIRD_PARTY_NOTICES.md +474 -0
- package/dist/chunk-7MV2EVXV.js +30779 -0
- package/dist/index.js +9 -0
- package/dist/stdio-main.js +5607 -0
- package/package.json +32 -0
- package/src/apps/mcp/index.ts +30 -0
- package/src/apps/mcp/local-client.ts +891 -0
- package/src/apps/mcp/local-server.ts +1512 -0
- package/src/apps/mcp/stdio-main.ts +26 -0
- package/src/apps/product-cli/bin.ts +24 -0
- package/src/apps/product-cli/cli.ts +2271 -0
- package/src/apps/product-cli/command.ts +1811 -0
- package/src/apps/product-cli/composition.ts +823 -0
- package/src/apps/product-cli/credential-store.ts +149 -0
- package/src/apps/product-cli/device-token-expiry.ts +89 -0
- package/src/apps/product-cli/file-credential-store.ts +166 -0
- package/src/apps/product-cli/index.ts +23 -0
- package/src/apps/product-cli/product-api.ts +1082 -0
- package/src/apps/product-cli/project-link-store.ts +30 -0
- package/src/apps/product-cli/psql-session.ts +29 -0
- package/src/apps/product-cli/repository-init.ts +1622 -0
- package/src/apps/product-cli/runtime-contract.ts +1747 -0
- package/src/apps/product-cli/user-token-file.ts +214 -0
- package/src/apps/product-cli/workspace.ts +92 -0
- package/src/packages/agent-skills/generated-skill-resources.ts +147 -0
- package/src/packages/agent-skills/index.ts +37 -0
- package/src/packages/contracts/application-root.ts +80 -0
- package/src/packages/contracts/billing.ts +172 -0
- package/src/packages/contracts/credit-pricing.ts +214 -0
- package/src/packages/contracts/database-access.ts +257 -0
- package/src/packages/contracts/database-compute.ts +72 -0
- package/src/packages/contracts/database-migration-admission.ts +235 -0
- package/src/packages/contracts/feedback.ts +96 -0
- package/src/packages/contracts/framework-admission.ts +280 -0
- package/src/packages/contracts/framework-family.ts +18 -0
- package/src/packages/contracts/functions.ts +76 -0
- package/src/packages/contracts/index.ts +245 -0
- package/src/packages/contracts/operation-deployment-result.ts +165 -0
- package/src/packages/contracts/operation-failure.ts +150 -0
- package/src/packages/contracts/platform-origins.ts +5 -0
- package/src/packages/contracts/project-context.ts +59 -0
- package/src/packages/contracts/project-exports.ts +102 -0
- package/src/packages/contracts/user-api-keys.ts +98 -0
- package/src/packages/sdk-ts/generated/client/client.gen.ts +277 -0
- package/src/packages/sdk-ts/generated/client/index.ts +27 -0
- package/src/packages/sdk-ts/generated/client/types.gen.ts +217 -0
- package/src/packages/sdk-ts/generated/client/utils.gen.ts +316 -0
- package/src/packages/sdk-ts/generated/client.gen.ts +24 -0
- package/src/packages/sdk-ts/generated/core/auth.gen.ts +48 -0
- package/src/packages/sdk-ts/generated/core/bodySerializer.gen.ts +82 -0
- package/src/packages/sdk-ts/generated/core/params.gen.ts +178 -0
- package/src/packages/sdk-ts/generated/core/pathSerializer.gen.ts +171 -0
- package/src/packages/sdk-ts/generated/core/queryKeySerializer.gen.ts +117 -0
- package/src/packages/sdk-ts/generated/core/serverSentEvents.gen.ts +242 -0
- package/src/packages/sdk-ts/generated/core/types.gen.ts +114 -0
- package/src/packages/sdk-ts/generated/core/utils.gen.ts +140 -0
- package/src/packages/sdk-ts/generated/index.ts +578 -0
- package/src/packages/sdk-ts/generated/sdk.gen.ts +3440 -0
- package/src/packages/sdk-ts/generated/types.gen.ts +5790 -0
- package/src/packages/sdk-ts/index.ts +7 -0
- package/src/packages/workos-auth-contracts/device-flow.ts +271 -0
- package/src/packages/workos-auth-contracts/index.ts +8 -0
- package/src/packages/workos-auth-contracts/management-rpc.ts +379 -0
- package/src/packages/workos-auth-contracts/organizations.ts +113 -0
- package/src/packages/workos-auth-contracts/parsing.ts +75 -0
- package/src/packages/workos-auth-contracts/session-status.ts +53 -0
- package/src/packages/workos-auth-contracts/tokens.ts +216 -0
- package/src/packages/workos-auth-contracts/user-api-keys.ts +85 -0
- package/src/packages/workos-auth-contracts/webhooks.ts +127 -0
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
import {
|
|
2
|
+
expectPositiveInteger,
|
|
3
|
+
expectRecord,
|
|
4
|
+
expectString,
|
|
5
|
+
parseJsonObject,
|
|
6
|
+
WorkosContractError,
|
|
7
|
+
} from "./parsing.js";
|
|
8
|
+
|
|
9
|
+
export interface WorkosPublicHttpRequest {
|
|
10
|
+
readonly url: string;
|
|
11
|
+
readonly method: "POST";
|
|
12
|
+
readonly headers: Readonly<Record<string, string>>;
|
|
13
|
+
readonly body: string;
|
|
14
|
+
readonly redirect: "error";
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface WorkosPublicHttpResponse {
|
|
18
|
+
readonly status: number;
|
|
19
|
+
readonly body: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface WorkosPublicTransport {
|
|
23
|
+
send(request: WorkosPublicHttpRequest): Promise<WorkosPublicHttpResponse>;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface WorkosClock {
|
|
27
|
+
now(): Date;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface DeviceAuthorization {
|
|
31
|
+
readonly deviceCode: string;
|
|
32
|
+
readonly userCode: string;
|
|
33
|
+
readonly verificationUri: string;
|
|
34
|
+
readonly verificationUriComplete?: string;
|
|
35
|
+
readonly expiresInSeconds: number;
|
|
36
|
+
readonly intervalSeconds: number;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface DeviceTokens {
|
|
40
|
+
readonly accessToken: string;
|
|
41
|
+
readonly refreshToken: string;
|
|
42
|
+
readonly tokenType: string;
|
|
43
|
+
readonly expiresInSeconds?: number;
|
|
44
|
+
readonly organizationId?: string;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface WorkosDeviceFlowTokens extends DeviceTokens {
|
|
48
|
+
readonly provenance: {
|
|
49
|
+
readonly kind: "workos_authkit_device_flow";
|
|
50
|
+
readonly clientId: string;
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export type DevicePollResult =
|
|
55
|
+
| { readonly state: "pending"; readonly nextPollAt: Date }
|
|
56
|
+
| { readonly state: "complete"; readonly tokens: WorkosDeviceFlowTokens };
|
|
57
|
+
|
|
58
|
+
export const WORKOS_AUTHKIT_DEVICE_AUTHORIZATION_ENDPOINT =
|
|
59
|
+
"https://api.workos.com/user_management/authorize/device";
|
|
60
|
+
export const WORKOS_AUTHKIT_DEVICE_TOKEN_ENDPOINT =
|
|
61
|
+
"https://api.workos.com/user_management/authenticate";
|
|
62
|
+
|
|
63
|
+
const allowedDeviceTokenFields = new Set([
|
|
64
|
+
"access_token",
|
|
65
|
+
"refresh_token",
|
|
66
|
+
"token_type",
|
|
67
|
+
"expires_in",
|
|
68
|
+
"organization_id",
|
|
69
|
+
"authentication_method",
|
|
70
|
+
"user",
|
|
71
|
+
]);
|
|
72
|
+
|
|
73
|
+
const assertSafeVerificationEndpoint = (value: string, field: string): string => {
|
|
74
|
+
let url: URL;
|
|
75
|
+
try {
|
|
76
|
+
url = new URL(value);
|
|
77
|
+
} catch {
|
|
78
|
+
throw new WorkosContractError("invalid_endpoint", `${field} must be an absolute HTTPS URL`);
|
|
79
|
+
}
|
|
80
|
+
if (url.protocol !== "https:" || url.username !== "" || url.password !== "" || url.hash !== "") {
|
|
81
|
+
throw new WorkosContractError("invalid_endpoint", `${field} must be an absolute HTTPS URL`);
|
|
82
|
+
}
|
|
83
|
+
return url.toString();
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
export const parseDeviceAuthorization = (
|
|
87
|
+
value: unknown,
|
|
88
|
+
expectedVerificationUri: string,
|
|
89
|
+
): DeviceAuthorization => {
|
|
90
|
+
const input = expectRecord(value);
|
|
91
|
+
const verificationUri = assertSafeVerificationEndpoint(
|
|
92
|
+
expectString(input["verification_uri"], "verification_uri"),
|
|
93
|
+
"verification_uri",
|
|
94
|
+
);
|
|
95
|
+
const expectedUri = assertSafeVerificationEndpoint(
|
|
96
|
+
expectedVerificationUri,
|
|
97
|
+
"expectedVerificationUri",
|
|
98
|
+
);
|
|
99
|
+
if (verificationUri !== expectedUri) {
|
|
100
|
+
throw new WorkosContractError(
|
|
101
|
+
"unexpected_verification_uri",
|
|
102
|
+
"Device verification URI is not allowed",
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
const complete = expectString(input["verification_uri_complete"], "verification_uri_complete", {
|
|
106
|
+
optional: true,
|
|
107
|
+
});
|
|
108
|
+
const userCode = expectString(input["user_code"], "user_code");
|
|
109
|
+
const expectedComplete = `${expectedUri}?user_code=${encodeURIComponent(userCode)}`;
|
|
110
|
+
if (
|
|
111
|
+
complete !== undefined &&
|
|
112
|
+
assertSafeVerificationEndpoint(complete, "verification_uri_complete") !== expectedComplete
|
|
113
|
+
) {
|
|
114
|
+
throw new WorkosContractError(
|
|
115
|
+
"unexpected_verification_uri",
|
|
116
|
+
"Complete device verification URI is not allowed",
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
return {
|
|
120
|
+
deviceCode: expectString(input["device_code"], "device_code"),
|
|
121
|
+
userCode,
|
|
122
|
+
verificationUri,
|
|
123
|
+
...(complete === undefined ? {} : { verificationUriComplete: complete }),
|
|
124
|
+
expiresInSeconds: expectPositiveInteger(input["expires_in"], "expires_in"),
|
|
125
|
+
intervalSeconds: expectPositiveInteger(input["interval"], "interval", { optional: true }) ?? 5,
|
|
126
|
+
};
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
export const parseDeviceTokens = (value: unknown): DeviceTokens => {
|
|
130
|
+
const input = expectRecord(value);
|
|
131
|
+
for (const field of Object.keys(input)) {
|
|
132
|
+
if (!allowedDeviceTokenFields.has(field)) {
|
|
133
|
+
throw new WorkosContractError(
|
|
134
|
+
"unexpected_device_token_field",
|
|
135
|
+
"Device Flow response contained an unexpected field",
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
const expiresIn = expectPositiveInteger(input["expires_in"], "expires_in", { optional: true });
|
|
140
|
+
const organizationId = expectString(input["organization_id"], "organization_id", {
|
|
141
|
+
optional: true,
|
|
142
|
+
});
|
|
143
|
+
return {
|
|
144
|
+
accessToken: expectString(input["access_token"], "access_token"),
|
|
145
|
+
refreshToken: expectString(input["refresh_token"], "refresh_token"),
|
|
146
|
+
tokenType: expectString(input["token_type"], "token_type", { optional: true }) ?? "Bearer",
|
|
147
|
+
...(expiresIn === undefined ? {} : { expiresInSeconds: expiresIn }),
|
|
148
|
+
...(organizationId === undefined ? {} : { organizationId }),
|
|
149
|
+
};
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
export interface WorkosDeviceFlowConfig {
|
|
153
|
+
readonly clientId: string;
|
|
154
|
+
readonly verificationUri: string;
|
|
155
|
+
readonly scope?: string;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export interface WorkosDeviceFlowSession {
|
|
159
|
+
readonly authorization: DeviceAuthorization;
|
|
160
|
+
poll(): Promise<DevicePollResult>;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const readClock = (clock: WorkosClock): number => {
|
|
164
|
+
const now = clock.now().getTime();
|
|
165
|
+
if (!Number.isFinite(now)) {
|
|
166
|
+
throw new WorkosContractError("invalid_clock", "Device Flow clock is invalid");
|
|
167
|
+
}
|
|
168
|
+
return now;
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
export class WorkosDeviceFlowClient {
|
|
172
|
+
public constructor(
|
|
173
|
+
private readonly config: WorkosDeviceFlowConfig,
|
|
174
|
+
private readonly transport: WorkosPublicTransport,
|
|
175
|
+
private readonly clock: WorkosClock,
|
|
176
|
+
) {
|
|
177
|
+
if (config.clientId.length === 0) {
|
|
178
|
+
throw new WorkosContractError("invalid_client", "Public client ID must not be empty");
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
public async authorize(): Promise<WorkosDeviceFlowSession> {
|
|
183
|
+
const body = new URLSearchParams({ client_id: this.config.clientId });
|
|
184
|
+
if (this.config.scope !== undefined) body.set("scope", this.config.scope);
|
|
185
|
+
const response = await this.transport.send({
|
|
186
|
+
url: WORKOS_AUTHKIT_DEVICE_AUTHORIZATION_ENDPOINT,
|
|
187
|
+
method: "POST",
|
|
188
|
+
headers: { "content-type": "application/x-www-form-urlencoded" },
|
|
189
|
+
body: body.toString(),
|
|
190
|
+
redirect: "error",
|
|
191
|
+
});
|
|
192
|
+
if (response.status < 200 || response.status >= 300) {
|
|
193
|
+
throw new WorkosContractError("device_authorization_failed", "Device authorization failed");
|
|
194
|
+
}
|
|
195
|
+
return new DeviceFlowSession(
|
|
196
|
+
parseDeviceAuthorization(parseJsonObject(response.body), this.config.verificationUri),
|
|
197
|
+
this.config.clientId,
|
|
198
|
+
this.transport,
|
|
199
|
+
this.clock,
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
class DeviceFlowSession implements WorkosDeviceFlowSession {
|
|
205
|
+
readonly #expiresAtMs: number;
|
|
206
|
+
#nextPollAtMs: number;
|
|
207
|
+
#intervalSeconds: number;
|
|
208
|
+
#terminal = false;
|
|
209
|
+
|
|
210
|
+
public constructor(
|
|
211
|
+
public readonly authorization: DeviceAuthorization,
|
|
212
|
+
private readonly clientId: string,
|
|
213
|
+
private readonly transport: WorkosPublicTransport,
|
|
214
|
+
private readonly clock: WorkosClock,
|
|
215
|
+
) {
|
|
216
|
+
const startedAt = readClock(clock);
|
|
217
|
+
this.#expiresAtMs = startedAt + authorization.expiresInSeconds * 1_000;
|
|
218
|
+
this.#intervalSeconds = authorization.intervalSeconds;
|
|
219
|
+
this.#nextPollAtMs = startedAt + this.#intervalSeconds * 1_000;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
public async poll(): Promise<DevicePollResult> {
|
|
223
|
+
if (this.#terminal) {
|
|
224
|
+
throw new WorkosContractError("device_flow_complete", "Device authorization is complete");
|
|
225
|
+
}
|
|
226
|
+
const now = readClock(this.clock);
|
|
227
|
+
if (now >= this.#expiresAtMs) {
|
|
228
|
+
this.#terminal = true;
|
|
229
|
+
throw new WorkosContractError("expired_token", "Device authorization expired");
|
|
230
|
+
}
|
|
231
|
+
if (now < this.#nextPollAtMs) {
|
|
232
|
+
throw new WorkosContractError(
|
|
233
|
+
"poll_too_soon",
|
|
234
|
+
"Device authorization polling is too frequent",
|
|
235
|
+
);
|
|
236
|
+
}
|
|
237
|
+
const response = await this.transport.send({
|
|
238
|
+
url: WORKOS_AUTHKIT_DEVICE_TOKEN_ENDPOINT,
|
|
239
|
+
method: "POST",
|
|
240
|
+
headers: { "content-type": "application/x-www-form-urlencoded" },
|
|
241
|
+
body: new URLSearchParams({
|
|
242
|
+
client_id: this.clientId,
|
|
243
|
+
device_code: this.authorization.deviceCode,
|
|
244
|
+
grant_type: "urn:ietf:params:oauth:grant-type:device_code",
|
|
245
|
+
}).toString(),
|
|
246
|
+
redirect: "error",
|
|
247
|
+
});
|
|
248
|
+
const payload = parseJsonObject(response.body);
|
|
249
|
+
if (response.status >= 200 && response.status < 300) {
|
|
250
|
+
this.#terminal = true;
|
|
251
|
+
return {
|
|
252
|
+
state: "complete",
|
|
253
|
+
tokens: {
|
|
254
|
+
...parseDeviceTokens(payload),
|
|
255
|
+
provenance: { kind: "workos_authkit_device_flow", clientId: this.clientId },
|
|
256
|
+
},
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
const error = expectString(payload["error"], "error");
|
|
260
|
+
if (error === "authorization_pending" || error === "slow_down") {
|
|
261
|
+
if (error === "slow_down") this.#intervalSeconds += 5;
|
|
262
|
+
this.#nextPollAtMs = now + this.#intervalSeconds * 1_000;
|
|
263
|
+
return { state: "pending", nextPollAt: new Date(this.#nextPollAtMs) };
|
|
264
|
+
}
|
|
265
|
+
this.#terminal = true;
|
|
266
|
+
if (error === "access_denied" || error === "expired_token") {
|
|
267
|
+
throw new WorkosContractError(error, `Device authorization ended: ${error}`);
|
|
268
|
+
}
|
|
269
|
+
throw new WorkosContractError("token_exchange_failed", "Device token exchange failed");
|
|
270
|
+
}
|
|
271
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export * from "./device-flow.js";
|
|
2
|
+
export * from "./management-rpc.js";
|
|
3
|
+
export * from "./organizations.js";
|
|
4
|
+
export * from "./parsing.js";
|
|
5
|
+
export * from "./session-status.js";
|
|
6
|
+
export * from "./tokens.js";
|
|
7
|
+
export * from "./user-api-keys.js";
|
|
8
|
+
export * from "./webhooks.js";
|
|
@@ -0,0 +1,379 @@
|
|
|
1
|
+
import { expectRecord, expectString, parseIsoDate, WorkosContractError } from "./parsing.js";
|
|
2
|
+
import type { WorkosMembershipState, WorkosOrganizationSnapshot } from "./organizations.js";
|
|
3
|
+
|
|
4
|
+
export type WorkosFgaPrincipalKind = "user" | "organization_api_key" | "m2m_service";
|
|
5
|
+
export type WorkosUserAuthenticationMethod = "interactive" | "device_flow";
|
|
6
|
+
export type WorkosFgaPrincipal =
|
|
7
|
+
| {
|
|
8
|
+
readonly kind: "user";
|
|
9
|
+
readonly id: `user:${string}`;
|
|
10
|
+
readonly authenticationMethod: WorkosUserAuthenticationMethod;
|
|
11
|
+
}
|
|
12
|
+
| { readonly kind: "organization_api_key"; readonly id: `api_key:${string}` }
|
|
13
|
+
| { readonly kind: "m2m_service"; readonly id: `service:${string}` };
|
|
14
|
+
export type WorkosFgaResourceType = "organization" | "project" | "environment";
|
|
15
|
+
export type WorkosFgaAction =
|
|
16
|
+
| "organization.read"
|
|
17
|
+
| "project.create"
|
|
18
|
+
| "project.read"
|
|
19
|
+
| "project.deploy"
|
|
20
|
+
| "project.transfer"
|
|
21
|
+
| "operation.read"
|
|
22
|
+
| "audit.read";
|
|
23
|
+
|
|
24
|
+
export const WORKOS_CAPABILITY_OBSERVATION_IDS = [
|
|
25
|
+
"authkit_oidc_metadata",
|
|
26
|
+
"connect_applications_read",
|
|
27
|
+
"connect_client_secrets_metadata_read",
|
|
28
|
+
"fga_resources_read",
|
|
29
|
+
"memberships_read",
|
|
30
|
+
"m2m_token_flow",
|
|
31
|
+
"organizations_read",
|
|
32
|
+
"webhook_endpoints_read",
|
|
33
|
+
] as const;
|
|
34
|
+
|
|
35
|
+
export type WorkosCapabilityObservationId = (typeof WORKOS_CAPABILITY_OBSERVATION_IDS)[number];
|
|
36
|
+
|
|
37
|
+
export interface WorkosCapabilityObservation {
|
|
38
|
+
readonly id: WorkosCapabilityObservationId;
|
|
39
|
+
readonly available: boolean;
|
|
40
|
+
readonly statusCode: number;
|
|
41
|
+
readonly count?: number;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export type WorkosManagementRpcRequest =
|
|
45
|
+
| {
|
|
46
|
+
readonly method: "organizations.get_current_snapshot";
|
|
47
|
+
readonly requestId: string;
|
|
48
|
+
readonly payload: { readonly workosOrganizationId: string };
|
|
49
|
+
}
|
|
50
|
+
| {
|
|
51
|
+
readonly method: "capabilities.probe_read_only";
|
|
52
|
+
readonly requestId: string;
|
|
53
|
+
readonly payload: Record<string, never>;
|
|
54
|
+
}
|
|
55
|
+
| {
|
|
56
|
+
readonly method: "fga.check_candidate";
|
|
57
|
+
readonly requestId: string;
|
|
58
|
+
readonly payload: {
|
|
59
|
+
readonly principal: WorkosFgaPrincipal;
|
|
60
|
+
readonly organizationId: string;
|
|
61
|
+
readonly resource: {
|
|
62
|
+
readonly type: WorkosFgaResourceType;
|
|
63
|
+
readonly id: string;
|
|
64
|
+
};
|
|
65
|
+
readonly action: WorkosFgaAction;
|
|
66
|
+
};
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
export type WorkosManagementRpcResponse =
|
|
70
|
+
| {
|
|
71
|
+
readonly method: "organizations.get_current_snapshot";
|
|
72
|
+
readonly requestId: string;
|
|
73
|
+
readonly ok: true;
|
|
74
|
+
readonly result: { readonly snapshot: WorkosOrganizationSnapshot };
|
|
75
|
+
}
|
|
76
|
+
| {
|
|
77
|
+
readonly method: "capabilities.probe_read_only";
|
|
78
|
+
readonly requestId: string;
|
|
79
|
+
readonly ok: true;
|
|
80
|
+
readonly result: { readonly observations: readonly WorkosCapabilityObservation[] };
|
|
81
|
+
}
|
|
82
|
+
| {
|
|
83
|
+
readonly method: "fga.check_candidate";
|
|
84
|
+
readonly requestId: string;
|
|
85
|
+
readonly ok: true;
|
|
86
|
+
readonly result: { readonly decision: "allow" | "deny" };
|
|
87
|
+
}
|
|
88
|
+
| {
|
|
89
|
+
readonly method: WorkosManagementRpcRequest["method"];
|
|
90
|
+
readonly requestId: string;
|
|
91
|
+
readonly ok: false;
|
|
92
|
+
readonly error: { readonly code: "invalid_request" | "not_found" | "unavailable" };
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
export interface WorkosManagementRpc {
|
|
96
|
+
call(request: WorkosManagementRpcRequest): Promise<WorkosManagementRpcResponse>;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const assertOnlyKeys = (input: Record<string, unknown>, allowed: readonly string[]) => {
|
|
100
|
+
const allowedKeys = new Set(allowed);
|
|
101
|
+
if (Object.keys(input).some((key) => !allowedKeys.has(key))) {
|
|
102
|
+
throw new WorkosContractError("rpc_field_not_allowed", "Management RPC field is not allowed");
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
const parseMembershipState = (value: unknown): WorkosMembershipState => {
|
|
107
|
+
if (value !== "active" && value !== "inactive" && value !== "pending") {
|
|
108
|
+
throw new WorkosContractError("invalid_rpc_membership", "RPC membership status is invalid");
|
|
109
|
+
}
|
|
110
|
+
return value;
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
const parsePrincipalKind = (value: unknown): WorkosFgaPrincipalKind => {
|
|
114
|
+
if (value !== "user" && value !== "organization_api_key" && value !== "m2m_service") {
|
|
115
|
+
throw new WorkosContractError("invalid_rpc_principal", "RPC principal kind is invalid");
|
|
116
|
+
}
|
|
117
|
+
return value;
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
const parseCanonicalPrincipalId = <Prefix extends "user" | "api_key" | "service">(
|
|
121
|
+
value: unknown,
|
|
122
|
+
prefix: Prefix,
|
|
123
|
+
): `${Prefix}:${string}` => {
|
|
124
|
+
const id = expectString(value, "principal.id");
|
|
125
|
+
if (!new RegExp(`^${prefix}:[A-Za-z0-9_-]+$`, "u").test(id)) {
|
|
126
|
+
throw new WorkosContractError("invalid_rpc_principal_id", "RPC principal ID is not canonical");
|
|
127
|
+
}
|
|
128
|
+
return id as `${Prefix}:${string}`;
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
const parsePrincipal = (value: unknown): WorkosFgaPrincipal => {
|
|
132
|
+
const principal = expectRecord(value, "invalid_rpc_principal");
|
|
133
|
+
const kind = parsePrincipalKind(principal["kind"]);
|
|
134
|
+
if (kind === "user") {
|
|
135
|
+
assertOnlyKeys(principal, ["kind", "id", "authenticationMethod"]);
|
|
136
|
+
const authenticationMethod = principal["authenticationMethod"];
|
|
137
|
+
if (authenticationMethod !== "interactive" && authenticationMethod !== "device_flow") {
|
|
138
|
+
throw new WorkosContractError(
|
|
139
|
+
"invalid_rpc_authentication_method",
|
|
140
|
+
"RPC user authentication method is invalid",
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
return {
|
|
144
|
+
kind,
|
|
145
|
+
id: parseCanonicalPrincipalId(principal["id"], "user"),
|
|
146
|
+
authenticationMethod,
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
assertOnlyKeys(principal, ["kind", "id"]);
|
|
150
|
+
return kind === "organization_api_key"
|
|
151
|
+
? { kind, id: parseCanonicalPrincipalId(principal["id"], "api_key") }
|
|
152
|
+
: { kind, id: parseCanonicalPrincipalId(principal["id"], "service") };
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
const parseResourceType = (value: unknown): WorkosFgaResourceType => {
|
|
156
|
+
if (value !== "organization" && value !== "project" && value !== "environment") {
|
|
157
|
+
throw new WorkosContractError("invalid_rpc_resource", "RPC resource type is invalid");
|
|
158
|
+
}
|
|
159
|
+
return value;
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
const parseFgaAction = (value: unknown): WorkosFgaAction => {
|
|
163
|
+
if (
|
|
164
|
+
value !== "organization.read" &&
|
|
165
|
+
value !== "project.create" &&
|
|
166
|
+
value !== "project.read" &&
|
|
167
|
+
value !== "project.deploy" &&
|
|
168
|
+
value !== "project.transfer" &&
|
|
169
|
+
value !== "operation.read" &&
|
|
170
|
+
value !== "audit.read"
|
|
171
|
+
) {
|
|
172
|
+
throw new WorkosContractError("invalid_rpc_action", "RPC action is invalid");
|
|
173
|
+
}
|
|
174
|
+
return value;
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
const parseCanonicalSnapshot = (value: unknown): WorkosOrganizationSnapshot => {
|
|
178
|
+
const input = expectRecord(value, "invalid_rpc_snapshot");
|
|
179
|
+
assertOnlyKeys(input, ["organization", "memberships", "fetchedAt"]);
|
|
180
|
+
const organization = expectRecord(input["organization"], "invalid_rpc_organization");
|
|
181
|
+
assertOnlyKeys(organization, ["id", "name", "createdAt", "updatedAt"]);
|
|
182
|
+
if (!Array.isArray(input["memberships"])) {
|
|
183
|
+
throw new WorkosContractError("invalid_rpc_snapshot", "RPC memberships must be an array");
|
|
184
|
+
}
|
|
185
|
+
const memberships = input["memberships"].map((value) => {
|
|
186
|
+
const membership = expectRecord(value, "invalid_rpc_membership");
|
|
187
|
+
assertOnlyKeys(membership, ["id", "userId", "status", "roles", "createdAt", "updatedAt"]);
|
|
188
|
+
const status = parseMembershipState(membership["status"]);
|
|
189
|
+
const roles = membership["roles"];
|
|
190
|
+
if (!Array.isArray(roles) || roles.length === 0) {
|
|
191
|
+
throw new WorkosContractError("invalid_rpc_membership", "RPC membership roles are invalid");
|
|
192
|
+
}
|
|
193
|
+
const uniqueRoles = [
|
|
194
|
+
...new Set(roles.map((role) => expectString(role, "membership.role"))),
|
|
195
|
+
].sort();
|
|
196
|
+
if (uniqueRoles.length > 1) {
|
|
197
|
+
throw new WorkosContractError(
|
|
198
|
+
"unsupported_multiple_roles",
|
|
199
|
+
"Membership has unsupported multiple roles",
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
return {
|
|
203
|
+
id: expectString(membership["id"], "membership.id"),
|
|
204
|
+
userId: expectString(membership["userId"], "membership.userId"),
|
|
205
|
+
status,
|
|
206
|
+
roles: uniqueRoles,
|
|
207
|
+
createdAt: parseIsoDate(membership["createdAt"], "membership.createdAt"),
|
|
208
|
+
updatedAt: parseIsoDate(membership["updatedAt"], "membership.updatedAt"),
|
|
209
|
+
};
|
|
210
|
+
});
|
|
211
|
+
return {
|
|
212
|
+
organization: {
|
|
213
|
+
id: expectString(organization["id"], "organization.id"),
|
|
214
|
+
name: expectString(organization["name"], "organization.name"),
|
|
215
|
+
createdAt: parseIsoDate(organization["createdAt"], "organization.createdAt"),
|
|
216
|
+
updatedAt: parseIsoDate(organization["updatedAt"], "organization.updatedAt"),
|
|
217
|
+
},
|
|
218
|
+
memberships,
|
|
219
|
+
fetchedAt: parseIsoDate(input["fetchedAt"], "fetchedAt"),
|
|
220
|
+
};
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
const capabilityObservationIds = new Set<string>(WORKOS_CAPABILITY_OBSERVATION_IDS);
|
|
224
|
+
|
|
225
|
+
const parseCapabilityObservation = (value: unknown): WorkosCapabilityObservation => {
|
|
226
|
+
const observation = expectRecord(value, "invalid_rpc_capability_observation");
|
|
227
|
+
assertOnlyKeys(observation, ["id", "available", "statusCode", "count"]);
|
|
228
|
+
const id = expectString(observation["id"], "observation.id");
|
|
229
|
+
if (!capabilityObservationIds.has(id)) {
|
|
230
|
+
throw new WorkosContractError(
|
|
231
|
+
"invalid_rpc_capability_observation",
|
|
232
|
+
"RPC capability observation ID is invalid",
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
if (typeof observation["available"] !== "boolean") {
|
|
236
|
+
throw new WorkosContractError(
|
|
237
|
+
"invalid_rpc_capability_observation",
|
|
238
|
+
"RPC capability availability is invalid",
|
|
239
|
+
);
|
|
240
|
+
}
|
|
241
|
+
const statusCode = observation["statusCode"];
|
|
242
|
+
if (
|
|
243
|
+
!Number.isSafeInteger(statusCode) ||
|
|
244
|
+
(statusCode as number) < 100 ||
|
|
245
|
+
(statusCode as number) > 599
|
|
246
|
+
) {
|
|
247
|
+
throw new WorkosContractError(
|
|
248
|
+
"invalid_rpc_capability_observation",
|
|
249
|
+
"RPC capability status code is invalid",
|
|
250
|
+
);
|
|
251
|
+
}
|
|
252
|
+
const count = observation["count"];
|
|
253
|
+
if (count !== undefined && (!Number.isSafeInteger(count) || (count as number) < 0)) {
|
|
254
|
+
throw new WorkosContractError(
|
|
255
|
+
"invalid_rpc_capability_observation",
|
|
256
|
+
"RPC capability count is invalid",
|
|
257
|
+
);
|
|
258
|
+
}
|
|
259
|
+
const parsed = {
|
|
260
|
+
id: id as WorkosCapabilityObservationId,
|
|
261
|
+
available: observation["available"],
|
|
262
|
+
statusCode: statusCode as number,
|
|
263
|
+
};
|
|
264
|
+
return count === undefined ? parsed : { ...parsed, count: count as number };
|
|
265
|
+
};
|
|
266
|
+
|
|
267
|
+
export const parseWorkosManagementRpcRequest = (value: unknown): WorkosManagementRpcRequest => {
|
|
268
|
+
const input = expectRecord(value, "invalid_rpc_request");
|
|
269
|
+
assertOnlyKeys(input, ["method", "requestId", "payload"]);
|
|
270
|
+
const requestId = expectString(input["requestId"], "requestId");
|
|
271
|
+
const payload = expectRecord(input["payload"], "invalid_rpc_payload");
|
|
272
|
+
if (input["method"] === "organizations.get_current_snapshot") {
|
|
273
|
+
assertOnlyKeys(payload, ["workosOrganizationId"]);
|
|
274
|
+
return {
|
|
275
|
+
method: input["method"],
|
|
276
|
+
requestId,
|
|
277
|
+
payload: {
|
|
278
|
+
workosOrganizationId: expectString(payload["workosOrganizationId"], "workosOrganizationId"),
|
|
279
|
+
},
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
if (input["method"] === "capabilities.probe_read_only") {
|
|
283
|
+
assertOnlyKeys(payload, []);
|
|
284
|
+
return { method: input["method"], requestId, payload: {} };
|
|
285
|
+
}
|
|
286
|
+
if (input["method"] === "fga.check_candidate") {
|
|
287
|
+
assertOnlyKeys(payload, ["principal", "organizationId", "resource", "action"]);
|
|
288
|
+
const principal = parsePrincipal(payload["principal"]);
|
|
289
|
+
const resource = expectRecord(payload["resource"], "invalid_rpc_resource");
|
|
290
|
+
assertOnlyKeys(resource, ["type", "id"]);
|
|
291
|
+
const resourceType = parseResourceType(resource["type"]);
|
|
292
|
+
const action = parseFgaAction(payload["action"]);
|
|
293
|
+
return {
|
|
294
|
+
method: input["method"],
|
|
295
|
+
requestId,
|
|
296
|
+
payload: {
|
|
297
|
+
principal,
|
|
298
|
+
organizationId: expectString(payload["organizationId"], "organizationId"),
|
|
299
|
+
resource: {
|
|
300
|
+
type: resourceType,
|
|
301
|
+
id: expectString(resource["id"], "resource.id"),
|
|
302
|
+
},
|
|
303
|
+
action,
|
|
304
|
+
},
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
throw new WorkosContractError("rpc_method_not_allowed", "Management RPC method is not allowed");
|
|
308
|
+
};
|
|
309
|
+
|
|
310
|
+
export const parseWorkosManagementRpcResponse = (value: unknown): WorkosManagementRpcResponse => {
|
|
311
|
+
const input = expectRecord(value, "invalid_rpc_response");
|
|
312
|
+
const requestId = expectString(input["requestId"], "requestId");
|
|
313
|
+
if (
|
|
314
|
+
input["method"] !== "organizations.get_current_snapshot" &&
|
|
315
|
+
input["method"] !== "capabilities.probe_read_only" &&
|
|
316
|
+
input["method"] !== "fga.check_candidate"
|
|
317
|
+
) {
|
|
318
|
+
throw new WorkosContractError("rpc_method_not_allowed", "Management RPC method is not allowed");
|
|
319
|
+
}
|
|
320
|
+
if (input["ok"] === false) {
|
|
321
|
+
assertOnlyKeys(input, ["method", "requestId", "ok", "error"]);
|
|
322
|
+
const error = expectRecord(input["error"], "invalid_rpc_error");
|
|
323
|
+
assertOnlyKeys(error, ["code"]);
|
|
324
|
+
if (
|
|
325
|
+
error["code"] !== "invalid_request" &&
|
|
326
|
+
error["code"] !== "not_found" &&
|
|
327
|
+
error["code"] !== "unavailable"
|
|
328
|
+
) {
|
|
329
|
+
throw new WorkosContractError("invalid_rpc_error", "Management RPC error code is invalid");
|
|
330
|
+
}
|
|
331
|
+
return { method: input["method"], requestId, ok: false, error: { code: error["code"] } };
|
|
332
|
+
}
|
|
333
|
+
if (input["ok"] !== true) {
|
|
334
|
+
throw new WorkosContractError("invalid_rpc_response", "Management RPC status is invalid");
|
|
335
|
+
}
|
|
336
|
+
assertOnlyKeys(input, ["method", "requestId", "ok", "result"]);
|
|
337
|
+
const result = expectRecord(input["result"], "invalid_rpc_result");
|
|
338
|
+
if (input["method"] === "organizations.get_current_snapshot") {
|
|
339
|
+
assertOnlyKeys(result, ["snapshot"]);
|
|
340
|
+
return {
|
|
341
|
+
method: input["method"],
|
|
342
|
+
requestId,
|
|
343
|
+
ok: true,
|
|
344
|
+
result: { snapshot: parseCanonicalSnapshot(result["snapshot"]) },
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
if (input["method"] === "capabilities.probe_read_only") {
|
|
348
|
+
assertOnlyKeys(result, ["observations"]);
|
|
349
|
+
if (!Array.isArray(result["observations"])) {
|
|
350
|
+
throw new WorkosContractError(
|
|
351
|
+
"invalid_rpc_result",
|
|
352
|
+
"Management RPC capability observations are invalid",
|
|
353
|
+
);
|
|
354
|
+
}
|
|
355
|
+
const observations = result["observations"].map(parseCapabilityObservation);
|
|
356
|
+
if (new Set(observations.map(({ id }) => id)).size !== observations.length) {
|
|
357
|
+
throw new WorkosContractError(
|
|
358
|
+
"invalid_rpc_result",
|
|
359
|
+
"Management RPC capability observations contain duplicates",
|
|
360
|
+
);
|
|
361
|
+
}
|
|
362
|
+
return {
|
|
363
|
+
method: input["method"],
|
|
364
|
+
requestId,
|
|
365
|
+
ok: true,
|
|
366
|
+
result: { observations },
|
|
367
|
+
};
|
|
368
|
+
}
|
|
369
|
+
assertOnlyKeys(result, ["decision"]);
|
|
370
|
+
if (result["decision"] !== "allow" && result["decision"] !== "deny") {
|
|
371
|
+
throw new WorkosContractError("invalid_rpc_result", "Management RPC decision is invalid");
|
|
372
|
+
}
|
|
373
|
+
return {
|
|
374
|
+
method: input["method"],
|
|
375
|
+
requestId,
|
|
376
|
+
ok: true,
|
|
377
|
+
result: { decision: result["decision"] },
|
|
378
|
+
};
|
|
379
|
+
};
|