@enterprisestandard/react 0.0.5-beta.20260115.1 → 0.0.5-beta.20260115.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/group-store.js +127 -0
- package/dist/iam.js +680 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +144 -3672
- package/dist/session-store.js +105 -0
- package/dist/sso-server.d.ts +1 -1
- package/dist/sso-server.d.ts.map +1 -1
- package/dist/sso-server.js +46 -0
- package/dist/sso.js +820 -0
- package/dist/tenant-server.js +6 -0
- package/dist/tenant.js +324 -0
- package/dist/types/base-user.js +1 -0
- package/dist/types/enterprise-user.js +1 -0
- package/dist/types/oidc-schema.js +328 -0
- package/dist/types/scim-schema.js +519 -0
- package/dist/types/standard-schema.js +1 -0
- package/dist/types/user.js +1 -0
- package/dist/types/workload-schema.js +208 -0
- package/dist/ui/sign-in-loading.js +8 -0
- package/dist/ui/signed-in.js +8 -0
- package/dist/ui/signed-out.js +8 -0
- package/dist/ui/sso-provider.js +275 -0
- package/dist/user-store.js +114 -0
- package/dist/utils.js +23 -0
- package/dist/vault.js +22 -0
- package/dist/workload-server.d.ts +1 -1
- package/dist/workload-server.d.ts.map +1 -1
- package/dist/workload-server.js +167 -0
- package/dist/workload-token-store.js +95 -0
- package/dist/workload.js +691 -0
- package/package.json +1 -1
- package/dist/index.js.map +0 -29
package/dist/tenant.js
ADDED
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tenant Management SDK
|
|
3
|
+
*
|
|
4
|
+
* Provides helper functions for applications to implement tenant creation endpoints
|
|
5
|
+
* that ESVS can test. Supports both synchronous (201) and asynchronous (202)
|
|
6
|
+
* tenant creation with webhook-based status updates.
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* Error thrown when tenant request validation fails
|
|
10
|
+
*/
|
|
11
|
+
export class TenantRequestError extends Error {
|
|
12
|
+
constructor(message) {
|
|
13
|
+
super(message);
|
|
14
|
+
this.name = 'TenantRequestError';
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Serializes an ESConfig or EnterpriseStandard instance to a JSON-serializable format
|
|
19
|
+
* by removing non-serializable properties like stores, validators, and functions.
|
|
20
|
+
*
|
|
21
|
+
* Since EnterpriseStandard now extends ESConfig, the config (including handler URLs)
|
|
22
|
+
* is accessible directly from the instance.
|
|
23
|
+
*
|
|
24
|
+
* @param configOrES - The ESConfig object or EnterpriseStandard instance to serialize
|
|
25
|
+
* @returns A JSON-serializable version of the config
|
|
26
|
+
*/
|
|
27
|
+
export function serializeESConfig(configOrES) {
|
|
28
|
+
// EnterpriseStandard now extends ESConfig, so we can access config directly
|
|
29
|
+
// Extract the config properties (sso, iam, workload, validation, defaultInstance)
|
|
30
|
+
// and exclude the service instances (vault, sso instance, iam instance, workload instance)
|
|
31
|
+
if (configOrES &&
|
|
32
|
+
typeof configOrES === 'object' &&
|
|
33
|
+
'sso' in configOrES &&
|
|
34
|
+
'workload' in configOrES &&
|
|
35
|
+
'iam' in configOrES) {
|
|
36
|
+
// It's an EnterpriseStandard instance
|
|
37
|
+
// Extract config from the service instances (they now extend their configs)
|
|
38
|
+
const config = {
|
|
39
|
+
defaultInstance: configOrES.defaultInstance,
|
|
40
|
+
sso: configOrES.sso ? serializeESConfig(configOrES.sso) : undefined,
|
|
41
|
+
iam: configOrES.iam ? serializeESConfig(configOrES.iam) : undefined,
|
|
42
|
+
workload: configOrES.workload ? serializeESConfig(configOrES.workload) : undefined,
|
|
43
|
+
validation: configOrES.validation,
|
|
44
|
+
};
|
|
45
|
+
return config;
|
|
46
|
+
}
|
|
47
|
+
// Otherwise, treat it as an ESConfig or service config
|
|
48
|
+
const config = configOrES;
|
|
49
|
+
if (config === null || config === undefined) {
|
|
50
|
+
return config;
|
|
51
|
+
}
|
|
52
|
+
// Handle primitives
|
|
53
|
+
if (typeof config !== 'object') {
|
|
54
|
+
return config;
|
|
55
|
+
}
|
|
56
|
+
// Handle arrays
|
|
57
|
+
if (Array.isArray(config)) {
|
|
58
|
+
return config.map((item) => serializeESConfig(item));
|
|
59
|
+
}
|
|
60
|
+
// Check if it's a store instance (has methods like get, set, create, delete, etc.)
|
|
61
|
+
const isStore = typeof config.get === 'function' ||
|
|
62
|
+
typeof config.set === 'function' ||
|
|
63
|
+
typeof config.create === 'function' ||
|
|
64
|
+
typeof config.delete === 'function' ||
|
|
65
|
+
typeof config.upsert === 'function' ||
|
|
66
|
+
typeof config.list === 'function';
|
|
67
|
+
// Check if it's a StandardSchemaV1 validator (has '~standard' property)
|
|
68
|
+
const isValidator = config['~standard'] !== undefined;
|
|
69
|
+
// Check if it's a function
|
|
70
|
+
if (typeof config === 'function' || isStore || isValidator) {
|
|
71
|
+
return undefined;
|
|
72
|
+
}
|
|
73
|
+
// Handle Date objects
|
|
74
|
+
if (config instanceof Date) {
|
|
75
|
+
return config.toISOString();
|
|
76
|
+
}
|
|
77
|
+
// Recursively serialize object properties
|
|
78
|
+
const serialized = {};
|
|
79
|
+
for (const key in config) {
|
|
80
|
+
if (Object.hasOwn(config, key)) {
|
|
81
|
+
// Skip non-serializable properties
|
|
82
|
+
if (key === 'session_store' ||
|
|
83
|
+
key === 'user_store' ||
|
|
84
|
+
key === 'token_store' ||
|
|
85
|
+
key === 'group_store' ||
|
|
86
|
+
key === 'validation' ||
|
|
87
|
+
key === 'vault' || // Skip vault instance
|
|
88
|
+
// Skip method properties from service instances
|
|
89
|
+
key === 'getUser' ||
|
|
90
|
+
key === 'getRequiredUser' ||
|
|
91
|
+
key === 'getJwt' ||
|
|
92
|
+
key === 'initiateLogin' ||
|
|
93
|
+
key === 'logout' ||
|
|
94
|
+
key === 'callbackHandler' ||
|
|
95
|
+
key === 'handler' ||
|
|
96
|
+
key === 'getToken' ||
|
|
97
|
+
key === 'refreshToken' ||
|
|
98
|
+
key === 'generateJWTAssertion' ||
|
|
99
|
+
key === 'revokeToken' ||
|
|
100
|
+
key === 'validateToken' ||
|
|
101
|
+
key === 'getWorkload' ||
|
|
102
|
+
key === 'parseJWT' ||
|
|
103
|
+
key === 'createUser' ||
|
|
104
|
+
key === 'getBaseUrl' ||
|
|
105
|
+
key === 'groups_outbound' ||
|
|
106
|
+
key === 'groups_inbound') {
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
const value = serializeESConfig(config[key]);
|
|
110
|
+
// Only include defined values
|
|
111
|
+
if (value !== undefined) {
|
|
112
|
+
serialized[key] = value;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
return serialized;
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Parse and validate a tenant creation request from an HTTP request.
|
|
120
|
+
*
|
|
121
|
+
* @param request - The HTTP request containing the tenant creation data
|
|
122
|
+
* @returns The validated tenant creation request
|
|
123
|
+
* @throws {TenantRequestError} If the request is invalid or missing required fields
|
|
124
|
+
*
|
|
125
|
+
* @example
|
|
126
|
+
* ```typescript
|
|
127
|
+
* app.post('/api/tenant', async (c) => {
|
|
128
|
+
* try {
|
|
129
|
+
* const tenantRequest = await parseTenantRequest(c.req.raw);
|
|
130
|
+
* // Create tenant...
|
|
131
|
+
* } catch (error) {
|
|
132
|
+
* if (error instanceof TenantRequestError) {
|
|
133
|
+
* return c.json({ error: error.message }, 400);
|
|
134
|
+
* }
|
|
135
|
+
* throw error;
|
|
136
|
+
* }
|
|
137
|
+
* });
|
|
138
|
+
* ```
|
|
139
|
+
*/
|
|
140
|
+
export async function parseTenantRequest(request) {
|
|
141
|
+
if (request.method !== 'POST') {
|
|
142
|
+
throw new TenantRequestError('Only POST method is supported');
|
|
143
|
+
}
|
|
144
|
+
let body;
|
|
145
|
+
try {
|
|
146
|
+
body = await request.json();
|
|
147
|
+
}
|
|
148
|
+
catch (_error) {
|
|
149
|
+
throw new TenantRequestError('Invalid JSON in request body');
|
|
150
|
+
}
|
|
151
|
+
if (typeof body !== 'object' || body === null) {
|
|
152
|
+
throw new TenantRequestError('Request body must be an object');
|
|
153
|
+
}
|
|
154
|
+
const tenantRequest = body;
|
|
155
|
+
// Validate required fields
|
|
156
|
+
if (!tenantRequest.appId) {
|
|
157
|
+
throw new TenantRequestError('Missing required field: appId');
|
|
158
|
+
}
|
|
159
|
+
if (!tenantRequest.companyId) {
|
|
160
|
+
throw new TenantRequestError('Missing required field: companyId');
|
|
161
|
+
}
|
|
162
|
+
if (!tenantRequest.companyName) {
|
|
163
|
+
throw new TenantRequestError('Missing required field: companyName');
|
|
164
|
+
}
|
|
165
|
+
if (!tenantRequest.environmentType) {
|
|
166
|
+
throw new TenantRequestError('Missing required field: environmentType');
|
|
167
|
+
}
|
|
168
|
+
if (!tenantRequest.email) {
|
|
169
|
+
throw new TenantRequestError('Missing required field: email');
|
|
170
|
+
}
|
|
171
|
+
if (!tenantRequest.webhookUrl) {
|
|
172
|
+
throw new TenantRequestError('Missing required field: webhookUrl');
|
|
173
|
+
}
|
|
174
|
+
// Validate environmentType enum
|
|
175
|
+
const validEnvironmentTypes = ['POC', 'DEV', 'QA', 'PROD'];
|
|
176
|
+
if (!validEnvironmentTypes.includes(tenantRequest.environmentType)) {
|
|
177
|
+
throw new TenantRequestError(`Invalid environmentType: ${tenantRequest.environmentType}. Must be one of: ${validEnvironmentTypes.join(', ')}`);
|
|
178
|
+
}
|
|
179
|
+
// Validate webhookUrl is a valid URL
|
|
180
|
+
try {
|
|
181
|
+
new URL(tenantRequest.webhookUrl);
|
|
182
|
+
}
|
|
183
|
+
catch {
|
|
184
|
+
throw new TenantRequestError('Invalid webhookUrl: must be a valid URL');
|
|
185
|
+
}
|
|
186
|
+
// At this point, all required fields are validated and non-null
|
|
187
|
+
// Extract them to satisfy TypeScript's type narrowing
|
|
188
|
+
const appId = tenantRequest.appId;
|
|
189
|
+
const companyId = tenantRequest.companyId;
|
|
190
|
+
const companyName = tenantRequest.companyName;
|
|
191
|
+
const environmentType = tenantRequest.environmentType;
|
|
192
|
+
const email = tenantRequest.email;
|
|
193
|
+
const webhookUrl = tenantRequest.webhookUrl;
|
|
194
|
+
return {
|
|
195
|
+
appId,
|
|
196
|
+
companyId,
|
|
197
|
+
companyName,
|
|
198
|
+
environmentType,
|
|
199
|
+
email,
|
|
200
|
+
webhookUrl,
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* Send a webhook update to ESVS with tenant creation status.
|
|
205
|
+
*
|
|
206
|
+
* @param webhookUrl - The webhook URL provided in the tenant creation request
|
|
207
|
+
* @param payload - The webhook payload with status and tenant information
|
|
208
|
+
* @throws Never throws - errors are logged but not propagated to avoid breaking tenant creation
|
|
209
|
+
*
|
|
210
|
+
* @example
|
|
211
|
+
* ```typescript
|
|
212
|
+
* // Send initial status
|
|
213
|
+
* await sendTenantWebhook(tenantRequest.webhookUrl, {
|
|
214
|
+
* companyId: tenantRequest.companyId,
|
|
215
|
+
* status: 'processing',
|
|
216
|
+
* });
|
|
217
|
+
*
|
|
218
|
+
* // Send completion status
|
|
219
|
+
* await sendTenantWebhook(tenantRequest.webhookUrl, {
|
|
220
|
+
* companyId: tenantRequest.companyId,
|
|
221
|
+
* status: 'completed',
|
|
222
|
+
* tenantUrl: 'https://app.example.com/tenants/tenant-123',
|
|
223
|
+
* });
|
|
224
|
+
* ```
|
|
225
|
+
*/
|
|
226
|
+
export async function sendTenantWebhook(webhookUrl, payload) {
|
|
227
|
+
try {
|
|
228
|
+
const response = await fetch(webhookUrl, {
|
|
229
|
+
method: 'POST',
|
|
230
|
+
headers: {
|
|
231
|
+
'Content-Type': 'application/json',
|
|
232
|
+
},
|
|
233
|
+
body: JSON.stringify(payload),
|
|
234
|
+
});
|
|
235
|
+
if (!response.ok) {
|
|
236
|
+
console.error(`Failed to send webhook update: ${response.status} ${response.statusText}`);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
catch (error) {
|
|
240
|
+
console.error('Failed to send webhook update:', error);
|
|
241
|
+
// Don't throw - webhook failures shouldn't break tenant creation
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
/**
|
|
245
|
+
* In-memory tenant store implementation using Maps.
|
|
246
|
+
*
|
|
247
|
+
* Suitable for:
|
|
248
|
+
* - Development and testing
|
|
249
|
+
* - Single-server deployments
|
|
250
|
+
* - Applications without high availability requirements
|
|
251
|
+
*
|
|
252
|
+
* NOT suitable for:
|
|
253
|
+
* - Multi-server deployments (tenants not shared)
|
|
254
|
+
* - High availability scenarios (tenants lost on restart)
|
|
255
|
+
* - Production applications with distributed architecture
|
|
256
|
+
*
|
|
257
|
+
* For production, implement TenantStore with Redis or a database.
|
|
258
|
+
*
|
|
259
|
+
* @template TExtended - Type-safe custom data that consumers can add to tenants
|
|
260
|
+
*/
|
|
261
|
+
export class InMemoryTenantStore {
|
|
262
|
+
constructor() {
|
|
263
|
+
/** Primary storage: appId -> tenant */
|
|
264
|
+
this.tenants = new Map();
|
|
265
|
+
/** Secondary index: companyId -> Set of appIds (since one company can have multiple apps) */
|
|
266
|
+
this.companyIdIndex = new Map();
|
|
267
|
+
}
|
|
268
|
+
async get(appId) {
|
|
269
|
+
return this.tenants.get(appId) ?? null;
|
|
270
|
+
}
|
|
271
|
+
async getByCompanyId(companyId) {
|
|
272
|
+
const appIds = this.companyIdIndex.get(companyId);
|
|
273
|
+
if (!appIds || appIds.size === 0)
|
|
274
|
+
return [];
|
|
275
|
+
const tenants = [];
|
|
276
|
+
for (const appId of appIds) {
|
|
277
|
+
const tenant = this.tenants.get(appId);
|
|
278
|
+
if (tenant) {
|
|
279
|
+
tenants.push(tenant);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
return tenants;
|
|
283
|
+
}
|
|
284
|
+
async list() {
|
|
285
|
+
return Array.from(this.tenants.values());
|
|
286
|
+
}
|
|
287
|
+
async upsert(tenant) {
|
|
288
|
+
const existing = this.tenants.get(tenant.appId);
|
|
289
|
+
// Clean up old index if companyId changed
|
|
290
|
+
if (existing && existing.companyId !== tenant.companyId) {
|
|
291
|
+
const oldAppIds = this.companyIdIndex.get(existing.companyId);
|
|
292
|
+
if (oldAppIds) {
|
|
293
|
+
oldAppIds.delete(tenant.appId);
|
|
294
|
+
if (oldAppIds.size === 0) {
|
|
295
|
+
this.companyIdIndex.delete(existing.companyId);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
// Store the tenant
|
|
300
|
+
this.tenants.set(tenant.appId, tenant);
|
|
301
|
+
// Update index (companyId -> Set of appIds)
|
|
302
|
+
let appIds = this.companyIdIndex.get(tenant.companyId);
|
|
303
|
+
if (!appIds) {
|
|
304
|
+
appIds = new Set();
|
|
305
|
+
this.companyIdIndex.set(tenant.companyId, appIds);
|
|
306
|
+
}
|
|
307
|
+
appIds.add(tenant.appId);
|
|
308
|
+
return tenant;
|
|
309
|
+
}
|
|
310
|
+
async delete(appId) {
|
|
311
|
+
const tenant = this.tenants.get(appId);
|
|
312
|
+
if (tenant) {
|
|
313
|
+
// Clean up index
|
|
314
|
+
const appIds = this.companyIdIndex.get(tenant.companyId);
|
|
315
|
+
if (appIds) {
|
|
316
|
+
appIds.delete(appId);
|
|
317
|
+
if (appIds.size === 0) {
|
|
318
|
+
this.companyIdIndex.delete(tenant.companyId);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
this.tenants.delete(appId);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Creates a StandardSchemaV1 for validating OIDC callback URL parameters.
|
|
3
|
+
* @param vendor - The name of the vendor creating this schema
|
|
4
|
+
* @returns A StandardSchemaV1 instance for OIDC callback parameters
|
|
5
|
+
*/
|
|
6
|
+
export function oidcCallbackSchema(vendor) {
|
|
7
|
+
return {
|
|
8
|
+
'~standard': {
|
|
9
|
+
version: 1,
|
|
10
|
+
vendor,
|
|
11
|
+
validate: (value) => {
|
|
12
|
+
if (typeof value !== 'object' || value === null) {
|
|
13
|
+
return {
|
|
14
|
+
issues: [
|
|
15
|
+
{
|
|
16
|
+
message: 'Expected an object',
|
|
17
|
+
},
|
|
18
|
+
],
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
const params = value;
|
|
22
|
+
const issues = [];
|
|
23
|
+
const result = {};
|
|
24
|
+
// Check required 'code' parameter
|
|
25
|
+
if ('code' in params) {
|
|
26
|
+
if (typeof params.code === 'string') {
|
|
27
|
+
result.code = params.code;
|
|
28
|
+
}
|
|
29
|
+
else {
|
|
30
|
+
issues.push({
|
|
31
|
+
message: 'code must be a string',
|
|
32
|
+
path: ['code'],
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
else if (!('error' in params)) {
|
|
37
|
+
// 'code' is required unless there's an error
|
|
38
|
+
issues.push({
|
|
39
|
+
message: 'code is required',
|
|
40
|
+
path: ['code'],
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
// Validate state if present
|
|
44
|
+
if ('state' in params) {
|
|
45
|
+
if (typeof params.state === 'string' || params.state === undefined) {
|
|
46
|
+
result.state = params.state;
|
|
47
|
+
}
|
|
48
|
+
else {
|
|
49
|
+
issues.push({
|
|
50
|
+
message: 'state must be a string',
|
|
51
|
+
path: ['state'],
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
// Validate session_state if present
|
|
56
|
+
if ('session_state' in params) {
|
|
57
|
+
if (typeof params.session_state === 'string' || params.session_state === undefined) {
|
|
58
|
+
result.session_state = params.session_state;
|
|
59
|
+
}
|
|
60
|
+
else {
|
|
61
|
+
issues.push({
|
|
62
|
+
message: 'session_state must be a string',
|
|
63
|
+
path: ['session_state'],
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
// Validate error related fields if present
|
|
68
|
+
if ('error' in params) {
|
|
69
|
+
if (typeof params.error === 'string') {
|
|
70
|
+
result.error = params.error;
|
|
71
|
+
}
|
|
72
|
+
else {
|
|
73
|
+
issues.push({
|
|
74
|
+
message: 'error must be a string',
|
|
75
|
+
path: ['error'],
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
if ('error_description' in params) {
|
|
79
|
+
if (typeof params.error_description === 'string' || params.error_description === undefined) {
|
|
80
|
+
result.error_description = params.error_description;
|
|
81
|
+
}
|
|
82
|
+
else {
|
|
83
|
+
issues.push({
|
|
84
|
+
message: 'error_description must be a string',
|
|
85
|
+
path: ['error_description'],
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
if ('error_uri' in params) {
|
|
90
|
+
if (typeof params.error_uri === 'string' || params.error_uri === undefined) {
|
|
91
|
+
result.error_uri = params.error_uri;
|
|
92
|
+
}
|
|
93
|
+
else {
|
|
94
|
+
issues.push({
|
|
95
|
+
message: 'error_uri must be a string',
|
|
96
|
+
path: ['error_uri'],
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
// Validate iss if present
|
|
102
|
+
if ('iss' in params) {
|
|
103
|
+
if (typeof params.iss === 'string' || params.iss === undefined) {
|
|
104
|
+
result.iss = params.iss;
|
|
105
|
+
}
|
|
106
|
+
else {
|
|
107
|
+
issues.push({
|
|
108
|
+
message: 'iss must be a string',
|
|
109
|
+
path: ['iss'],
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
if (issues.length > 0) {
|
|
114
|
+
return { issues };
|
|
115
|
+
}
|
|
116
|
+
return { value: result };
|
|
117
|
+
},
|
|
118
|
+
},
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Creates a StandardSchemaV1 for validating OIDC Token Responses.
|
|
123
|
+
* @param vendor - The name of the vendor creating this schema
|
|
124
|
+
* @returns A StandardSchemaV1 instance for Token Response validation
|
|
125
|
+
*/
|
|
126
|
+
export function tokenResponseSchema(vendor) {
|
|
127
|
+
return {
|
|
128
|
+
'~standard': {
|
|
129
|
+
version: 1,
|
|
130
|
+
vendor,
|
|
131
|
+
validate: (value) => {
|
|
132
|
+
if (typeof value !== 'object' || value === null) {
|
|
133
|
+
return {
|
|
134
|
+
issues: [
|
|
135
|
+
{
|
|
136
|
+
message: 'Expected an object',
|
|
137
|
+
},
|
|
138
|
+
],
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
const response = value;
|
|
142
|
+
const issues = [];
|
|
143
|
+
const result = {};
|
|
144
|
+
// Check required 'access_token' parameter
|
|
145
|
+
if ('access_token' in response) {
|
|
146
|
+
if (typeof response.access_token === 'string') {
|
|
147
|
+
result.access_token = response.access_token;
|
|
148
|
+
}
|
|
149
|
+
else {
|
|
150
|
+
issues.push({
|
|
151
|
+
message: 'access_token must be a string',
|
|
152
|
+
path: ['access_token'],
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
else {
|
|
157
|
+
issues.push({
|
|
158
|
+
message: 'access_token is required',
|
|
159
|
+
path: ['access_token'],
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
// Check required 'id_token' parameter
|
|
163
|
+
if ('id_token' in response) {
|
|
164
|
+
if (typeof response.id_token === 'string') {
|
|
165
|
+
result.id_token = response.id_token;
|
|
166
|
+
}
|
|
167
|
+
else {
|
|
168
|
+
issues.push({
|
|
169
|
+
message: 'id_token must be a string',
|
|
170
|
+
path: ['id_token'],
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
else {
|
|
175
|
+
issues.push({
|
|
176
|
+
message: 'id_token is required',
|
|
177
|
+
path: ['id_token'],
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
// Check required 'token_type' parameter
|
|
181
|
+
if ('token_type' in response) {
|
|
182
|
+
if (typeof response.token_type === 'string') {
|
|
183
|
+
result.token_type = response.token_type;
|
|
184
|
+
}
|
|
185
|
+
else {
|
|
186
|
+
issues.push({
|
|
187
|
+
message: 'token_type must be a string',
|
|
188
|
+
path: ['token_type'],
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
else {
|
|
193
|
+
issues.push({
|
|
194
|
+
message: 'token_type is required',
|
|
195
|
+
path: ['token_type'],
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
// Optional string fields
|
|
199
|
+
if ('refresh_token' in response) {
|
|
200
|
+
if (typeof response.refresh_token === 'string' || response.refresh_token === undefined) {
|
|
201
|
+
result.refresh_token = response.refresh_token;
|
|
202
|
+
}
|
|
203
|
+
else {
|
|
204
|
+
issues.push({
|
|
205
|
+
message: 'refresh_token must be a string',
|
|
206
|
+
path: ['refresh_token'],
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
if ('scope' in response) {
|
|
211
|
+
if (typeof response.scope === 'string' || response.scope === undefined) {
|
|
212
|
+
result.scope = response.scope;
|
|
213
|
+
}
|
|
214
|
+
else {
|
|
215
|
+
issues.push({
|
|
216
|
+
message: 'scope must be a string',
|
|
217
|
+
path: ['scope'],
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
if ('session_state' in response) {
|
|
222
|
+
if (typeof response.session_state === 'string' || response.session_state === undefined) {
|
|
223
|
+
result.session_state = response.session_state;
|
|
224
|
+
}
|
|
225
|
+
else {
|
|
226
|
+
issues.push({
|
|
227
|
+
message: 'session_state must be a string',
|
|
228
|
+
path: ['session_state'],
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
if ('expires' in response) {
|
|
233
|
+
if (typeof response.expires === 'string' || response.expires === undefined) {
|
|
234
|
+
result.expires = response.expires;
|
|
235
|
+
}
|
|
236
|
+
else {
|
|
237
|
+
issues.push({
|
|
238
|
+
message: 'expires must be a string',
|
|
239
|
+
path: ['expires'],
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
// Optional number fields
|
|
244
|
+
if ('expires_in' in response) {
|
|
245
|
+
if (typeof response.expires_in === 'number' || response.expires_in === undefined) {
|
|
246
|
+
result.expires_in = response.expires_in;
|
|
247
|
+
}
|
|
248
|
+
else {
|
|
249
|
+
issues.push({
|
|
250
|
+
message: 'expires_in must be a number',
|
|
251
|
+
path: ['expires_in'],
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
if ('refresh_expires_in' in response) {
|
|
256
|
+
if (typeof response.refresh_expires_in === 'number' || response.refresh_expires_in === undefined) {
|
|
257
|
+
result.refresh_expires_in = response.refresh_expires_in;
|
|
258
|
+
}
|
|
259
|
+
else {
|
|
260
|
+
issues.push({
|
|
261
|
+
message: 'refresh_expires_in must be a number',
|
|
262
|
+
path: ['refresh_expires_in'],
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
if (issues.length > 0) {
|
|
267
|
+
return { issues };
|
|
268
|
+
}
|
|
269
|
+
return { value: result };
|
|
270
|
+
},
|
|
271
|
+
},
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
/**
|
|
275
|
+
* Creates a StandardSchemaV1 for validating ID Token Claims.
|
|
276
|
+
* @param vendor - The name of the vendor creating this schema
|
|
277
|
+
* @returns A StandardSchemaV1 instance for ID Token Claims validation
|
|
278
|
+
*/
|
|
279
|
+
export function idTokenClaimsSchema(vendor) {
|
|
280
|
+
return {
|
|
281
|
+
'~standard': {
|
|
282
|
+
version: 1,
|
|
283
|
+
vendor,
|
|
284
|
+
validate: (value) => {
|
|
285
|
+
if (typeof value !== 'object' || value === null) {
|
|
286
|
+
return {
|
|
287
|
+
issues: [
|
|
288
|
+
{
|
|
289
|
+
message: 'Expected an object',
|
|
290
|
+
},
|
|
291
|
+
],
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
const claims = value;
|
|
295
|
+
const issues = [];
|
|
296
|
+
const result = { ...claims };
|
|
297
|
+
// Validate optional string fields
|
|
298
|
+
const stringFields = ['iss', 'aud', 'sub', 'sid', 'name', 'email', 'preferred_username', 'picture'];
|
|
299
|
+
for (const field of stringFields) {
|
|
300
|
+
if (field in claims && claims[field] !== undefined) {
|
|
301
|
+
if (typeof claims[field] !== 'string') {
|
|
302
|
+
issues.push({
|
|
303
|
+
message: `${field} must be a string`,
|
|
304
|
+
path: [field],
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
// Validate optional number fields
|
|
310
|
+
const numberFields = ['exp', 'iat'];
|
|
311
|
+
for (const field of numberFields) {
|
|
312
|
+
if (field in claims && claims[field] !== undefined) {
|
|
313
|
+
if (typeof claims[field] !== 'number') {
|
|
314
|
+
issues.push({
|
|
315
|
+
message: `${field} must be a number`,
|
|
316
|
+
path: [field],
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
if (issues.length > 0) {
|
|
322
|
+
return { issues };
|
|
323
|
+
}
|
|
324
|
+
return { value: result };
|
|
325
|
+
},
|
|
326
|
+
},
|
|
327
|
+
};
|
|
328
|
+
}
|