@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,113 @@
|
|
|
1
|
+
import { expectRecord, expectString, parseIsoDate, WorkosContractError } from "./parsing.js";
|
|
2
|
+
|
|
3
|
+
export type WorkosMembershipState = "active" | "inactive" | "pending";
|
|
4
|
+
|
|
5
|
+
export interface WorkosOrganizationSnapshot {
|
|
6
|
+
readonly organization: {
|
|
7
|
+
readonly id: string;
|
|
8
|
+
readonly name: string;
|
|
9
|
+
readonly createdAt: string;
|
|
10
|
+
readonly updatedAt: string;
|
|
11
|
+
};
|
|
12
|
+
readonly memberships: readonly {
|
|
13
|
+
readonly id: string;
|
|
14
|
+
readonly userId: string;
|
|
15
|
+
readonly status: WorkosMembershipState;
|
|
16
|
+
readonly roles: readonly string[];
|
|
17
|
+
readonly createdAt: string;
|
|
18
|
+
readonly updatedAt: string;
|
|
19
|
+
}[];
|
|
20
|
+
readonly fetchedAt: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const parseMembership = (value: unknown, organizationId: string) => {
|
|
24
|
+
const input = expectRecord(value, "invalid_membership");
|
|
25
|
+
if (input["organization_id"] !== organizationId) {
|
|
26
|
+
throw new WorkosContractError(
|
|
27
|
+
"organization_mismatch",
|
|
28
|
+
"Membership belongs to a different organization",
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
if (
|
|
32
|
+
input["status"] !== "active" &&
|
|
33
|
+
input["status"] !== "inactive" &&
|
|
34
|
+
input["status"] !== "pending"
|
|
35
|
+
) {
|
|
36
|
+
throw new WorkosContractError("invalid_membership", "Membership status is invalid");
|
|
37
|
+
}
|
|
38
|
+
const rawRoles = input["roles"];
|
|
39
|
+
if (!Array.isArray(rawRoles) || rawRoles.length === 0) {
|
|
40
|
+
throw new WorkosContractError("invalid_membership", "Membership roles are required");
|
|
41
|
+
}
|
|
42
|
+
const roles = rawRoles.map((value) => {
|
|
43
|
+
const role = expectRecord(value, "invalid_role");
|
|
44
|
+
return expectString(role["slug"], "role.slug");
|
|
45
|
+
});
|
|
46
|
+
const uniqueRoles = [...new Set(roles)].sort();
|
|
47
|
+
if (uniqueRoles.length > 1) {
|
|
48
|
+
throw new WorkosContractError(
|
|
49
|
+
"unsupported_multiple_roles",
|
|
50
|
+
"Membership has unsupported multiple roles",
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
return {
|
|
54
|
+
id: expectString(input["id"], "membership.id"),
|
|
55
|
+
userId: expectString(input["user_id"], "membership.user_id"),
|
|
56
|
+
status: input["status"],
|
|
57
|
+
roles: uniqueRoles,
|
|
58
|
+
createdAt: parseIsoDate(input["created_at"], "membership.created_at"),
|
|
59
|
+
updatedAt: parseIsoDate(input["updated_at"], "membership.updated_at"),
|
|
60
|
+
} satisfies WorkosOrganizationSnapshot["memberships"][number];
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
export const parseWorkosOrganizationSnapshot = (
|
|
64
|
+
value: unknown,
|
|
65
|
+
fetchedAt: Date,
|
|
66
|
+
): WorkosOrganizationSnapshot => {
|
|
67
|
+
const input = expectRecord(value, "invalid_organization_snapshot");
|
|
68
|
+
const organization = expectRecord(input["organization"], "invalid_organization");
|
|
69
|
+
const organizationId = expectString(organization["id"], "organization.id");
|
|
70
|
+
if (!Array.isArray(input["memberships"])) {
|
|
71
|
+
throw new WorkosContractError("invalid_organization_snapshot", "Membership list is required");
|
|
72
|
+
}
|
|
73
|
+
const memberships = input["memberships"].map((item) => parseMembership(item, organizationId));
|
|
74
|
+
const seenIds = new Set<string>();
|
|
75
|
+
const seenUsers = new Set<string>();
|
|
76
|
+
for (const membership of memberships) {
|
|
77
|
+
if (seenIds.has(membership.id) || seenUsers.has(membership.userId)) {
|
|
78
|
+
throw new WorkosContractError(
|
|
79
|
+
"duplicate_membership",
|
|
80
|
+
"Membership snapshot contains duplicates",
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
seenIds.add(membership.id);
|
|
84
|
+
seenUsers.add(membership.userId);
|
|
85
|
+
}
|
|
86
|
+
return {
|
|
87
|
+
organization: {
|
|
88
|
+
id: organizationId,
|
|
89
|
+
name: expectString(organization["name"], "organization.name"),
|
|
90
|
+
createdAt: parseIsoDate(organization["created_at"], "organization.created_at"),
|
|
91
|
+
updatedAt: parseIsoDate(organization["updated_at"], "organization.updated_at"),
|
|
92
|
+
},
|
|
93
|
+
memberships: memberships.sort((left, right) => left.userId.localeCompare(right.userId)),
|
|
94
|
+
fetchedAt: fetchedAt.toISOString(),
|
|
95
|
+
};
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
export const canonicalizeWorkosOrganizationSnapshot = (
|
|
99
|
+
snapshot: WorkosOrganizationSnapshot,
|
|
100
|
+
): string =>
|
|
101
|
+
JSON.stringify({ organization: snapshot.organization, memberships: snapshot.memberships });
|
|
102
|
+
|
|
103
|
+
export interface WorkosOrganizationSnapshotSource {
|
|
104
|
+
fetchCurrentOrganizationSnapshot(workosOrganizationId: string): Promise<unknown>;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export interface WorkosOrganizationSnapshotSink {
|
|
108
|
+
applyCurrentSnapshot(input: {
|
|
109
|
+
readonly triggerEventId: string;
|
|
110
|
+
readonly snapshot: WorkosOrganizationSnapshot;
|
|
111
|
+
readonly canonicalSnapshot: string;
|
|
112
|
+
}): Promise<{ readonly changed: boolean; readonly mappingRevision: number }>;
|
|
113
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
export class WorkosContractError extends Error {
|
|
2
|
+
public constructor(
|
|
3
|
+
public readonly code: string,
|
|
4
|
+
message: string,
|
|
5
|
+
) {
|
|
6
|
+
super(message);
|
|
7
|
+
this.name = "WorkosContractError";
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export const expectRecord = (value: unknown, code = "invalid_object"): Record<string, unknown> => {
|
|
12
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
13
|
+
throw new WorkosContractError(code, "WorkOS response must be an object");
|
|
14
|
+
}
|
|
15
|
+
return value as Record<string, unknown>;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export function expectString(value: unknown, field: string): string;
|
|
19
|
+
export function expectString(
|
|
20
|
+
value: unknown,
|
|
21
|
+
field: string,
|
|
22
|
+
options: { readonly optional: true },
|
|
23
|
+
): string | undefined;
|
|
24
|
+
export function expectString(
|
|
25
|
+
value: unknown,
|
|
26
|
+
field: string,
|
|
27
|
+
options: { readonly optional?: boolean } = {},
|
|
28
|
+
): string | undefined {
|
|
29
|
+
if (value === undefined && options.optional === true) return undefined;
|
|
30
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
31
|
+
throw new WorkosContractError(
|
|
32
|
+
"invalid_field",
|
|
33
|
+
`WorkOS field ${field} must be a non-empty string`,
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
return value;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function expectPositiveInteger(value: unknown, field: string): number;
|
|
40
|
+
export function expectPositiveInteger(
|
|
41
|
+
value: unknown,
|
|
42
|
+
field: string,
|
|
43
|
+
options: { readonly optional: true },
|
|
44
|
+
): number | undefined;
|
|
45
|
+
export function expectPositiveInteger(
|
|
46
|
+
value: unknown,
|
|
47
|
+
field: string,
|
|
48
|
+
options: { readonly optional?: boolean } = {},
|
|
49
|
+
): number | undefined {
|
|
50
|
+
if (value === undefined && options.optional === true) return undefined;
|
|
51
|
+
if (!Number.isSafeInteger(value) || (value as number) <= 0) {
|
|
52
|
+
throw new WorkosContractError("invalid_field", `WorkOS field ${field} must be positive`);
|
|
53
|
+
}
|
|
54
|
+
return value as number;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export const parseJsonObject = (body: string): Record<string, unknown> => {
|
|
58
|
+
try {
|
|
59
|
+
return expectRecord(JSON.parse(body) as unknown);
|
|
60
|
+
} catch (error) {
|
|
61
|
+
if (error instanceof WorkosContractError) throw error;
|
|
62
|
+
throw new WorkosContractError("invalid_json", "WorkOS returned invalid JSON");
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
export const parseIsoDate = (value: unknown, field: string): string => {
|
|
67
|
+
const text = expectString(value, field);
|
|
68
|
+
if (text === undefined || !Number.isFinite(Date.parse(text))) {
|
|
69
|
+
throw new WorkosContractError(
|
|
70
|
+
"invalid_field",
|
|
71
|
+
`WorkOS field ${field} must be an ISO timestamp`,
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
return new Date(text).toISOString();
|
|
75
|
+
};
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import {
|
|
2
|
+
expectPositiveInteger,
|
|
3
|
+
expectRecord,
|
|
4
|
+
expectString,
|
|
5
|
+
WorkosContractError,
|
|
6
|
+
} from "./parsing.js";
|
|
7
|
+
|
|
8
|
+
export interface WorkosSessionStatusRequest {
|
|
9
|
+
readonly sessionId: string;
|
|
10
|
+
readonly userId: string;
|
|
11
|
+
readonly organizationId: string | null;
|
|
12
|
+
readonly tokenExpiresAt: number;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface WorkosSessionStatusResponse {
|
|
16
|
+
readonly active: boolean;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const assertOnlyKeys = (input: Record<string, unknown>, allowed: readonly string[]): void => {
|
|
20
|
+
const allowedKeys = new Set(allowed);
|
|
21
|
+
if (Object.keys(input).some((key) => !allowedKeys.has(key))) {
|
|
22
|
+
throw new WorkosContractError(
|
|
23
|
+
"session_status_field_not_allowed",
|
|
24
|
+
"Session-status field is not allowed",
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export const parseWorkosSessionStatusRequest = (value: unknown): WorkosSessionStatusRequest => {
|
|
30
|
+
const input = expectRecord(value, "invalid_session_status_request");
|
|
31
|
+
assertOnlyKeys(input, ["sessionId", "userId", "organizationId", "tokenExpiresAt"]);
|
|
32
|
+
return {
|
|
33
|
+
sessionId: expectString(input["sessionId"], "sessionId"),
|
|
34
|
+
userId: expectString(input["userId"], "userId"),
|
|
35
|
+
organizationId:
|
|
36
|
+
input["organizationId"] === null
|
|
37
|
+
? null
|
|
38
|
+
: expectString(input["organizationId"], "organizationId"),
|
|
39
|
+
tokenExpiresAt: expectPositiveInteger(input["tokenExpiresAt"], "tokenExpiresAt"),
|
|
40
|
+
};
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
export const parseWorkosSessionStatusResponse = (value: unknown): WorkosSessionStatusResponse => {
|
|
44
|
+
const input = expectRecord(value, "invalid_session_status_response");
|
|
45
|
+
assertOnlyKeys(input, ["active"]);
|
|
46
|
+
if (typeof input["active"] !== "boolean") {
|
|
47
|
+
throw new WorkosContractError(
|
|
48
|
+
"invalid_session_status_response",
|
|
49
|
+
"Session-status active field must be a boolean",
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
return { active: input["active"] };
|
|
53
|
+
};
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
import {
|
|
2
|
+
expectPositiveInteger,
|
|
3
|
+
expectRecord,
|
|
4
|
+
expectString,
|
|
5
|
+
WorkosContractError,
|
|
6
|
+
} from "./parsing.js";
|
|
7
|
+
|
|
8
|
+
export interface WorkosJwtHeader {
|
|
9
|
+
readonly alg: "RS256";
|
|
10
|
+
readonly kid: string;
|
|
11
|
+
readonly typ?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface WorkosSessionClaims {
|
|
15
|
+
readonly kind: "session";
|
|
16
|
+
readonly issuer: string;
|
|
17
|
+
readonly subject: string;
|
|
18
|
+
readonly clientId: string;
|
|
19
|
+
readonly expiresAt: number;
|
|
20
|
+
readonly issuedAt: number;
|
|
21
|
+
readonly notBefore?: number;
|
|
22
|
+
readonly organizationId?: string;
|
|
23
|
+
readonly sessionId?: string;
|
|
24
|
+
readonly role?: string;
|
|
25
|
+
readonly permissions: readonly string[];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface WorkosM2mClaims {
|
|
29
|
+
readonly kind: "m2m";
|
|
30
|
+
readonly issuer: string;
|
|
31
|
+
readonly audience: readonly string[];
|
|
32
|
+
readonly subject: string;
|
|
33
|
+
readonly clientId: string;
|
|
34
|
+
readonly organizationId: string;
|
|
35
|
+
readonly expiresAt: number;
|
|
36
|
+
readonly issuedAt: number;
|
|
37
|
+
readonly notBefore?: number;
|
|
38
|
+
readonly sessionId?: string;
|
|
39
|
+
readonly tokenId?: string;
|
|
40
|
+
readonly scopes: readonly string[];
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export type WorkosTokenClaims = WorkosSessionClaims | WorkosM2mClaims;
|
|
44
|
+
|
|
45
|
+
export interface WorkosRsaJwk {
|
|
46
|
+
readonly kty: "RSA";
|
|
47
|
+
readonly kid: string;
|
|
48
|
+
readonly alg: "RS256";
|
|
49
|
+
readonly use: "sig";
|
|
50
|
+
readonly n: string;
|
|
51
|
+
readonly e: string;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const parseAudience = (value: unknown): readonly string[] => {
|
|
55
|
+
if (typeof value === "string" && value.length > 0) return [value];
|
|
56
|
+
if (
|
|
57
|
+
Array.isArray(value) &&
|
|
58
|
+
value.length > 0 &&
|
|
59
|
+
value.every((item) => typeof item === "string" && item.length > 0)
|
|
60
|
+
) {
|
|
61
|
+
return [...new Set(value)];
|
|
62
|
+
}
|
|
63
|
+
throw new WorkosContractError("invalid_claim", "Token audience is invalid");
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
const parseScopes = (value: unknown): readonly string[] => {
|
|
67
|
+
if (value === undefined) return [];
|
|
68
|
+
if (typeof value === "string") return [...new Set(value.split(" ").filter(Boolean))].sort();
|
|
69
|
+
if (
|
|
70
|
+
Array.isArray(value) &&
|
|
71
|
+
value.every((scope) => typeof scope === "string" && scope.length > 0)
|
|
72
|
+
) {
|
|
73
|
+
return [...new Set(value)].sort();
|
|
74
|
+
}
|
|
75
|
+
throw new WorkosContractError("invalid_claim", "Token scopes are invalid");
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
const parseEpoch = (value: unknown, field: string): number => expectPositiveInteger(value, field);
|
|
79
|
+
|
|
80
|
+
const decodeCanonicalBase64Url = (value: unknown, field: string): Uint8Array => {
|
|
81
|
+
const encoded = expectString(value, field);
|
|
82
|
+
if (!/^[A-Za-z0-9_-]+$/u.test(encoded) || encoded.length % 4 === 1) {
|
|
83
|
+
throw new WorkosContractError("invalid_jwk", `JWK field ${field} is invalid`);
|
|
84
|
+
}
|
|
85
|
+
try {
|
|
86
|
+
const normalized = encoded.replaceAll("-", "+").replaceAll("_", "/");
|
|
87
|
+
const binary = globalThis.atob(normalized.padEnd(Math.ceil(normalized.length / 4) * 4, "="));
|
|
88
|
+
const canonical = globalThis
|
|
89
|
+
.btoa(binary)
|
|
90
|
+
.replaceAll("+", "-")
|
|
91
|
+
.replaceAll("/", "_")
|
|
92
|
+
.replace(/=+$/u, "");
|
|
93
|
+
if (canonical !== encoded) throw new Error("Non-canonical base64url");
|
|
94
|
+
return Uint8Array.from(binary, (character) => character.charCodeAt(0));
|
|
95
|
+
} catch {
|
|
96
|
+
throw new WorkosContractError("invalid_jwk", `JWK field ${field} is invalid`);
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
const validateRsaMaterial = (modulus: unknown, exponent: unknown) => {
|
|
101
|
+
const n = decodeCanonicalBase64Url(modulus, "n");
|
|
102
|
+
const e = decodeCanonicalBase64Url(exponent, "e");
|
|
103
|
+
if (
|
|
104
|
+
n.length < 256 ||
|
|
105
|
+
(n.length === 256 && (n[0] ?? 0) < 0x80) ||
|
|
106
|
+
((n[n.length - 1] ?? 0) & 1) === 0
|
|
107
|
+
) {
|
|
108
|
+
throw new WorkosContractError("invalid_jwk", "JWK RSA modulus is too short");
|
|
109
|
+
}
|
|
110
|
+
if (e.length === 0 || e.length > 4) {
|
|
111
|
+
throw new WorkosContractError("invalid_jwk", "JWK RSA exponent is invalid");
|
|
112
|
+
}
|
|
113
|
+
const exponentValue = e.reduce((value, byte) => value * 256 + byte, 0);
|
|
114
|
+
if (!Number.isSafeInteger(exponentValue) || exponentValue < 3 || exponentValue % 2 === 0) {
|
|
115
|
+
throw new WorkosContractError("invalid_jwk", "JWK RSA exponent is invalid");
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
export const parseWorkosJwtHeader = (value: unknown): WorkosJwtHeader => {
|
|
120
|
+
const input = expectRecord(value, "invalid_jwt_header");
|
|
121
|
+
if (input["alg"] !== "RS256") {
|
|
122
|
+
throw new WorkosContractError("unsupported_algorithm", "Token algorithm is not allowed");
|
|
123
|
+
}
|
|
124
|
+
const typ = expectString(input["typ"], "typ", { optional: true });
|
|
125
|
+
return {
|
|
126
|
+
alg: "RS256",
|
|
127
|
+
kid: expectString(input["kid"], "kid"),
|
|
128
|
+
...(typ === undefined ? {} : { typ }),
|
|
129
|
+
};
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
export const parseWorkosTokenClaims = (
|
|
133
|
+
value: unknown,
|
|
134
|
+
kind: "session" | "m2m",
|
|
135
|
+
): WorkosTokenClaims => {
|
|
136
|
+
const input = expectRecord(value, "invalid_jwt_claims");
|
|
137
|
+
const common = {
|
|
138
|
+
issuer: expectString(input["iss"], "iss"),
|
|
139
|
+
subject: expectString(input["sub"], "sub"),
|
|
140
|
+
expiresAt: parseEpoch(input["exp"], "exp"),
|
|
141
|
+
issuedAt: parseEpoch(input["iat"], "iat"),
|
|
142
|
+
};
|
|
143
|
+
const notBefore = input["nbf"] === undefined ? undefined : parseEpoch(input["nbf"], "nbf");
|
|
144
|
+
if (kind === "m2m") {
|
|
145
|
+
if (!common.subject.startsWith("client_")) {
|
|
146
|
+
throw new WorkosContractError(
|
|
147
|
+
"invalid_principal_kind",
|
|
148
|
+
"Connect M2M token subject must identify a client",
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
const organizationId = expectString(input["org_id"], "org_id");
|
|
152
|
+
const sessionId = expectString(input["sid"], "sid", { optional: true });
|
|
153
|
+
const tokenId = expectString(input["jti"], "jti", { optional: true });
|
|
154
|
+
return {
|
|
155
|
+
kind,
|
|
156
|
+
...common,
|
|
157
|
+
audience: parseAudience(input["aud"]),
|
|
158
|
+
clientId: common.subject,
|
|
159
|
+
organizationId,
|
|
160
|
+
...(notBefore === undefined ? {} : { notBefore }),
|
|
161
|
+
...(sessionId === undefined ? {} : { sessionId }),
|
|
162
|
+
...(tokenId === undefined ? {} : { tokenId }),
|
|
163
|
+
scopes: parseScopes(input["scope"]),
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
if (!common.subject.startsWith("user_")) {
|
|
167
|
+
throw new WorkosContractError(
|
|
168
|
+
"invalid_principal_kind",
|
|
169
|
+
"Session token subject must identify a user",
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
const clientId = expectString(input["client_id"], "client_id");
|
|
173
|
+
const organizationId = expectString(input["org_id"], "org_id", {
|
|
174
|
+
optional: true,
|
|
175
|
+
});
|
|
176
|
+
const sessionId = expectString(input["sid"], "sid", { optional: true });
|
|
177
|
+
const role = expectString(input["role"], "role", { optional: true });
|
|
178
|
+
const permissions = parseScopes(input["permissions"]);
|
|
179
|
+
return {
|
|
180
|
+
kind,
|
|
181
|
+
...common,
|
|
182
|
+
...(notBefore === undefined ? {} : { notBefore }),
|
|
183
|
+
clientId,
|
|
184
|
+
...(organizationId === undefined ? {} : { organizationId }),
|
|
185
|
+
...(sessionId === undefined ? {} : { sessionId }),
|
|
186
|
+
...(role === undefined ? {} : { role }),
|
|
187
|
+
permissions,
|
|
188
|
+
};
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
export const parseWorkosJwks = (value: unknown): readonly WorkosRsaJwk[] => {
|
|
192
|
+
const input = expectRecord(value, "invalid_jwks");
|
|
193
|
+
if (!Array.isArray(input["keys"]) || input["keys"].length === 0) {
|
|
194
|
+
throw new WorkosContractError("invalid_jwks", "JWKS must contain signing keys");
|
|
195
|
+
}
|
|
196
|
+
const seen = new Set<string>();
|
|
197
|
+
return input["keys"].map((candidate) => {
|
|
198
|
+
const key = expectRecord(candidate, "invalid_jwk");
|
|
199
|
+
if (key["kty"] !== "RSA" || key["alg"] !== "RS256" || key["use"] !== "sig") {
|
|
200
|
+
throw new WorkosContractError("invalid_jwk", "JWKS contains an unsupported signing key");
|
|
201
|
+
}
|
|
202
|
+
const kid = expectString(key["kid"], "kid");
|
|
203
|
+
if (seen.has(kid))
|
|
204
|
+
throw new WorkosContractError("duplicate_jwk", "JWKS key IDs must be unique");
|
|
205
|
+
seen.add(kid);
|
|
206
|
+
validateRsaMaterial(key["n"], key["e"]);
|
|
207
|
+
return {
|
|
208
|
+
kty: "RSA",
|
|
209
|
+
kid,
|
|
210
|
+
alg: "RS256",
|
|
211
|
+
use: "sig",
|
|
212
|
+
n: expectString(key["n"], "n"),
|
|
213
|
+
e: expectString(key["e"], "e"),
|
|
214
|
+
};
|
|
215
|
+
});
|
|
216
|
+
};
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { expectRecord, parseIsoDate, WorkosContractError } from "./parsing.js";
|
|
2
|
+
|
|
3
|
+
export const WORKOS_USER_API_KEY_PATTERN = /^sk_[A-Za-z0-9_-]{20,128}$/u;
|
|
4
|
+
|
|
5
|
+
export interface WorkosUserApiKeyMetadata {
|
|
6
|
+
readonly id: string;
|
|
7
|
+
readonly userId: string;
|
|
8
|
+
readonly organizationId: string;
|
|
9
|
+
readonly permissions: readonly string[];
|
|
10
|
+
readonly expiresAt: string | null;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface WorkosUserApiKeyObservationRequest {
|
|
14
|
+
readonly keyId: string;
|
|
15
|
+
readonly userId: string;
|
|
16
|
+
readonly organizationId: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function parseWorkosUserApiKeyObservationRequest(
|
|
20
|
+
value: unknown,
|
|
21
|
+
): WorkosUserApiKeyObservationRequest {
|
|
22
|
+
const input = expectRecord(value, "invalid_api_key_request");
|
|
23
|
+
if (
|
|
24
|
+
Object.keys(input).sort().join(",") !== "keyId,organizationId,userId" ||
|
|
25
|
+
typeof input["keyId"] !== "string" ||
|
|
26
|
+
!/^api_key_[A-Za-z0-9_]{1,120}$/u.test(input["keyId"]) ||
|
|
27
|
+
typeof input["userId"] !== "string" ||
|
|
28
|
+
!/^user_[A-Za-z0-9_]{1,123}$/u.test(input["userId"]) ||
|
|
29
|
+
typeof input["organizationId"] !== "string" ||
|
|
30
|
+
!/^org_[A-Za-z0-9_]{1,124}$/u.test(input["organizationId"])
|
|
31
|
+
)
|
|
32
|
+
throw new WorkosContractError("invalid_api_key_request", "Invalid API key observation");
|
|
33
|
+
return {
|
|
34
|
+
keyId: input["keyId"],
|
|
35
|
+
userId: input["userId"],
|
|
36
|
+
organizationId: input["organizationId"],
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function parseWorkosUserApiKeyRequest(value: unknown): { value: string } {
|
|
41
|
+
const input = expectRecord(value, "invalid_api_key_request");
|
|
42
|
+
if (
|
|
43
|
+
Object.keys(input).join(",") !== "value" ||
|
|
44
|
+
typeof input["value"] !== "string" ||
|
|
45
|
+
!WORKOS_USER_API_KEY_PATTERN.test(input["value"])
|
|
46
|
+
)
|
|
47
|
+
throw new WorkosContractError("invalid_api_key_request", "Invalid API key request");
|
|
48
|
+
return { value: input["value"] };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function parseWorkosUserApiKeyResponse(value: unknown): {
|
|
52
|
+
key: WorkosUserApiKeyMetadata | null;
|
|
53
|
+
} {
|
|
54
|
+
const input = expectRecord(value, "invalid_api_key_response");
|
|
55
|
+
if (Object.keys(input).join(",") !== "key")
|
|
56
|
+
throw new WorkosContractError("invalid_api_key_response", "Invalid API key response");
|
|
57
|
+
if (input["key"] === null) return { key: null };
|
|
58
|
+
const key = expectRecord(input["key"], "invalid_api_key_response");
|
|
59
|
+
if (
|
|
60
|
+
Object.keys(key).sort().join(",") !== "expiresAt,id,organizationId,permissions,userId" ||
|
|
61
|
+
typeof key["id"] !== "string" ||
|
|
62
|
+
!/^api_key_[A-Za-z0-9_]{1,120}$/u.test(key["id"]) ||
|
|
63
|
+
typeof key["userId"] !== "string" ||
|
|
64
|
+
!/^user_[A-Za-z0-9_]{1,123}$/u.test(key["userId"]) ||
|
|
65
|
+
typeof key["organizationId"] !== "string" ||
|
|
66
|
+
!/^org_[A-Za-z0-9_]{1,124}$/u.test(key["organizationId"]) ||
|
|
67
|
+
!Array.isArray(key["permissions"]) ||
|
|
68
|
+
key["permissions"].length > 100 ||
|
|
69
|
+
key["permissions"].some(
|
|
70
|
+
(permission: unknown) =>
|
|
71
|
+
typeof permission !== "string" || !/^[a-zA-Z0-9_.*:-]{1,128}$/u.test(permission),
|
|
72
|
+
) ||
|
|
73
|
+
new Set(key["permissions"]).size !== key["permissions"].length
|
|
74
|
+
)
|
|
75
|
+
throw new WorkosContractError("invalid_api_key_response", "Invalid API key response");
|
|
76
|
+
return {
|
|
77
|
+
key: {
|
|
78
|
+
id: key["id"],
|
|
79
|
+
userId: key["userId"],
|
|
80
|
+
organizationId: key["organizationId"],
|
|
81
|
+
permissions: key["permissions"] as string[],
|
|
82
|
+
expiresAt: key["expiresAt"] === null ? null : parseIsoDate(key["expiresAt"], "expiresAt"),
|
|
83
|
+
},
|
|
84
|
+
};
|
|
85
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { expectRecord, expectString, parseJsonObject, WorkosContractError } from "./parsing.js";
|
|
2
|
+
|
|
3
|
+
export interface WorkosWebhookSignature {
|
|
4
|
+
readonly issuedAtMs: number;
|
|
5
|
+
readonly signatures: readonly string[];
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export interface WorkosWebhookEnvelope {
|
|
9
|
+
readonly id: string;
|
|
10
|
+
readonly event: string;
|
|
11
|
+
readonly createdAt: string;
|
|
12
|
+
readonly data: Readonly<Record<string, unknown>>;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface VerifyWorkosWebhookInput {
|
|
16
|
+
readonly rawBody: Uint8Array;
|
|
17
|
+
readonly signatureHeader: string;
|
|
18
|
+
readonly secret: string;
|
|
19
|
+
readonly now: Date;
|
|
20
|
+
readonly toleranceMs: number;
|
|
21
|
+
readonly maximumBodyBytes: number;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export const parseWorkosWebhookSignature = (header: string): WorkosWebhookSignature => {
|
|
25
|
+
let issuedAtMs: number | undefined;
|
|
26
|
+
const signatures: string[] = [];
|
|
27
|
+
for (const rawPart of header.split(",")) {
|
|
28
|
+
const [rawName, rawValue, ...rest] = rawPart.trim().split("=");
|
|
29
|
+
if (rest.length > 0 || rawName === undefined || rawValue === undefined) continue;
|
|
30
|
+
if (rawName === "t") {
|
|
31
|
+
if (issuedAtMs !== undefined || !/^\d+$/u.test(rawValue)) {
|
|
32
|
+
throw new WorkosContractError("invalid_signature_header", "Webhook timestamp is invalid");
|
|
33
|
+
}
|
|
34
|
+
issuedAtMs = Number(rawValue);
|
|
35
|
+
} else if (rawName === "v1" && /^[a-f0-9]{64}$/iu.test(rawValue)) {
|
|
36
|
+
signatures.push(rawValue.toLowerCase());
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
if (issuedAtMs === undefined || !Number.isSafeInteger(issuedAtMs) || signatures.length === 0) {
|
|
40
|
+
throw new WorkosContractError(
|
|
41
|
+
"invalid_signature_header",
|
|
42
|
+
"Webhook signature header is invalid",
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
return { issuedAtMs, signatures };
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
export const parseWorkosWebhookEnvelope = (value: unknown): WorkosWebhookEnvelope => {
|
|
49
|
+
const input = expectRecord(value, "invalid_webhook_envelope");
|
|
50
|
+
const event = expectString(input["event"], "event");
|
|
51
|
+
if (!/^[a-z][a-z0-9_]*(?:\.[a-z][a-z0-9_]*)+$/u.test(event)) {
|
|
52
|
+
throw new WorkosContractError("invalid_webhook_event", "Webhook event type is invalid");
|
|
53
|
+
}
|
|
54
|
+
const createdAt = expectString(input["created_at"], "created_at");
|
|
55
|
+
if (!Number.isFinite(Date.parse(createdAt))) {
|
|
56
|
+
throw new WorkosContractError("invalid_webhook_envelope", "Webhook creation time is invalid");
|
|
57
|
+
}
|
|
58
|
+
return {
|
|
59
|
+
id: expectString(input["id"], "id"),
|
|
60
|
+
event,
|
|
61
|
+
createdAt: new Date(createdAt).toISOString(),
|
|
62
|
+
data: expectRecord(input["data"], "invalid_webhook_data"),
|
|
63
|
+
};
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
export const verifyAndParseWorkosWebhook = async (
|
|
67
|
+
input: VerifyWorkosWebhookInput,
|
|
68
|
+
): Promise<WorkosWebhookEnvelope> => {
|
|
69
|
+
if (input.secret.length === 0) {
|
|
70
|
+
throw new WorkosContractError("invalid_webhook_secret", "Webhook secret is invalid");
|
|
71
|
+
}
|
|
72
|
+
if (
|
|
73
|
+
!Number.isSafeInteger(input.maximumBodyBytes) ||
|
|
74
|
+
input.maximumBodyBytes <= 0 ||
|
|
75
|
+
input.rawBody.byteLength > input.maximumBodyBytes
|
|
76
|
+
) {
|
|
77
|
+
throw new WorkosContractError("webhook_body_too_large", "Webhook body exceeds its limit");
|
|
78
|
+
}
|
|
79
|
+
if (!Number.isSafeInteger(input.toleranceMs) || input.toleranceMs < 0) {
|
|
80
|
+
throw new WorkosContractError("invalid_tolerance", "Webhook tolerance is invalid");
|
|
81
|
+
}
|
|
82
|
+
const signature = parseWorkosWebhookSignature(input.signatureHeader);
|
|
83
|
+
const nowMs = input.now.getTime();
|
|
84
|
+
if (!Number.isFinite(nowMs)) {
|
|
85
|
+
throw new WorkosContractError("invalid_clock", "Webhook verification clock is invalid");
|
|
86
|
+
}
|
|
87
|
+
if (Math.abs(nowMs - signature.issuedAtMs) > input.toleranceMs) {
|
|
88
|
+
throw new WorkosContractError(
|
|
89
|
+
"webhook_timestamp_outside_tolerance",
|
|
90
|
+
"Webhook timestamp is stale",
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
const prefix = new TextEncoder().encode(`${signature.issuedAtMs}.`);
|
|
94
|
+
const message = new Uint8Array(prefix.byteLength + input.rawBody.byteLength);
|
|
95
|
+
message.set(prefix);
|
|
96
|
+
message.set(input.rawBody, prefix.byteLength);
|
|
97
|
+
const importKey = async () => {
|
|
98
|
+
try {
|
|
99
|
+
return await globalThis.crypto.subtle.importKey(
|
|
100
|
+
"raw",
|
|
101
|
+
new TextEncoder().encode(input.secret),
|
|
102
|
+
{ name: "HMAC", hash: "SHA-256" },
|
|
103
|
+
false,
|
|
104
|
+
["verify"],
|
|
105
|
+
);
|
|
106
|
+
} catch {
|
|
107
|
+
throw new WorkosContractError("invalid_webhook_secret", "Webhook secret is invalid");
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
const key = await importKey();
|
|
111
|
+
let valid = false;
|
|
112
|
+
for (const expectedHexDigest of signature.signatures) {
|
|
113
|
+
const expected = Uint8Array.from(expectedHexDigest.match(/.{2}/gu) ?? [], (byte) =>
|
|
114
|
+
Number.parseInt(byte, 16),
|
|
115
|
+
);
|
|
116
|
+
try {
|
|
117
|
+
valid = (await globalThis.crypto.subtle.verify("HMAC", key, expected, message)) || valid;
|
|
118
|
+
} catch {
|
|
119
|
+
// Only a verified signature can change the accumulated result.
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
if (!valid)
|
|
123
|
+
throw new WorkosContractError("invalid_webhook_signature", "Webhook signature is invalid");
|
|
124
|
+
return parseWorkosWebhookEnvelope(
|
|
125
|
+
parseJsonObject(new TextDecoder("utf-8", { fatal: true }).decode(input.rawBody)),
|
|
126
|
+
);
|
|
127
|
+
};
|