@neutrome/platform-contracts 0.1.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/package.json ADDED
@@ -0,0 +1,20 @@
1
+ {
2
+ "name": "@neutrome/platform-contracts",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "exports": {
6
+ ".": "./src/index.ts"
7
+ },
8
+ "dependencies": {
9
+ "jose": "^6.2.0",
10
+ "zod": "^4.4.3"
11
+ },
12
+ "devDependencies": {
13
+ "typescript": "^6.0.3",
14
+ "vitest": "^4.1.6"
15
+ },
16
+ "scripts": {
17
+ "test": "vitest run",
18
+ "typecheck": "tsc -p tsconfig.json --noEmit"
19
+ }
20
+ }
package/src/auth.ts ADDED
@@ -0,0 +1,24 @@
1
+ import { z } from "zod";
2
+
3
+ const nonEmptyString = z.string().min(1);
4
+
5
+ export const PlatformSessionSchema = z
6
+ .object({
7
+ user: z
8
+ .object({
9
+ id: nonEmptyString,
10
+ email: nonEmptyString,
11
+ name: z.string().nullable().optional(),
12
+ })
13
+ .passthrough(),
14
+ session: z
15
+ .object({
16
+ id: nonEmptyString.optional(),
17
+ expiresAt: nonEmptyString.optional(),
18
+ })
19
+ .passthrough()
20
+ .nullable(),
21
+ })
22
+ .passthrough();
23
+
24
+ export type PlatformSession = z.infer<typeof PlatformSessionSchema>;
@@ -0,0 +1,51 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { BillingExecutionTraceSchema } from "./billing-trace.ts";
3
+
4
+ const trace = {
5
+ spanId: "span-1",
6
+ startedAt: "2026-07-12T12:00:00.000Z",
7
+ spans: [
8
+ {
9
+ kind: "provider",
10
+ event: "provider.finished",
11
+ requestId: "request-1",
12
+ executionId: "provider-1",
13
+ startedAt: "2026-07-12T12:00:00.000Z",
14
+ tokensInput: 10,
15
+ tokensOutput: 20,
16
+ cachedTokensInput: 2,
17
+ requestBytes: 100,
18
+ responseBytes: 200,
19
+ },
20
+ ],
21
+ totalTokensInput: 10,
22
+ totalTokensOutput: 20,
23
+ totalCachedTokensInput: 2,
24
+ };
25
+
26
+ describe("BillingExecutionTrace", () => {
27
+ it("accepts complete router trace data", () => {
28
+ expect(BillingExecutionTraceSchema.safeParse(trace).data).toEqual(trace);
29
+ });
30
+
31
+ it("rejects malformed spans and negative counters", () => {
32
+ expect(
33
+ BillingExecutionTraceSchema.safeParse({
34
+ ...trace,
35
+ spans: [{ ...trace.spans[0], kind: "other" }],
36
+ }).success,
37
+ ).toBe(false);
38
+ expect(
39
+ BillingExecutionTraceSchema.safeParse({
40
+ ...trace,
41
+ spans: [{ ...trace.spans[0], tokensOutput: -1 }],
42
+ }).success,
43
+ ).toBe(false);
44
+ expect(
45
+ BillingExecutionTraceSchema.safeParse({
46
+ ...trace,
47
+ totalTokensInput: "10",
48
+ }).success,
49
+ ).toBe(false);
50
+ });
51
+ });
@@ -0,0 +1,42 @@
1
+ import { z } from "zod";
2
+
3
+ const counter = z.number().finite().nonnegative();
4
+ const optionalText = z.string().min(1).optional();
5
+
6
+ export const BillingTraceSpanSchema = z.object({
7
+ kind: z.enum(["request", "provider", "tool", "executor"]),
8
+ event: z.string().min(1),
9
+ requestId: z.string().min(1),
10
+ executionId: z.string().min(1),
11
+ parentExecutionId: optionalText,
12
+ model: optionalText,
13
+ provider: optionalText,
14
+ toolName: optionalText,
15
+ executor: optionalText,
16
+ startedAt: z.string().min(1),
17
+ finishedAt: optionalText,
18
+ tokensInput: counter,
19
+ tokensOutput: counter,
20
+ cachedTokensInput: counter,
21
+ requestBytes: counter,
22
+ responseBytes: counter,
23
+ error: optionalText,
24
+ });
25
+
26
+ export const BillingExecutionTraceSchema = z.object({
27
+ spanId: z.string().min(1),
28
+ startedAt: z.string().min(1),
29
+ finishedAt: optionalText,
30
+ spans: z.array(BillingTraceSpanSchema),
31
+ totalTokensInput: counter,
32
+ totalTokensOutput: counter,
33
+ totalCachedTokensInput: counter,
34
+ error: optionalText,
35
+ });
36
+ export const BillingTraceRequestSchema = z.object({
37
+ workspaceId: z.string().min(1),
38
+ trace: BillingExecutionTraceSchema,
39
+ });
40
+
41
+ export type BillingTraceSpan = z.infer<typeof BillingTraceSpanSchema>;
42
+ export type BillingExecutionTrace = z.infer<typeof BillingExecutionTraceSchema>;
package/src/catalog.ts ADDED
@@ -0,0 +1,71 @@
1
+ import { z } from "zod";
2
+
3
+ const modelPattern = z
4
+ .string()
5
+ .trim()
6
+ .regex(/^[^/]+(?:\/[^/]+)+$/, "Expected a slash-separated model path");
7
+ const cost = z.number().finite().int().nonnegative();
8
+ const sourceExtension = z.enum([".js", ".ts"]);
9
+ const galleryKind = z.enum(["model", "tool", "transform"]);
10
+
11
+ export const CreateModelPricingRequestSchema = z.object({
12
+ modelPattern,
13
+ inputCostMicrosPerM: cost,
14
+ outputCostMicrosPerM: cost,
15
+ cachedInputCostMicrosPerM: cost,
16
+ });
17
+ export const UpdateModelPricingRequestSchema =
18
+ CreateModelPricingRequestSchema.partial()
19
+ .refine((input) => Object.keys(input).length > 0)
20
+ .transform((input) => ({
21
+ ...(input.modelPattern === undefined
22
+ ? {}
23
+ : { modelPattern: input.modelPattern }),
24
+ ...(input.inputCostMicrosPerM === undefined
25
+ ? {}
26
+ : { inputCostMicrosPerM: input.inputCostMicrosPerM }),
27
+ ...(input.outputCostMicrosPerM === undefined
28
+ ? {}
29
+ : { outputCostMicrosPerM: input.outputCostMicrosPerM }),
30
+ ...(input.cachedInputCostMicrosPerM === undefined
31
+ ? {}
32
+ : { cachedInputCostMicrosPerM: input.cachedInputCostMicrosPerM }),
33
+ }));
34
+
35
+ export const TemplateGalleryKindSchema = galleryKind;
36
+ export const CreateTemplateGalleryRequestSchema = z.object({
37
+ kind: galleryKind,
38
+ name: z.string().trim().min(1),
39
+ description: z.string().trim(),
40
+ sourceCode: z.string().min(1),
41
+ sourceExtension,
42
+ });
43
+ export const UpdateTemplateGalleryRequestSchema =
44
+ CreateTemplateGalleryRequestSchema.partial()
45
+ .refine((input) => Object.keys(input).length > 0)
46
+ .transform((input) => ({
47
+ ...(input.kind === undefined ? {} : { kind: input.kind }),
48
+ ...(input.name === undefined ? {} : { name: input.name }),
49
+ ...(input.description === undefined
50
+ ? {}
51
+ : { description: input.description }),
52
+ ...(input.sourceCode === undefined
53
+ ? {}
54
+ : { sourceCode: input.sourceCode }),
55
+ ...(input.sourceExtension === undefined
56
+ ? {}
57
+ : { sourceExtension: input.sourceExtension }),
58
+ }));
59
+
60
+ export type CreateModelPricingRequest = z.infer<
61
+ typeof CreateModelPricingRequestSchema
62
+ >;
63
+ export type UpdateModelPricingRequest = z.infer<
64
+ typeof UpdateModelPricingRequestSchema
65
+ >;
66
+ export type CreateTemplateGalleryRequest = z.infer<
67
+ typeof CreateTemplateGalleryRequestSchema
68
+ >;
69
+ export type UpdateTemplateGalleryRequest = z.infer<
70
+ typeof UpdateTemplateGalleryRequestSchema
71
+ >;
package/src/index.ts ADDED
@@ -0,0 +1,164 @@
1
+ export { PlatformSessionSchema } from "./auth.ts";
2
+ export type { PlatformSession } from "./auth.ts";
3
+ export {
4
+ createRuntimeJwtIssuer,
5
+ createRuntimeJwtAuthenticator,
6
+ RUNTIME_JWT_AUDIENCE,
7
+ RUNTIME_JWT_ISSUER,
8
+ RuntimeJwtClaimsSchema,
9
+ RuntimeJwtIssueInputSchema,
10
+ verifyRuntimeJwt,
11
+ } from "./runtime-jwt.ts";
12
+ export type {
13
+ RuntimeJwtClaims,
14
+ RuntimeJwtAuthenticator,
15
+ RuntimeJwtAuthConfig,
16
+ RuntimeJwtIssueInput,
17
+ RuntimeJwtIssuer,
18
+ RuntimeJwtVerificationConfig,
19
+ } from "./runtime-jwt.ts";
20
+ export {
21
+ BillingExecutionTraceSchema,
22
+ BillingTraceRequestSchema,
23
+ BillingTraceSpanSchema,
24
+ } from "./billing-trace.ts";
25
+ export type {
26
+ BillingExecutionTrace,
27
+ BillingTraceSpan,
28
+ } from "./billing-trace.ts";
29
+ export {
30
+ CreateWorkspaceInputSchema,
31
+ UpdateWorkspaceInputSchema,
32
+ } from "./workspace.ts";
33
+ export type {
34
+ CreateWorkspaceInput,
35
+ UpdateWorkspaceInput,
36
+ } from "./workspace.ts";
37
+ export {
38
+ CreateProviderNamespaceInputSchema,
39
+ UpdateProviderNamespaceInputSchema,
40
+ } from "./namespace.ts";
41
+ export type {
42
+ CreateProviderNamespaceInput,
43
+ UpdateProviderNamespaceInput,
44
+ } from "./namespace.ts";
45
+ export {
46
+ CreateModelRequestSchema,
47
+ CreateRemoteToolRequestSchema,
48
+ CreateToolRequestSchema,
49
+ CreateTransformRequestSchema,
50
+ RemoteMcpToolSnapshotSchema,
51
+ UpdateModelRequestSchema,
52
+ UpdateRemoteToolRequestSchema,
53
+ UpdateToolRequestSchema,
54
+ UpdateTransformRequestSchema,
55
+ UpsertModelSourceRequestSchema,
56
+ } from "./resources.ts";
57
+ export {
58
+ BalanceCheckSchema,
59
+ BillingLogSchema,
60
+ BillingSummarySchema,
61
+ BillingTransactionSchema,
62
+ CreatePlatformApiKeyResultSchema,
63
+ CheckoutSessionSchema,
64
+ DeployStatusSchema,
65
+ EmptyResponseSchema,
66
+ ModelPricingSchema,
67
+ ModelSchema,
68
+ OpenAiDeviceAuthStartSchema,
69
+ OpenAiDevicePollSchema,
70
+ PlatformApiKeySchema,
71
+ ProviderNamespaceSchema,
72
+ RemoteToolSchema,
73
+ TemplateGalleryPageSchema,
74
+ TemplateGallerySchema,
75
+ ToolSchema,
76
+ TransformSchema,
77
+ TriggerDeployResultSchema,
78
+ UpstreamProviderSchema,
79
+ WorkspaceCustomDomainSchema,
80
+ WorkspaceSchema,
81
+ WorkspaceStatsSchema,
82
+ WorkspacePermissionsSchema,
83
+ } from "./records.ts";
84
+ export type {
85
+ BillingLog,
86
+ BillingLogRecord,
87
+ BillingSummary,
88
+ BalanceCheckResult,
89
+ BillingTransaction,
90
+ BillingTransactionRecord,
91
+ CreatePlatformApiKeyResult,
92
+ DeployStatus,
93
+ Model,
94
+ ModelPricingRecord,
95
+ ModelRecord,
96
+ ModelPricing,
97
+ OpenAiDeviceAuthStart,
98
+ OpenAiDevicePoll,
99
+ PlatformApiKey,
100
+ PlatformApiKeyRecord,
101
+ ProviderNamespace,
102
+ ProviderNamespaceRecord,
103
+ RemoteTool,
104
+ RemoteToolRecord,
105
+ TemplateGallery,
106
+ TemplateGalleryKind,
107
+ TemplateGalleryPage,
108
+ TemplateGalleryRecord,
109
+ Tool,
110
+ ToolRecord,
111
+ TriggerDeployResult,
112
+ Transform,
113
+ TransformRecord,
114
+ UpstreamProvider,
115
+ UpstreamProviderRecord,
116
+ Workspace,
117
+ WorkspacePermissions,
118
+ WorkspaceRecord,
119
+ WorkspaceCustomDomainRecord,
120
+ ModelSourceExtension,
121
+ ModelStatus,
122
+ WorkspaceStats,
123
+ } from "./records.ts";
124
+ export {
125
+ CheckoutRequestSchema,
126
+ CreateApiKeyRequestSchema,
127
+ CreateUpstreamProviderRequestSchema,
128
+ CustomDomainRequestSchema,
129
+ OpenAiDevicePollRequestSchema,
130
+ OpenAiRefreshRequestSchema,
131
+ UpdateApiKeyRequestSchema,
132
+ UpdateUpstreamProviderRequestSchema,
133
+ } from "./management.ts";
134
+ export {
135
+ CreateModelPricingRequestSchema,
136
+ CreateTemplateGalleryRequestSchema,
137
+ TemplateGalleryKindSchema,
138
+ UpdateModelPricingRequestSchema,
139
+ UpdateTemplateGalleryRequestSchema,
140
+ } from "./catalog.ts";
141
+ export type {
142
+ CreateModelPricingRequest,
143
+ CreateTemplateGalleryRequest,
144
+ UpdateModelPricingRequest,
145
+ UpdateTemplateGalleryRequest,
146
+ } from "./catalog.ts";
147
+ export type {
148
+ CreateApiKeyRequest,
149
+ CreateUpstreamProviderRequest,
150
+ UpdateApiKeyRequest,
151
+ UpdateUpstreamProviderRequest,
152
+ } from "./management.ts";
153
+ export type {
154
+ CreateModelRequest,
155
+ CreateRemoteToolRequest,
156
+ CreateToolRequest,
157
+ CreateTransformRequest,
158
+ RemoteMcpToolSnapshot,
159
+ UpdateModelRequest,
160
+ UpdateRemoteToolRequest,
161
+ UpdateToolRequest,
162
+ UpdateTransformRequest,
163
+ UpsertModelSourceRequest,
164
+ } from "./resources.ts";
@@ -0,0 +1,98 @@
1
+ import { z } from "zod";
2
+
3
+ const nonEmptyString = z.string().min(1);
4
+ const nullableText = z.string().nullable();
5
+
6
+ export const CustomDomainRequestSchema = z.object({ hostname: nonEmptyString });
7
+
8
+ export const CreateApiKeyRequestSchema = z
9
+ .object({
10
+ displayName: nonEmptyString,
11
+ allowedModels: z.array(nonEmptyString).optional(),
12
+ allowedNamespaces: z.array(nonEmptyString).optional(),
13
+ expiresAt: z.string().optional(),
14
+ isDefault: z.boolean().optional(),
15
+ })
16
+ .transform((input) => ({
17
+ displayName: input.displayName,
18
+ ...(input.allowedModels === undefined
19
+ ? {}
20
+ : { allowedModels: input.allowedModels }),
21
+ ...(input.allowedNamespaces === undefined
22
+ ? {}
23
+ : { allowedNamespaces: input.allowedNamespaces }),
24
+ ...(input.expiresAt === undefined ? {} : { expiresAt: input.expiresAt }),
25
+ ...(input.isDefault === undefined ? {} : { isDefault: input.isDefault }),
26
+ }));
27
+ export const UpdateApiKeyRequestSchema = z
28
+ .object({ displayName: nonEmptyString })
29
+ .partial()
30
+ .refine((input) => Object.keys(input).length > 0)
31
+ .transform((input) =>
32
+ input.displayName === undefined ? {} : { displayName: input.displayName },
33
+ );
34
+
35
+ const upstreamProviderFields = z.object({
36
+ displayName: nonEmptyString,
37
+ apiBaseUrl: nullableText.optional(),
38
+ style: nullableText.optional(),
39
+ apiKey: nullableText.optional(),
40
+ oauthAccessKey: nullableText.optional(),
41
+ oauthRefreshToken: nullableText.optional(),
42
+ oauthExpiresAt: nullableText.optional(),
43
+ });
44
+ const partialUpstreamProviderFields = upstreamProviderFields.partial();
45
+ const upstreamProviderOutput = (
46
+ input: z.input<typeof partialUpstreamProviderFields>,
47
+ ) => ({
48
+ ...(input.displayName === undefined
49
+ ? {}
50
+ : { displayName: input.displayName }),
51
+ ...(input.apiBaseUrl === undefined ? {} : { apiBaseUrl: input.apiBaseUrl }),
52
+ ...(input.style === undefined ? {} : { style: input.style }),
53
+ ...(input.apiKey === undefined ? {} : { apiKey: input.apiKey }),
54
+ ...(input.oauthAccessKey === undefined
55
+ ? {}
56
+ : { oauthAccessKey: input.oauthAccessKey }),
57
+ ...(input.oauthRefreshToken === undefined
58
+ ? {}
59
+ : { oauthRefreshToken: input.oauthRefreshToken }),
60
+ ...(input.oauthExpiresAt === undefined
61
+ ? {}
62
+ : { oauthExpiresAt: input.oauthExpiresAt }),
63
+ });
64
+
65
+ export const CreateUpstreamProviderRequestSchema = upstreamProviderFields
66
+ .extend({
67
+ type: z.enum(["oauth_openai", "byok"]),
68
+ })
69
+ .transform((input) => ({
70
+ ...upstreamProviderOutput(input),
71
+ type: input.type,
72
+ displayName: input.displayName,
73
+ }));
74
+ export const UpdateUpstreamProviderRequestSchema = partialUpstreamProviderFields
75
+ .refine((input) => Object.keys(input).length > 0)
76
+ .transform(upstreamProviderOutput);
77
+
78
+ export const OpenAiDevicePollRequestSchema = z.object({
79
+ deviceAuthId: nonEmptyString,
80
+ userCode: nonEmptyString,
81
+ });
82
+ export const OpenAiRefreshRequestSchema = z.object({
83
+ providerId: nonEmptyString,
84
+ });
85
+ export const CheckoutRequestSchema = z.object({
86
+ amountCents: z.number().finite().int().positive(),
87
+ successUrl: z.string().url(),
88
+ cancelUrl: z.string().url(),
89
+ });
90
+
91
+ export type CreateApiKeyRequest = z.infer<typeof CreateApiKeyRequestSchema>;
92
+ export type UpdateApiKeyRequest = z.infer<typeof UpdateApiKeyRequestSchema>;
93
+ export type CreateUpstreamProviderRequest = z.infer<
94
+ typeof CreateUpstreamProviderRequestSchema
95
+ >;
96
+ export type UpdateUpstreamProviderRequest = z.infer<
97
+ typeof UpdateUpstreamProviderRequestSchema
98
+ >;
@@ -0,0 +1,17 @@
1
+ import { z } from "zod";
2
+
3
+ export const CreateProviderNamespaceInputSchema = z.object({
4
+ code: z.string().min(1),
5
+ });
6
+ export const UpdateProviderNamespaceInputSchema =
7
+ CreateProviderNamespaceInputSchema.partial()
8
+ .refine((input) => Object.keys(input).length > 0)
9
+ .transform((input): { code?: string } =>
10
+ input.code === undefined ? {} : { code: input.code },
11
+ );
12
+ export type CreateProviderNamespaceInput = z.infer<
13
+ typeof CreateProviderNamespaceInputSchema
14
+ >;
15
+ export type UpdateProviderNamespaceInput = z.infer<
16
+ typeof UpdateProviderNamespaceInputSchema
17
+ >;
package/src/records.ts ADDED
@@ -0,0 +1,297 @@
1
+ import { z } from "zod";
2
+ import { RemoteMcpToolSnapshotSchema } from "./resources.ts";
3
+
4
+ const identifier = z.string().min(1);
5
+ const timestamp = z.string().min(1);
6
+ const sourceExtension = z.enum([".js", ".ts"]);
7
+ const nullableText = z.string().nullable();
8
+
9
+ export const EmptyResponseSchema = z.null().transform(() => undefined);
10
+
11
+ export const WorkspaceCustomDomainSchema = z.object({
12
+ workspaceId: identifier,
13
+ hostname: identifier,
14
+ customHostnameId: identifier,
15
+ workerRouteId: nullableText,
16
+ status: z.string(),
17
+ sslStatus: nullableText,
18
+ verificationErrors: z.array(z.string()),
19
+ cnameTarget: z.string(),
20
+ ownershipTxtName: nullableText,
21
+ ownershipTxtValue: nullableText,
22
+ createdAt: timestamp,
23
+ updatedAt: timestamp,
24
+ lastCheckedAt: nullableText,
25
+ });
26
+
27
+ export const WorkspaceSchema = z.object({
28
+ id: identifier,
29
+ code: identifier,
30
+ displayName: identifier,
31
+ deployedUrl: nullableText,
32
+ generatedUrl: nullableText,
33
+ deployedAt: nullableText,
34
+ deployVersion: z.number().int(),
35
+ runtimeBindingsVersion: z.number().int(),
36
+ workerName: nullableText,
37
+ workerTag: nullableText,
38
+ repoConnectionUuid: nullableText,
39
+ buildTriggerUuid: nullableText,
40
+ customDomain: WorkspaceCustomDomainSchema.nullable(),
41
+ createdAt: timestamp,
42
+ updatedAt: timestamp,
43
+ });
44
+
45
+ export const ProviderNamespaceSchema = z.object({
46
+ id: identifier,
47
+ workspaceId: identifier,
48
+ code: identifier,
49
+ createdAt: timestamp,
50
+ updatedAt: timestamp,
51
+ });
52
+
53
+ export const ModelSchema = z.object({
54
+ id: identifier,
55
+ workspaceId: identifier,
56
+ providerNamespaceId: identifier,
57
+ providerNamespaceCode: identifier,
58
+ code: identifier,
59
+ sourceCode: z.string(),
60
+ sourceExtension,
61
+ status: z.enum(["draft", "deployed", "disabled"]),
62
+ deployedVersion: z.number().int(),
63
+ createdAt: timestamp,
64
+ updatedAt: timestamp,
65
+ });
66
+
67
+ export const ToolSchema = z.object({
68
+ id: identifier,
69
+ workspaceId: identifier,
70
+ displayName: identifier,
71
+ sourceCode: z.string(),
72
+ sourceExtension,
73
+ attachedModelIds: z.array(identifier),
74
+ deployedVersion: z.number().int(),
75
+ createdAt: timestamp,
76
+ updatedAt: timestamp,
77
+ });
78
+
79
+ export const TransformSchema = z.object({
80
+ id: identifier,
81
+ workspaceId: identifier,
82
+ code: identifier,
83
+ sourceCode: z.string(),
84
+ sourceExtension,
85
+ deployedVersion: z.number().int(),
86
+ createdAt: timestamp,
87
+ updatedAt: timestamp,
88
+ });
89
+
90
+ export const RemoteToolSchema = z.object({
91
+ id: identifier,
92
+ workspaceId: identifier,
93
+ code: identifier,
94
+ displayName: identifier,
95
+ type: z.literal("mcp_streamable_http"),
96
+ endpointUrl: z.string(),
97
+ staticHeaders: z.record(z.string(), z.string()),
98
+ oauthMode: z.literal("none"),
99
+ attachedModelIds: z.array(identifier),
100
+ discoveredTools: z.array(RemoteMcpToolSnapshotSchema),
101
+ lastRefreshedAt: nullableText,
102
+ lastRefreshError: nullableText,
103
+ deployedVersion: z.number().int(),
104
+ createdAt: timestamp,
105
+ updatedAt: timestamp,
106
+ });
107
+
108
+ export const PlatformApiKeySchema = z.object({
109
+ id: identifier,
110
+ workspaceId: identifier,
111
+ displayName: identifier,
112
+ allowedModels: z.array(identifier),
113
+ allowedNamespaces: z.array(identifier),
114
+ expiresAt: timestamp,
115
+ revokedAt: nullableText,
116
+ isDefault: z.boolean(),
117
+ createdAt: timestamp,
118
+ updatedAt: timestamp,
119
+ });
120
+
121
+ export const CreatePlatformApiKeyResultSchema = z.object({
122
+ key: PlatformApiKeySchema,
123
+ token: identifier,
124
+ });
125
+
126
+ export const WorkspaceStatsSchema = z.object({
127
+ namespaces: z.number().int().nonnegative(),
128
+ models: z.number().int().nonnegative(),
129
+ tools: z.number().int().nonnegative(),
130
+ transforms: z.number().int().nonnegative(),
131
+ remoteTools: z.number().int().nonnegative(),
132
+ apiKeys: z.number().int().nonnegative(),
133
+ });
134
+
135
+ export const WorkspacePermissionsSchema = z.object({
136
+ templateGalleryWrite: z.boolean(),
137
+ pricingWrite: z.boolean(),
138
+ });
139
+
140
+ export const TriggerDeployResultSchema = z.object({
141
+ workerName: identifier,
142
+ url: z.string().url(),
143
+ commitSha: identifier,
144
+ });
145
+
146
+ export const DeployStatusSchema = z.object({
147
+ status: z.enum([
148
+ "queued",
149
+ "initializing",
150
+ "building",
151
+ "success",
152
+ "failure",
153
+ "canceled",
154
+ ]),
155
+ url: nullableText,
156
+ buildUuid: nullableText,
157
+ });
158
+
159
+ export const CheckoutSessionSchema = z.object({
160
+ sessionId: identifier,
161
+ url: z.string().url(),
162
+ });
163
+
164
+ export const BillingLogSchema = z.object({
165
+ id: identifier,
166
+ workspaceId: identifier,
167
+ spanId: identifier,
168
+ providerCalls: z.number().int(),
169
+ toolCalls: z.number().int(),
170
+ tokensInput: z.number().int(),
171
+ tokensOutput: z.number().int(),
172
+ costMicros: z.number().int(),
173
+ errorMessage: nullableText,
174
+ createdAt: timestamp,
175
+ });
176
+
177
+ export const BillingSummarySchema = z.object({
178
+ totalRequests: z.number().int(),
179
+ totalTokensInput: z.number().int(),
180
+ totalTokensOutput: z.number().int(),
181
+ totalCostMicros: z.number().int(),
182
+ });
183
+
184
+ export const BillingTransactionSchema = z.object({
185
+ id: identifier,
186
+ workspaceId: identifier,
187
+ amountMicros: z.number().int(),
188
+ type: z.string(),
189
+ referenceId: nullableText,
190
+ description: nullableText,
191
+ createdAt: timestamp,
192
+ });
193
+
194
+ export const BalanceCheckSchema = z.object({
195
+ allowed: z.boolean(),
196
+ balanceMicros: z.number().int(),
197
+ });
198
+
199
+ export const ModelPricingSchema = z.object({
200
+ id: identifier,
201
+ modelPattern: identifier,
202
+ inputCostMicrosPerM: z.number().int(),
203
+ outputCostMicrosPerM: z.number().int(),
204
+ cachedInputCostMicrosPerM: z.number().int(),
205
+ createdAt: timestamp,
206
+ updatedAt: timestamp,
207
+ });
208
+
209
+ export const TemplateGallerySchema = z.object({
210
+ id: identifier,
211
+ kind: z.enum(["model", "tool", "transform"]),
212
+ name: identifier,
213
+ description: z.string(),
214
+ sourceCode: z.string(),
215
+ sourceExtension,
216
+ sortOrder: z.number().int(),
217
+ isEnabled: z.boolean(),
218
+ createdAt: timestamp,
219
+ updatedAt: timestamp,
220
+ });
221
+
222
+ export const TemplateGalleryPageSchema = z.object({
223
+ items: z.array(TemplateGallerySchema),
224
+ hasMore: z.boolean(),
225
+ nextOffset: z.number().int().nullable(),
226
+ });
227
+
228
+ export const UpstreamProviderSchema = z.object({
229
+ id: identifier,
230
+ workspaceId: identifier,
231
+ code: identifier,
232
+ displayName: identifier,
233
+ type: z.enum(["oauth_openai", "byok"]),
234
+ apiBaseUrl: nullableText,
235
+ style: nullableText,
236
+ apiKey: nullableText,
237
+ oauthAccessKey: nullableText,
238
+ oauthRefreshToken: nullableText,
239
+ oauthExpiresAt: nullableText,
240
+ createdAt: timestamp,
241
+ updatedAt: timestamp,
242
+ });
243
+
244
+ export const OpenAiDeviceAuthStartSchema = z.object({
245
+ deviceAuthId: identifier,
246
+ userCode: identifier,
247
+ verificationUrl: z.string().url(),
248
+ interval: z.number().int().positive(),
249
+ });
250
+
251
+ export const OpenAiDevicePollSchema = z.discriminatedUnion("status", [
252
+ z.object({ status: z.literal("pending") }),
253
+ z.object({ status: z.literal("complete"), provider: UpstreamProviderSchema }),
254
+ ]);
255
+
256
+ export type Workspace = z.infer<typeof WorkspaceSchema>;
257
+ export type WorkspaceRecord = Workspace;
258
+ export type WorkspaceCustomDomainRecord = z.infer<
259
+ typeof WorkspaceCustomDomainSchema
260
+ >;
261
+ export type ModelSourceExtension = z.infer<typeof sourceExtension>;
262
+ export type ModelStatus = z.infer<typeof ModelSchema>["status"];
263
+ export type ProviderNamespaceRecord = z.infer<typeof ProviderNamespaceSchema>;
264
+ export type ModelRecord = z.infer<typeof ModelSchema>;
265
+ export type ToolRecord = z.infer<typeof ToolSchema>;
266
+ export type TransformRecord = z.infer<typeof TransformSchema>;
267
+ export type RemoteToolRecord = z.infer<typeof RemoteToolSchema>;
268
+ export type PlatformApiKeyRecord = z.infer<typeof PlatformApiKeySchema>;
269
+ export type CreatePlatformApiKeyResult = z.infer<
270
+ typeof CreatePlatformApiKeyResultSchema
271
+ >;
272
+ export type WorkspaceStats = z.infer<typeof WorkspaceStatsSchema>;
273
+ export type WorkspacePermissions = z.infer<typeof WorkspacePermissionsSchema>;
274
+ export type ModelPricingRecord = z.infer<typeof ModelPricingSchema>;
275
+ export type TemplateGalleryRecord = z.infer<typeof TemplateGallerySchema>;
276
+ export type TemplateGalleryPage = z.infer<typeof TemplateGalleryPageSchema>;
277
+ export type TemplateGalleryKind = TemplateGalleryRecord["kind"];
278
+ export type TriggerDeployResult = z.infer<typeof TriggerDeployResultSchema>;
279
+ export type DeployStatus = z.infer<typeof DeployStatusSchema>;
280
+ export type OpenAiDeviceAuthStart = z.infer<typeof OpenAiDeviceAuthStartSchema>;
281
+ export type OpenAiDevicePoll = z.infer<typeof OpenAiDevicePollSchema>;
282
+ export type ProviderNamespace = z.infer<typeof ProviderNamespaceSchema>;
283
+ export type Model = z.infer<typeof ModelSchema>;
284
+ export type Tool = z.infer<typeof ToolSchema>;
285
+ export type Transform = z.infer<typeof TransformSchema>;
286
+ export type RemoteTool = z.infer<typeof RemoteToolSchema>;
287
+ export type PlatformApiKey = z.infer<typeof PlatformApiKeySchema>;
288
+ export type BillingLog = z.infer<typeof BillingLogSchema>;
289
+ export type BillingLogRecord = BillingLog;
290
+ export type BillingSummary = z.infer<typeof BillingSummarySchema>;
291
+ export type BillingTransaction = z.infer<typeof BillingTransactionSchema>;
292
+ export type BillingTransactionRecord = BillingTransaction;
293
+ export type BalanceCheckResult = z.infer<typeof BalanceCheckSchema>;
294
+ export type ModelPricing = z.infer<typeof ModelPricingSchema>;
295
+ export type TemplateGallery = z.infer<typeof TemplateGallerySchema>;
296
+ export type UpstreamProvider = z.infer<typeof UpstreamProviderSchema>;
297
+ export type UpstreamProviderRecord = UpstreamProvider;
@@ -0,0 +1,163 @@
1
+ import { z } from "zod";
2
+
3
+ const nonEmptyString = z.string().min(1);
4
+ const sourceExtension = z.enum([".js", ".ts"]);
5
+ const remoteToolType = z.literal("mcp_streamable_http");
6
+ const remoteToolOauthMode = z.literal("none");
7
+ const stringList = z.array(nonEmptyString);
8
+ const jsonRecord = z.record(z.string(), z.unknown());
9
+
10
+ export const CreateModelRequestSchema = z.object({
11
+ providerNamespaceId: nonEmptyString,
12
+ code: nonEmptyString,
13
+ sourceCode: z.string().optional(),
14
+ sourceExtension: sourceExtension.optional(),
15
+ });
16
+ export const UpdateModelRequestSchema = CreateModelRequestSchema.partial()
17
+ .extend({
18
+ status: z.enum(["draft", "deployed", "disabled"]).optional(),
19
+ })
20
+ .refine((input) => Object.keys(input).length > 0)
21
+ .transform((input) => ({
22
+ ...(input.providerNamespaceId === undefined
23
+ ? {}
24
+ : { providerNamespaceId: input.providerNamespaceId }),
25
+ ...(input.code === undefined ? {} : { code: input.code }),
26
+ ...(input.sourceCode === undefined ? {} : { sourceCode: input.sourceCode }),
27
+ ...(input.sourceExtension === undefined
28
+ ? {}
29
+ : { sourceExtension: input.sourceExtension }),
30
+ ...(input.status === undefined ? {} : { status: input.status }),
31
+ }));
32
+ export const UpsertModelSourceRequestSchema = z.object({
33
+ namespaceCode: nonEmptyString,
34
+ modelCode: nonEmptyString,
35
+ sourceCode: z.string().min(1),
36
+ sourceExtension,
37
+ });
38
+
39
+ export const CreateToolRequestSchema = z.object({
40
+ displayName: nonEmptyString,
41
+ sourceCode: z.string().optional(),
42
+ sourceExtension: sourceExtension.optional(),
43
+ attachedModelIds: stringList.optional(),
44
+ });
45
+ export const UpdateToolRequestSchema = CreateToolRequestSchema.partial()
46
+ .refine((input) => Object.keys(input).length > 0)
47
+ .transform((input) => ({
48
+ ...(input.displayName === undefined
49
+ ? {}
50
+ : { displayName: input.displayName }),
51
+ ...(input.sourceCode === undefined ? {} : { sourceCode: input.sourceCode }),
52
+ ...(input.sourceExtension === undefined
53
+ ? {}
54
+ : { sourceExtension: input.sourceExtension }),
55
+ ...(input.attachedModelIds === undefined
56
+ ? {}
57
+ : { attachedModelIds: input.attachedModelIds }),
58
+ }));
59
+
60
+ export const CreateTransformRequestSchema = z.object({
61
+ code: nonEmptyString,
62
+ sourceCode: z.string().optional(),
63
+ sourceExtension: sourceExtension.optional(),
64
+ });
65
+ export const UpdateTransformRequestSchema =
66
+ CreateTransformRequestSchema.partial()
67
+ .refine((input) => Object.keys(input).length > 0)
68
+ .transform((input) => ({
69
+ ...(input.code === undefined ? {} : { code: input.code }),
70
+ ...(input.sourceCode === undefined
71
+ ? {}
72
+ : { sourceCode: input.sourceCode }),
73
+ ...(input.sourceExtension === undefined
74
+ ? {}
75
+ : { sourceExtension: input.sourceExtension }),
76
+ }));
77
+
78
+ export const RemoteMcpToolSnapshotSchema = z.object({
79
+ name: nonEmptyString,
80
+ description: z.string(),
81
+ inputSchema: jsonRecord,
82
+ });
83
+ const remoteToolRequest = z.object({
84
+ code: nonEmptyString,
85
+ displayName: nonEmptyString,
86
+ type: remoteToolType,
87
+ endpointUrl: z.string().optional(),
88
+ staticHeaders: z.record(z.string(), z.string()).optional(),
89
+ oauthMode: remoteToolOauthMode.optional(),
90
+ attachedModelIds: stringList.optional(),
91
+ });
92
+ export const CreateRemoteToolRequestSchema = remoteToolRequest.transform(
93
+ (input) => ({
94
+ code: input.code,
95
+ displayName: input.displayName,
96
+ type: input.type,
97
+ ...(input.endpointUrl === undefined
98
+ ? {}
99
+ : { endpointUrl: input.endpointUrl }),
100
+ ...(input.staticHeaders === undefined
101
+ ? {}
102
+ : { staticHeaders: input.staticHeaders }),
103
+ ...(input.oauthMode === undefined ? {} : { oauthMode: input.oauthMode }),
104
+ ...(input.attachedModelIds === undefined
105
+ ? {}
106
+ : { attachedModelIds: input.attachedModelIds }),
107
+ }),
108
+ );
109
+ export const UpdateRemoteToolRequestSchema = remoteToolRequest
110
+ .partial()
111
+ .extend({
112
+ discoveredTools: z.array(RemoteMcpToolSnapshotSchema).optional(),
113
+ lastRefreshedAt: z.string().nullable().optional(),
114
+ lastRefreshError: z.string().nullable().optional(),
115
+ })
116
+ .refine((input) => Object.keys(input).length > 0)
117
+ .transform((input) => ({
118
+ ...(input.code === undefined ? {} : { code: input.code }),
119
+ ...(input.displayName === undefined
120
+ ? {}
121
+ : { displayName: input.displayName }),
122
+ ...(input.type === undefined ? {} : { type: input.type }),
123
+ ...(input.endpointUrl === undefined
124
+ ? {}
125
+ : { endpointUrl: input.endpointUrl }),
126
+ ...(input.staticHeaders === undefined
127
+ ? {}
128
+ : { staticHeaders: input.staticHeaders }),
129
+ ...(input.oauthMode === undefined ? {} : { oauthMode: input.oauthMode }),
130
+ ...(input.attachedModelIds === undefined
131
+ ? {}
132
+ : { attachedModelIds: input.attachedModelIds }),
133
+ ...(input.discoveredTools === undefined
134
+ ? {}
135
+ : { discoveredTools: input.discoveredTools }),
136
+ ...(input.lastRefreshedAt === undefined
137
+ ? {}
138
+ : { lastRefreshedAt: input.lastRefreshedAt }),
139
+ ...(input.lastRefreshError === undefined
140
+ ? {}
141
+ : { lastRefreshError: input.lastRefreshError }),
142
+ }));
143
+
144
+ export type CreateModelRequest = z.infer<typeof CreateModelRequestSchema>;
145
+ export type UpdateModelRequest = z.infer<typeof UpdateModelRequestSchema>;
146
+ export type UpsertModelSourceRequest = z.infer<
147
+ typeof UpsertModelSourceRequestSchema
148
+ >;
149
+ export type CreateToolRequest = z.infer<typeof CreateToolRequestSchema>;
150
+ export type UpdateToolRequest = z.infer<typeof UpdateToolRequestSchema>;
151
+ export type CreateTransformRequest = z.infer<
152
+ typeof CreateTransformRequestSchema
153
+ >;
154
+ export type UpdateTransformRequest = z.infer<
155
+ typeof UpdateTransformRequestSchema
156
+ >;
157
+ export type CreateRemoteToolRequest = z.infer<
158
+ typeof CreateRemoteToolRequestSchema
159
+ >;
160
+ export type UpdateRemoteToolRequest = z.infer<
161
+ typeof UpdateRemoteToolRequestSchema
162
+ >;
163
+ export type RemoteMcpToolSnapshot = z.infer<typeof RemoteMcpToolSnapshotSchema>;
@@ -0,0 +1,106 @@
1
+ import { exportJWK, generateKeyPair } from "jose";
2
+ import { describe, expect, it } from "vitest";
3
+ import {
4
+ createRuntimeJwtIssuer,
5
+ RuntimeJwtIssueInputSchema,
6
+ verifyRuntimeJwt,
7
+ } from "./runtime-jwt.ts";
8
+
9
+ describe("runtime JWT contracts", () => {
10
+ it("verifies valid claims and enforces workspace, audience, and revocation", async () => {
11
+ const issuer = await testIssuer("key-1");
12
+ const token = await issuer.issue({
13
+ workspaceId: "workspace-1",
14
+ keyId: "api-key-1",
15
+ expiresAt: futureDate(),
16
+ allowedModels: ["default/chat"],
17
+ allowedNamespaces: null,
18
+ });
19
+ const config = {
20
+ jwksJson: issuer.publicJwksJson(),
21
+ workspaceId: "workspace-1",
22
+ };
23
+
24
+ await expect(verifyRuntimeJwt(token, config)).resolves.toEqual({
25
+ workspaceId: "workspace-1",
26
+ keyId: "api-key-1",
27
+ allowedModels: ["default/chat"],
28
+ allowedNamespaces: null,
29
+ });
30
+ await expect(
31
+ verifyRuntimeJwt(token, { ...config, workspaceId: "other" }),
32
+ ).resolves.toBeNull();
33
+ await expect(
34
+ verifyRuntimeJwt(token, { ...config, audience: "other" }),
35
+ ).resolves.toBeNull();
36
+ await expect(
37
+ verifyRuntimeJwt(token, { ...config, revokedKeyIds: ["api-key-1"] }),
38
+ ).resolves.toBeNull();
39
+ });
40
+
41
+ it("rejects expired and malformed tokens", async () => {
42
+ const issuer = await testIssuer("key-1");
43
+ const expired = await issuer.issue({
44
+ workspaceId: "workspace-1",
45
+ keyId: "api-key-1",
46
+ expiresAt: new Date(Date.now() - 1_000).toISOString(),
47
+ allowedModels: null,
48
+ allowedNamespaces: null,
49
+ });
50
+ const config = { jwksJson: issuer.publicJwksJson() };
51
+
52
+ await expect(verifyRuntimeJwt(expired, config)).resolves.toBeNull();
53
+ await expect(verifyRuntimeJwt(`${expired}x`, config)).resolves.toBeNull();
54
+ });
55
+
56
+ it("accepts active keys during key rotation", async () => {
57
+ const previous = await testIssuer("old-key");
58
+ const current = await testIssuer("new-key");
59
+ const jwksJson = JSON.stringify({
60
+ keys: [
61
+ ...jwks(previous.publicJwksJson()),
62
+ ...jwks(current.publicJwksJson()),
63
+ ],
64
+ });
65
+ const input = {
66
+ workspaceId: "workspace-1",
67
+ keyId: "api-key-1",
68
+ expiresAt: futureDate(),
69
+ allowedModels: null,
70
+ allowedNamespaces: ["default"],
71
+ };
72
+
73
+ await expect(
74
+ verifyRuntimeJwt(await previous.issue(input), { jwksJson }),
75
+ ).resolves.not.toBeNull();
76
+ await expect(
77
+ verifyRuntimeJwt(await current.issue(input), { jwksJson }),
78
+ ).resolves.not.toBeNull();
79
+ });
80
+
81
+ it("rejects malformed issue input at the schema boundary", () => {
82
+ expect(
83
+ RuntimeJwtIssueInputSchema.safeParse({
84
+ workspaceId: "workspace-1",
85
+ keyId: "api-key-1",
86
+ expiresAt: "not-a-date",
87
+ allowedModels: ["default/chat", 1],
88
+ allowedNamespaces: null,
89
+ }).success,
90
+ ).toBe(false);
91
+ });
92
+ });
93
+
94
+ async function testIssuer(keyId: string) {
95
+ const pair = await generateKeyPair("ES256", { extractable: true });
96
+ const privateJwk = await exportJWK(pair.privateKey);
97
+ return createRuntimeJwtIssuer(JSON.stringify({ ...privateJwk, kid: keyId }));
98
+ }
99
+
100
+ function futureDate(): string {
101
+ return new Date(Date.now() + 60_000).toISOString();
102
+ }
103
+
104
+ function jwks(value: string): JsonWebKey[] {
105
+ return (JSON.parse(value) as { keys: JsonWebKey[] }).keys;
106
+ }
@@ -0,0 +1,201 @@
1
+ import { SignJWT, createLocalJWKSet, importJWK, jwtVerify } from "jose";
2
+ import { z } from "zod";
3
+
4
+ export const RUNTIME_JWT_ISSUER = "neutrome-platform";
5
+ export const RUNTIME_JWT_AUDIENCE = "workspace-runtime";
6
+
7
+ const nonEmptyString = z.string().min(1);
8
+ const nullableStringArray = z
9
+ .array(z.string())
10
+ .nullish()
11
+ .transform((value) => value ?? null);
12
+ const runtimeJwk = z
13
+ .object({
14
+ kty: z.literal("EC"),
15
+ crv: z.literal("P-256"),
16
+ x: nonEmptyString,
17
+ y: nonEmptyString,
18
+ })
19
+ .passthrough();
20
+ const parseJson = <T extends z.ZodType>(schema: T) =>
21
+ z
22
+ .string()
23
+ .transform((value, context) => {
24
+ try {
25
+ return JSON.parse(value);
26
+ } catch {
27
+ context.addIssue({ code: "custom", message: "Expected JSON" });
28
+ return z.NEVER;
29
+ }
30
+ })
31
+ .pipe(schema);
32
+ const privateRuntimeJwkJson = parseJson(
33
+ runtimeJwk.extend({ d: nonEmptyString }),
34
+ );
35
+ const runtimeJwksJson = parseJson(z.object({ keys: z.array(runtimeJwk) }));
36
+ const revocationsJson = parseJson(z.array(nonEmptyString));
37
+ const envString = nonEmptyString.optional();
38
+
39
+ export const RuntimeJwtClaimsSchema = z.object({
40
+ workspaceId: nonEmptyString,
41
+ keyId: nonEmptyString,
42
+ allowedModels: nullableStringArray,
43
+ allowedNamespaces: nullableStringArray,
44
+ });
45
+ const runtimeJwtPayload = z
46
+ .object({
47
+ sub: nonEmptyString,
48
+ jti: nonEmptyString,
49
+ models: nullableStringArray,
50
+ namespaces: nullableStringArray,
51
+ })
52
+ .transform(({ sub, jti, models, namespaces }) => ({
53
+ workspaceId: sub,
54
+ keyId: jti,
55
+ allowedModels: models,
56
+ allowedNamespaces: namespaces,
57
+ }));
58
+
59
+ export type RuntimeJwtClaims = z.infer<typeof RuntimeJwtClaimsSchema>;
60
+ export const RuntimeJwtIssueInputSchema = RuntimeJwtClaimsSchema.extend({
61
+ expiresAt: z
62
+ .string()
63
+ .refine((value) => Number.isFinite(new Date(value).getTime()), {
64
+ message: "Runtime JWT expiration must be an ISO timestamp",
65
+ }),
66
+ });
67
+ export type RuntimeJwtIssueInput = z.infer<typeof RuntimeJwtIssueInputSchema>;
68
+
69
+ export type RuntimeJwtIssuer = {
70
+ publicJwksJson(): string;
71
+ issue(input: RuntimeJwtIssueInput): Promise<string>;
72
+ };
73
+
74
+ export type RuntimeJwtVerificationConfig = {
75
+ jwksJson: string;
76
+ issuer?: string;
77
+ audience?: string;
78
+ revokedKeyIds?: readonly string[];
79
+ workspaceId?: string;
80
+ };
81
+
82
+ export type RuntimeJwtAuthenticator = {
83
+ authenticate(
84
+ request: Request,
85
+ env: Record<string, unknown>,
86
+ ): Promise<RuntimeJwtClaims | null>;
87
+ };
88
+
89
+ export type RuntimeJwtAuthConfig = Omit<
90
+ RuntimeJwtVerificationConfig,
91
+ "jwksJson" | "revokedKeyIds"
92
+ > & {
93
+ jwksJson?: string;
94
+ revokedKeyIdsJson?: string;
95
+ };
96
+
97
+ export function createRuntimeJwtIssuer(
98
+ privateJwkJson: string,
99
+ ): RuntimeJwtIssuer {
100
+ const privateJwk = privateRuntimeJwkJson.parse(privateJwkJson);
101
+ const keyId = nonEmptyString.safeParse(privateJwk.kid).data ?? "runtime-key";
102
+
103
+ return {
104
+ publicJwksJson() {
105
+ const { d: _d, key_ops: _keyOps, ...publicJwk } = privateJwk;
106
+ return JSON.stringify({
107
+ keys: [{ ...publicJwk, kid: keyId, alg: "ES256", use: "sig" }],
108
+ });
109
+ },
110
+ async issue(input) {
111
+ const parsedInput = RuntimeJwtIssueInputSchema.parse(input);
112
+ const expiresAt = Math.floor(
113
+ new Date(parsedInput.expiresAt).getTime() / 1000,
114
+ );
115
+ const key = await importJWK(privateJwk, "ES256");
116
+ return new SignJWT({
117
+ ...(parsedInput.allowedModels
118
+ ? { models: parsedInput.allowedModels }
119
+ : {}),
120
+ ...(parsedInput.allowedNamespaces
121
+ ? { namespaces: parsedInput.allowedNamespaces }
122
+ : {}),
123
+ })
124
+ .setProtectedHeader({ alg: "ES256", kid: keyId, typ: "JWT" })
125
+ .setIssuer(RUNTIME_JWT_ISSUER)
126
+ .setAudience(RUNTIME_JWT_AUDIENCE)
127
+ .setSubject(parsedInput.workspaceId)
128
+ .setJti(parsedInput.keyId)
129
+ .setIssuedAt()
130
+ .setExpirationTime(expiresAt)
131
+ .sign(key);
132
+ },
133
+ };
134
+ }
135
+
136
+ export async function verifyRuntimeJwt(
137
+ token: string,
138
+ config: RuntimeJwtVerificationConfig,
139
+ ): Promise<RuntimeJwtClaims | null> {
140
+ try {
141
+ const jwks = createLocalJWKSet(runtimeJwksJson.parse(config.jwksJson));
142
+ const { payload } = await jwtVerify(token, jwks, {
143
+ issuer: config.issuer ?? RUNTIME_JWT_ISSUER,
144
+ audience: config.audience ?? RUNTIME_JWT_AUDIENCE,
145
+ });
146
+ const claims = runtimeJwtPayload.safeParse(payload);
147
+ if (
148
+ !claims.success ||
149
+ (config.workspaceId && claims.data.workspaceId !== config.workspaceId)
150
+ )
151
+ return null;
152
+ if (config.revokedKeyIds?.includes(claims.data.keyId)) return null;
153
+ return claims.data;
154
+ } catch {
155
+ return null;
156
+ }
157
+ }
158
+
159
+ export function createRuntimeJwtAuthenticator(
160
+ config: RuntimeJwtAuthConfig = {},
161
+ ): RuntimeJwtAuthenticator {
162
+ return {
163
+ async authenticate(request, env) {
164
+ const token = readRuntimeToken(request);
165
+ if (!token) return null;
166
+ const issuer =
167
+ config.issuer ?? envString.safeParse(env.LIL_RUNTIME_AUTH_ISSUER).data;
168
+ const audience =
169
+ config.audience ??
170
+ envString.safeParse(env.LIL_RUNTIME_AUTH_AUDIENCE).data;
171
+ return verifyRuntimeJwt(token, {
172
+ jwksJson:
173
+ config.jwksJson ??
174
+ envString.safeParse(env.LIL_RUNTIME_AUTH_JWKS_JSON).data ??
175
+ "",
176
+ revokedKeyIds:
177
+ revocationsJson.safeParse(
178
+ config.revokedKeyIdsJson ??
179
+ envString.safeParse(env.LIL_RUNTIME_REVOKED_KEY_IDS_JSON).data ??
180
+ "[]",
181
+ ).data ?? [],
182
+ ...(issuer ? { issuer } : {}),
183
+ ...(audience ? { audience } : {}),
184
+ ...(config.workspaceId ? { workspaceId: config.workspaceId } : {}),
185
+ });
186
+ },
187
+ };
188
+ }
189
+
190
+ function readRuntimeToken(request: Request): string | null {
191
+ const bearer = request.headers
192
+ .get("authorization")
193
+ ?.match(/^Bearer\s+(.+)$/i)?.[1];
194
+ if (bearer) return bearer;
195
+ return (
196
+ request.headers.get("x-api-key") ??
197
+ request.headers.get("x-goog-api-key") ??
198
+ new URL(request.url).searchParams.get("key") ??
199
+ new URL(request.url).searchParams.get("api_key")
200
+ );
201
+ }
@@ -0,0 +1,17 @@
1
+ import { z } from "zod";
2
+
3
+ const nonEmptyString = z.string().min(1);
4
+ export const CreateWorkspaceInputSchema = z.object({
5
+ code: nonEmptyString,
6
+ displayName: nonEmptyString,
7
+ });
8
+ export const UpdateWorkspaceInputSchema = CreateWorkspaceInputSchema.partial()
9
+ .refine((input) => Object.keys(input).length > 0)
10
+ .transform((input): { code?: string; displayName?: string } => ({
11
+ ...(input.code === undefined ? {} : { code: input.code }),
12
+ ...(input.displayName === undefined
13
+ ? {}
14
+ : { displayName: input.displayName }),
15
+ }));
16
+ export type CreateWorkspaceInput = z.infer<typeof CreateWorkspaceInputSchema>;
17
+ export type UpdateWorkspaceInput = z.infer<typeof UpdateWorkspaceInputSchema>;
package/tsconfig.json ADDED
@@ -0,0 +1,17 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "ESNext",
5
+ "moduleResolution": "Bundler",
6
+ "lib": ["ES2022", "DOM"],
7
+ "strict": true,
8
+ "allowImportingTsExtensions": true,
9
+ "rewriteRelativeImportExtensions": true,
10
+ "noUncheckedIndexedAccess": true,
11
+ "exactOptionalPropertyTypes": true,
12
+ "skipLibCheck": true,
13
+ "noEmit": true,
14
+ "types": ["vitest/globals"]
15
+ },
16
+ "include": ["src/**/*.ts"]
17
+ }