@seamless-auth/types 0.4.0 → 0.4.1
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/CHANGELOG.md +16 -0
- package/README.md +121 -63
- package/dist/schemas/webauthn/schema.d.ts +2 -2
- package/package.json +6 -6
- package/src/index.ts +19 -0
- package/src/schemas/admin/schema.ts +54 -0
- package/src/schemas/auth/auth.schema.ts +139 -0
- package/src/schemas/authEvent/schema.ts +120 -0
- package/src/schemas/common/schema.ts +33 -0
- package/src/schemas/credential/schema.ts +103 -0
- package/src/schemas/me/schema.ts +32 -0
- package/src/schemas/messaging/schema.ts +86 -0
- package/src/schemas/metrics/schema.ts +115 -0
- package/src/schemas/oauth/schema.ts +70 -0
- package/src/schemas/organization/schema.ts +135 -0
- package/src/schemas/role/matching.ts +76 -0
- package/src/schemas/role/schema.ts +15 -0
- package/src/schemas/session/schema.ts +26 -0
- package/src/schemas/stepUp/schema.ts +27 -0
- package/src/schemas/systemConfig/schema.ts +196 -0
- package/src/schemas/totp/schema.ts +34 -0
- package/src/schemas/user/schema.ts +75 -0
- package/src/schemas/webauthn/schema.ts +82 -0
- package/src/shared.ts +3 -0
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
|
|
3
|
+
export const PaginationQuerySchema = z.object({
|
|
4
|
+
limit: z.coerce.number().min(1).max(100).optional().default(50),
|
|
5
|
+
offset: z.coerce.number().min(0).optional().default(0),
|
|
6
|
+
});
|
|
7
|
+
|
|
8
|
+
export type PaginationQuery = z.infer<typeof PaginationQuerySchema>;
|
|
9
|
+
|
|
10
|
+
/** Body for endpoints that only acknowledge the request. */
|
|
11
|
+
export const MessageResponseSchema = z.object({
|
|
12
|
+
message: z.string(),
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
export type MessageResponse = z.infer<typeof MessageResponseSchema>;
|
|
16
|
+
|
|
17
|
+
export const ErrorResponseSchema = z.object({
|
|
18
|
+
message: z.string().optional(),
|
|
19
|
+
error: z.string(),
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
export type ErrorResponse = z.infer<typeof ErrorResponseSchema>;
|
|
23
|
+
|
|
24
|
+
export const InvalidPayloadResponseSchema = z.object({
|
|
25
|
+
error: z.string(),
|
|
26
|
+
details: z.unknown().optional(),
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
export type InvalidPayloadResponse = z.infer<typeof InvalidPayloadResponseSchema>;
|
|
30
|
+
|
|
31
|
+
export const MetadataSchema = z.record(z.string(), z.unknown()).nullable().optional();
|
|
32
|
+
|
|
33
|
+
export type Metadata = z.infer<typeof MetadataSchema>;
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import z from 'zod';
|
|
2
|
+
import { IsoDate } from '../../shared.js';
|
|
3
|
+
|
|
4
|
+
// Mirrors WebAuthn's AuthenticatorTransportFuture. Authenticators report
|
|
5
|
+
// transports the browser passes through verbatim, so anything narrower rejects
|
|
6
|
+
// credentials the platform considers valid.
|
|
7
|
+
export const TransportSchema = z.enum([
|
|
8
|
+
'ble',
|
|
9
|
+
'cable',
|
|
10
|
+
'hybrid',
|
|
11
|
+
'internal',
|
|
12
|
+
'nfc',
|
|
13
|
+
'smart-card',
|
|
14
|
+
'usb',
|
|
15
|
+
]);
|
|
16
|
+
|
|
17
|
+
export type Transport = z.infer<typeof TransportSchema>;
|
|
18
|
+
|
|
19
|
+
export const DeviceTypeSchema = z.enum(['singleDevice', 'multiDevice']);
|
|
20
|
+
|
|
21
|
+
export type DeviceType = z.infer<typeof DeviceTypeSchema>;
|
|
22
|
+
|
|
23
|
+
export const CredentialSchema = z.object({
|
|
24
|
+
id: z.string(),
|
|
25
|
+
userId: z.string(),
|
|
26
|
+
|
|
27
|
+
publicKey: z.string(),
|
|
28
|
+
counter: z.number(),
|
|
29
|
+
|
|
30
|
+
transports: z.array(TransportSchema).optional(),
|
|
31
|
+
|
|
32
|
+
deviceType: DeviceTypeSchema.optional(),
|
|
33
|
+
|
|
34
|
+
backedUp: z.boolean(),
|
|
35
|
+
|
|
36
|
+
friendlyName: z.string().nullable().optional(),
|
|
37
|
+
lastUsedAt: IsoDate.nullable().optional(),
|
|
38
|
+
|
|
39
|
+
platform: z.string().nullable().optional(),
|
|
40
|
+
browser: z.string().nullable().optional(),
|
|
41
|
+
deviceInfo: z.string().nullable().optional(),
|
|
42
|
+
|
|
43
|
+
createdAt: IsoDate,
|
|
44
|
+
updatedAt: IsoDate.optional(),
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
export type Credential = z.infer<typeof CredentialSchema>;
|
|
48
|
+
|
|
49
|
+
export const UpdateCredentialRequestSchema = z.object({
|
|
50
|
+
id: z.string(),
|
|
51
|
+
friendlyName: z.string().min(1).max(128).optional(),
|
|
52
|
+
deviceInfo: z.string().max(256).optional(),
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
export type UpdateCredentialRequest = z.infer<typeof UpdateCredentialRequestSchema>;
|
|
56
|
+
|
|
57
|
+
export const DeleteCredentialRequestSchema = z.object({
|
|
58
|
+
id: z.string(),
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
export type DeleteCredentialRequest = z.infer<typeof DeleteCredentialRequestSchema>;
|
|
62
|
+
|
|
63
|
+
export const CredentialApiSchema = CredentialSchema.pick({
|
|
64
|
+
id: true,
|
|
65
|
+
transports: true,
|
|
66
|
+
deviceType: true,
|
|
67
|
+
backedUp: true,
|
|
68
|
+
counter: true,
|
|
69
|
+
friendlyName: true,
|
|
70
|
+
lastUsedAt: true,
|
|
71
|
+
platform: true,
|
|
72
|
+
browser: true,
|
|
73
|
+
deviceInfo: true,
|
|
74
|
+
createdAt: true,
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
export type CredentialApi = z.infer<typeof CredentialApiSchema>;
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* A credential as the API returns it. `backedup` is the historical lowercase
|
|
81
|
+
* spelling kept alongside `backedUp` so older SDK builds keep working; new
|
|
82
|
+
* consumers should read `backedUp`.
|
|
83
|
+
*/
|
|
84
|
+
export const CredentialResponseSchema = CredentialApiSchema.extend({
|
|
85
|
+
backedup: z.boolean(),
|
|
86
|
+
backedUp: z.boolean(),
|
|
87
|
+
prfCapable: z.boolean().optional(),
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
export type CredentialResponse = z.infer<typeof CredentialResponseSchema>;
|
|
91
|
+
|
|
92
|
+
export const CredentialUpdateResponseSchema = z.object({
|
|
93
|
+
message: z.string(),
|
|
94
|
+
credential: CredentialResponseSchema,
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
export type CredentialUpdateResponse = z.infer<typeof CredentialUpdateResponseSchema>;
|
|
98
|
+
|
|
99
|
+
export const CredentialCountResponseSchema = z.object({
|
|
100
|
+
count: z.number(),
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
export type CredentialCountResponse = z.infer<typeof CredentialCountResponseSchema>;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { IsoDate } from '../../shared.js';
|
|
3
|
+
import { CredentialResponseSchema } from '../credential/schema.js';
|
|
4
|
+
import { OrganizationSchema } from '../organization/schema.js';
|
|
5
|
+
import { RoleNameSchema } from '../role/schema.js';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* The caller's own user record. `lastLogin` is null until the first login, and
|
|
9
|
+
* `activeOrganizationId` is null when the access token carries no org context.
|
|
10
|
+
*/
|
|
11
|
+
export const MeUserSchema = z.object({
|
|
12
|
+
id: z.string(),
|
|
13
|
+
email: z.email(),
|
|
14
|
+
phone: z.string().nullable(),
|
|
15
|
+
roles: z.array(RoleNameSchema),
|
|
16
|
+
lastLogin: IsoDate.nullable().optional(),
|
|
17
|
+
activeOrganizationId: z.string().nullable().optional(),
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
export type MeUser = z.infer<typeof MeUserSchema>;
|
|
21
|
+
|
|
22
|
+
/** @deprecated Use {@link MeUser}. */
|
|
23
|
+
export type SeamlessUser = MeUser;
|
|
24
|
+
|
|
25
|
+
export const MeResponseSchema = z.object({
|
|
26
|
+
user: MeUserSchema,
|
|
27
|
+
credentials: z.array(CredentialResponseSchema),
|
|
28
|
+
organizations: z.array(OrganizationSchema).optional(),
|
|
29
|
+
activeOrganization: OrganizationSchema.nullable().optional(),
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
export type MeResponse = z.infer<typeof MeResponseSchema>;
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
|
|
3
|
+
export const MessagingChannelSchema = z.enum(['email', 'sms']);
|
|
4
|
+
|
|
5
|
+
export type MessagingChannel = z.infer<typeof MessagingChannelSchema>;
|
|
6
|
+
|
|
7
|
+
export const DeliveryResultSchema = z.object({
|
|
8
|
+
accepted: z.boolean(),
|
|
9
|
+
provider: z.string(),
|
|
10
|
+
channel: MessagingChannelSchema,
|
|
11
|
+
messageId: z.string().optional(),
|
|
12
|
+
raw: z.unknown().optional(),
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
export type DeliveryResult = z.infer<typeof DeliveryResultSchema>;
|
|
16
|
+
|
|
17
|
+
export const EmailMessageSchema = z.object({
|
|
18
|
+
to: z.string(),
|
|
19
|
+
from: z.string().optional(),
|
|
20
|
+
subject: z.string(),
|
|
21
|
+
text: z.string(),
|
|
22
|
+
html: z.string().optional(),
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
export type EmailMessage = z.infer<typeof EmailMessageSchema>;
|
|
26
|
+
|
|
27
|
+
export const SmsMessageSchema = z.object({
|
|
28
|
+
to: z.string(),
|
|
29
|
+
from: z.string().optional(),
|
|
30
|
+
body: z.string(),
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
export type SmsMessage = z.infer<typeof SmsMessageSchema>;
|
|
34
|
+
|
|
35
|
+
export const SendOtpEmailInputSchema = z.object({
|
|
36
|
+
to: z.string(),
|
|
37
|
+
token: z.string(),
|
|
38
|
+
from: z.string().optional(),
|
|
39
|
+
subject: z.string().optional(),
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
export type SendOtpEmailInput = z.infer<typeof SendOtpEmailInputSchema>;
|
|
43
|
+
|
|
44
|
+
export const SendOtpSmsInputSchema = z.object({
|
|
45
|
+
to: z.string(),
|
|
46
|
+
token: z.union([z.string(), z.number()]),
|
|
47
|
+
from: z.string().optional(),
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
export type SendOtpSmsInput = z.infer<typeof SendOtpSmsInputSchema>;
|
|
51
|
+
|
|
52
|
+
export const SendMagicLinkEmailInputSchema = z.object({
|
|
53
|
+
to: z.string(),
|
|
54
|
+
magicLinkUrl: z.string(),
|
|
55
|
+
token: z.string().optional(),
|
|
56
|
+
from: z.string().optional(),
|
|
57
|
+
subject: z.string().optional(),
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
export type SendMagicLinkEmailInput = z.infer<typeof SendMagicLinkEmailInputSchema>;
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* What the auth API tells a self-hosted server to deliver. The API never sends
|
|
64
|
+
* the message itself when the adopter owns delivery, so this is the contract
|
|
65
|
+
* between the two.
|
|
66
|
+
*/
|
|
67
|
+
export const AuthDeliverySchema = z.discriminatedUnion('kind', [
|
|
68
|
+
z.object({
|
|
69
|
+
kind: z.literal('otp_email'),
|
|
70
|
+
to: z.string(),
|
|
71
|
+
token: z.string(),
|
|
72
|
+
}),
|
|
73
|
+
z.object({
|
|
74
|
+
kind: z.literal('otp_sms'),
|
|
75
|
+
to: z.string(),
|
|
76
|
+
token: z.union([z.string(), z.number()]),
|
|
77
|
+
}),
|
|
78
|
+
z.object({
|
|
79
|
+
kind: z.literal('magic_link_email'),
|
|
80
|
+
to: z.string(),
|
|
81
|
+
token: z.string().optional(),
|
|
82
|
+
magicLinkUrl: z.string(),
|
|
83
|
+
}),
|
|
84
|
+
]);
|
|
85
|
+
|
|
86
|
+
export type AuthDeliveryInstruction = z.infer<typeof AuthDeliverySchema>;
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { AuthEventSchema } from '../authEvent/schema.js';
|
|
3
|
+
|
|
4
|
+
const MAX_METRICS_WINDOW_MS = 1000 * 60 * 60 * 24 * 366; // ~1 year
|
|
5
|
+
|
|
6
|
+
export const MetricsIntervalSchema = z.enum(['hour', 'day']);
|
|
7
|
+
|
|
8
|
+
export type MetricsInterval = z.infer<typeof MetricsIntervalSchema>;
|
|
9
|
+
|
|
10
|
+
export const MetricsQuerySchema = z
|
|
11
|
+
.object({
|
|
12
|
+
userId: z.string().optional(),
|
|
13
|
+
from: z.string().optional(),
|
|
14
|
+
to: z.string().optional(),
|
|
15
|
+
interval: MetricsIntervalSchema.optional().default('hour'),
|
|
16
|
+
})
|
|
17
|
+
.superRefine((data, ctx) => {
|
|
18
|
+
const fromDate = data.from ? new Date(data.from) : undefined;
|
|
19
|
+
const toDate = data.to ? new Date(data.to) : undefined;
|
|
20
|
+
|
|
21
|
+
const fromValid = fromDate !== undefined && !Number.isNaN(fromDate.getTime());
|
|
22
|
+
const toValid = toDate !== undefined && !Number.isNaN(toDate.getTime());
|
|
23
|
+
|
|
24
|
+
if (data.from !== undefined && !fromValid) {
|
|
25
|
+
ctx.addIssue({ code: 'custom', path: ['from'], message: 'Invalid from date' });
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (data.to !== undefined && !toValid) {
|
|
29
|
+
ctx.addIssue({ code: 'custom', path: ['to'], message: 'Invalid to date' });
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (!fromValid || !toValid || !fromDate || !toDate) {
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (fromDate.getTime() > toDate.getTime()) {
|
|
37
|
+
ctx.addIssue({ code: 'custom', path: ['to'], message: 'from must be on or before to' });
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Unbounded windows let a single request scan the whole event table.
|
|
42
|
+
if (toDate.getTime() - fromDate.getTime() > MAX_METRICS_WINDOW_MS) {
|
|
43
|
+
ctx.addIssue({
|
|
44
|
+
code: 'custom',
|
|
45
|
+
path: ['to'],
|
|
46
|
+
message: 'time range exceeds the maximum window',
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
export type MetricsQuery = z.infer<typeof MetricsQuerySchema>;
|
|
52
|
+
|
|
53
|
+
export const AuthEventSummaryItemSchema = z.object({
|
|
54
|
+
type: z.string(),
|
|
55
|
+
count: z.number(),
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
export type AuthEventSummaryItem = z.infer<typeof AuthEventSummaryItemSchema>;
|
|
59
|
+
|
|
60
|
+
export const AuthEventSummaryResponseSchema = z.object({
|
|
61
|
+
summary: z.array(AuthEventSummaryItemSchema),
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
export type AuthEventSummaryResponse = z.infer<typeof AuthEventSummaryResponseSchema>;
|
|
65
|
+
|
|
66
|
+
export const AuthEventTimeseriesPointSchema = z.object({
|
|
67
|
+
bucket: z.string(),
|
|
68
|
+
success: z.number(),
|
|
69
|
+
failed: z.number(),
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
export type AuthEventTimeseriesPoint = z.infer<typeof AuthEventTimeseriesPointSchema>;
|
|
73
|
+
|
|
74
|
+
export const AuthEventTimeseriesResponseSchema = z.object({
|
|
75
|
+
timeseries: z.array(AuthEventTimeseriesPointSchema),
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
export type AuthEventTimeseriesResponse = z.infer<typeof AuthEventTimeseriesResponseSchema>;
|
|
79
|
+
|
|
80
|
+
export const LoginStatsResponseSchema = z.object({
|
|
81
|
+
success: z.number(),
|
|
82
|
+
failed: z.number(),
|
|
83
|
+
successRate: z.number(),
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
export type LoginStatsResponse = z.infer<typeof LoginStatsResponseSchema>;
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Anomaly rows come straight out of the event store, where a partially written
|
|
90
|
+
* event is still worth surfacing, so every field is optional here.
|
|
91
|
+
*/
|
|
92
|
+
export const PartialAuthEventSchema = AuthEventSchema.partial();
|
|
93
|
+
|
|
94
|
+
export type PartialAuthEvent = z.infer<typeof PartialAuthEventSchema>;
|
|
95
|
+
|
|
96
|
+
export const SecurityAnomaliesResponseSchema = z.object({
|
|
97
|
+
suspiciousEvents: z.array(PartialAuthEventSchema),
|
|
98
|
+
total: z.number().int().nonnegative(),
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
export type SecurityAnomaliesResponse = z.infer<typeof SecurityAnomaliesResponseSchema>;
|
|
102
|
+
|
|
103
|
+
export const DashboardMetricsResponseSchema = z.object({
|
|
104
|
+
totalUsers: z.number(),
|
|
105
|
+
activeSessions: z.number(),
|
|
106
|
+
newUsers24h: z.number(),
|
|
107
|
+
loginSuccess24h: z.number(),
|
|
108
|
+
loginFailed24h: z.number(),
|
|
109
|
+
successRate24h: z.number(),
|
|
110
|
+
otpUsage24h: z.number(),
|
|
111
|
+
passkeyUsage24h: z.number(),
|
|
112
|
+
databaseSize: z.number(),
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
export type DashboardMetricsResponse = z.infer<typeof DashboardMetricsResponseSchema>;
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { RefreshSuccessResponseSchema } from '../auth/auth.schema.js';
|
|
3
|
+
import { OAuthProviderIdSchema } from '../systemConfig/schema.js';
|
|
4
|
+
|
|
5
|
+
export const OAuthProviderParamSchema = z.object({
|
|
6
|
+
providerId: OAuthProviderIdSchema,
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
export type OAuthProviderParam = z.infer<typeof OAuthProviderParamSchema>;
|
|
10
|
+
|
|
11
|
+
/** A provider as an unauthenticated client may see it: no client or secret material. */
|
|
12
|
+
export const PublicOAuthProviderSchema = z.object({
|
|
13
|
+
id: z.string(),
|
|
14
|
+
name: z.string(),
|
|
15
|
+
scopes: z.array(z.string()),
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
export type PublicOAuthProvider = z.infer<typeof PublicOAuthProviderSchema>;
|
|
19
|
+
|
|
20
|
+
export const OAuthProvidersResponseSchema = z.object({
|
|
21
|
+
providers: z.array(PublicOAuthProviderSchema),
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
export type OAuthProvidersResponse = z.infer<typeof OAuthProvidersResponseSchema>;
|
|
25
|
+
|
|
26
|
+
export const StartOAuthLoginRequestSchema = z.object({
|
|
27
|
+
redirectUri: z.url().optional(),
|
|
28
|
+
returnTo: z.url().optional(),
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
export type StartOAuthLoginRequest = z.infer<typeof StartOAuthLoginRequestSchema>;
|
|
32
|
+
|
|
33
|
+
export const StartOAuthLoginResponseSchema = z.object({
|
|
34
|
+
provider: PublicOAuthProviderSchema,
|
|
35
|
+
state: z.string(),
|
|
36
|
+
authorizationUrl: z.url(),
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
export type StartOAuthLoginResponse = z.infer<typeof StartOAuthLoginResponseSchema>;
|
|
40
|
+
|
|
41
|
+
export const FinishOAuthLoginRequestSchema = z.object({
|
|
42
|
+
code: z.string().trim().min(1),
|
|
43
|
+
state: z.string().trim().min(1),
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
export type FinishOAuthLoginRequest = z.infer<typeof FinishOAuthLoginRequestSchema>;
|
|
47
|
+
|
|
48
|
+
export const OAuthLoginSuccessResponseSchema = RefreshSuccessResponseSchema.omit({
|
|
49
|
+
sessionId: true,
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
export type OAuthLoginSuccessResponse = z.infer<typeof OAuthLoginSuccessResponseSchema>;
|
|
53
|
+
|
|
54
|
+
export const OAUTH_ERROR_CODES = [
|
|
55
|
+
'oauth_missing_email',
|
|
56
|
+
'oauth_email_not_verified',
|
|
57
|
+
'oauth_missing_subject',
|
|
58
|
+
] as const;
|
|
59
|
+
|
|
60
|
+
export const OAuthErrorCodeSchema = z.enum(OAUTH_ERROR_CODES);
|
|
61
|
+
|
|
62
|
+
export type OAuthErrorCode = z.infer<typeof OAuthErrorCodeSchema>;
|
|
63
|
+
|
|
64
|
+
export const OAuthLoginErrorResponseSchema = z.object({
|
|
65
|
+
message: z.string().optional(),
|
|
66
|
+
error: z.string(),
|
|
67
|
+
code: OAuthErrorCodeSchema.optional(),
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
export type OAuthLoginErrorResponse = z.infer<typeof OAuthLoginErrorResponseSchema>;
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { IsoDate } from '../../shared.js';
|
|
3
|
+
import { MetadataSchema } from '../common/schema.js';
|
|
4
|
+
|
|
5
|
+
export const OrganizationIdParamSchema = z.object({
|
|
6
|
+
organizationId: z.uuid(),
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
export type OrganizationIdParam = z.infer<typeof OrganizationIdParamSchema>;
|
|
10
|
+
|
|
11
|
+
export const OrganizationMemberParamSchema = OrganizationIdParamSchema.extend({
|
|
12
|
+
userId: z.uuid(),
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
export type OrganizationMemberParam = z.infer<typeof OrganizationMemberParamSchema>;
|
|
16
|
+
|
|
17
|
+
export const CreateOrganizationRequestSchema = z.object({
|
|
18
|
+
name: z.string().trim().min(1).max(120),
|
|
19
|
+
slug: z.string().trim().min(1).max(100).optional(),
|
|
20
|
+
metadata: MetadataSchema,
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
export type CreateOrganizationRequest = z.infer<typeof CreateOrganizationRequestSchema>;
|
|
24
|
+
|
|
25
|
+
export const UpdateOrganizationRequestSchema = z.object({
|
|
26
|
+
name: z.string().trim().min(1).max(120).optional(),
|
|
27
|
+
slug: z.string().trim().min(1).max(100).optional(),
|
|
28
|
+
metadata: MetadataSchema,
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
export type UpdateOrganizationRequest = z.infer<typeof UpdateOrganizationRequestSchema>;
|
|
32
|
+
|
|
33
|
+
const MembershipRoleSchema = z.string().trim().min(1).max(80);
|
|
34
|
+
const MembershipScopeSchema = z.string().trim().min(1).max(120);
|
|
35
|
+
|
|
36
|
+
export const AddOrganizationMemberRequestSchema = z
|
|
37
|
+
.object({
|
|
38
|
+
userId: z.uuid().optional(),
|
|
39
|
+
email: z.email().optional(),
|
|
40
|
+
roles: z.array(MembershipRoleSchema).optional(),
|
|
41
|
+
scopes: z.array(MembershipScopeSchema).optional(),
|
|
42
|
+
})
|
|
43
|
+
.refine((value) => Boolean(value.userId || value.email), {
|
|
44
|
+
message: 'userId or email is required',
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
export type AddOrganizationMemberRequest = z.infer<typeof AddOrganizationMemberRequestSchema>;
|
|
48
|
+
|
|
49
|
+
export const UpdateOrganizationMemberRequestSchema = z.object({
|
|
50
|
+
roles: z.array(MembershipRoleSchema).optional(),
|
|
51
|
+
scopes: z.array(MembershipScopeSchema).optional(),
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
export type UpdateOrganizationMemberRequest = z.infer<typeof UpdateOrganizationMemberRequestSchema>;
|
|
55
|
+
|
|
56
|
+
const OrganizationMembershipUserSchema = z.object({
|
|
57
|
+
id: z.string(),
|
|
58
|
+
email: z.email(),
|
|
59
|
+
phone: z.string().nullable(),
|
|
60
|
+
roles: z.array(z.string()),
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
export const OrganizationMembershipSchema = z.object({
|
|
64
|
+
id: z.string(),
|
|
65
|
+
organizationId: z.string(),
|
|
66
|
+
userId: z.string(),
|
|
67
|
+
roles: z.array(z.string()),
|
|
68
|
+
scopes: z.array(z.string()),
|
|
69
|
+
createdAt: IsoDate,
|
|
70
|
+
updatedAt: IsoDate,
|
|
71
|
+
user: OrganizationMembershipUserSchema.optional(),
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
export type OrganizationMembership = z.infer<typeof OrganizationMembershipSchema>;
|
|
75
|
+
|
|
76
|
+
export const OrganizationSchema = z.object({
|
|
77
|
+
id: z.string(),
|
|
78
|
+
name: z.string(),
|
|
79
|
+
slug: z.string(),
|
|
80
|
+
createdByUserId: z.string().nullable(),
|
|
81
|
+
metadata: z.record(z.string(), z.unknown()).nullable(),
|
|
82
|
+
createdAt: IsoDate,
|
|
83
|
+
updatedAt: IsoDate,
|
|
84
|
+
membership: OrganizationMembershipSchema.optional(),
|
|
85
|
+
memberCount: z.number().int().nonnegative().optional(),
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
export type Organization = z.infer<typeof OrganizationSchema>;
|
|
89
|
+
|
|
90
|
+
export const OrganizationEnvelopeResponseSchema = z.object({
|
|
91
|
+
organization: OrganizationSchema,
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
export type OrganizationEnvelopeResponse = z.infer<typeof OrganizationEnvelopeResponseSchema>;
|
|
95
|
+
|
|
96
|
+
export const OrganizationListResponseSchema = z.object({
|
|
97
|
+
organizations: z.array(OrganizationSchema),
|
|
98
|
+
activeOrganizationId: z.string().nullable(),
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
export type OrganizationListResponse = z.infer<typeof OrganizationListResponseSchema>;
|
|
102
|
+
|
|
103
|
+
export const AdminOrganizationListResponseSchema = z.object({
|
|
104
|
+
organizations: z.array(OrganizationSchema),
|
|
105
|
+
total: z.number().int().nonnegative(),
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
export type AdminOrganizationListResponse = z.infer<typeof AdminOrganizationListResponseSchema>;
|
|
109
|
+
|
|
110
|
+
export const OrganizationMembersResponseSchema = z.object({
|
|
111
|
+
members: z.array(OrganizationMembershipSchema),
|
|
112
|
+
total: z.number().int().nonnegative(),
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
export type OrganizationMembersResponse = z.infer<typeof OrganizationMembersResponseSchema>;
|
|
116
|
+
|
|
117
|
+
export const OrganizationMembershipEnvelopeResponseSchema = z.object({
|
|
118
|
+
membership: OrganizationMembershipSchema,
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
export type OrganizationMembershipEnvelopeResponse = z.infer<
|
|
122
|
+
typeof OrganizationMembershipEnvelopeResponseSchema
|
|
123
|
+
>;
|
|
124
|
+
|
|
125
|
+
export const OrganizationSwitchResponseSchema = z.object({
|
|
126
|
+
message: z.string(),
|
|
127
|
+
token: z.string(),
|
|
128
|
+
sub: z.string(),
|
|
129
|
+
sessionId: z.string(),
|
|
130
|
+
organizationId: z.string(),
|
|
131
|
+
organization: OrganizationSchema,
|
|
132
|
+
ttl: z.number(),
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
export type OrganizationSwitchResponse = z.infer<typeof OrganizationSwitchResponseSchema>;
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Role matching logic, deliberately free of any Zod import. Consumers on the
|
|
3
|
+
* authorization hot path can reach this through the `./role/matching` subpath
|
|
4
|
+
* export without pulling Zod into their dependency tree.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Roles are colon-scoped (`billing:invoices:read`). Underscores, slashes, and
|
|
9
|
+
* whitespace are excluded so a role name is always safe to embed in a JWT claim,
|
|
10
|
+
* a URL path segment, and a config file without escaping.
|
|
11
|
+
*/
|
|
12
|
+
export const ROLE_NAME_PATTERN = /^(?!.*[_/\\\s])(?=.{1,80}$)[A-Za-z0-9-]+(?::[A-Za-z0-9-]+)*$/;
|
|
13
|
+
|
|
14
|
+
function normalizeRole(value: string) {
|
|
15
|
+
return value.trim();
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function samePrefix(left: string[], right: string[]) {
|
|
19
|
+
return left.length === right.length && left.every((part, index) => part === right[index]);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Whether a granted role satisfies a required role. A `:*` suffix grants every
|
|
24
|
+
* role under that prefix, an unscoped role grants every role beneath it, and a
|
|
25
|
+
* `:write` role implies the matching `:read` role.
|
|
26
|
+
*/
|
|
27
|
+
export function roleGrantsAccess(grantedRole: string, requiredRole: string): boolean {
|
|
28
|
+
const granted = normalizeRole(grantedRole);
|
|
29
|
+
const required = normalizeRole(requiredRole);
|
|
30
|
+
|
|
31
|
+
if (!granted || !required) {
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (granted === required) {
|
|
36
|
+
return true;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (granted.endsWith(':*')) {
|
|
40
|
+
const prefix = granted.slice(0, -2);
|
|
41
|
+
return required === prefix || required.startsWith(`${prefix}:`);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (!required.includes(':')) {
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (!granted.includes(':')) {
|
|
49
|
+
return required.startsWith(`${granted}:`);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const grantedParts = granted.split(':');
|
|
53
|
+
const requiredParts = required.split(':');
|
|
54
|
+
const grantedAction = grantedParts[grantedParts.length - 1];
|
|
55
|
+
const requiredAction = requiredParts[requiredParts.length - 1];
|
|
56
|
+
|
|
57
|
+
return (
|
|
58
|
+
grantedAction === 'write' &&
|
|
59
|
+
requiredAction === 'read' &&
|
|
60
|
+
samePrefix(grantedParts.slice(0, -1), requiredParts.slice(0, -1))
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Whether any granted role satisfies at least one of the required roles. */
|
|
65
|
+
export function hasScopedRole(grantedRoles: unknown, requiredRoles: string | string[]): boolean {
|
|
66
|
+
if (!Array.isArray(grantedRoles)) {
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const required = Array.isArray(requiredRoles) ? requiredRoles : [requiredRoles];
|
|
71
|
+
const granted = grantedRoles.filter((role): role is string => typeof role === 'string');
|
|
72
|
+
|
|
73
|
+
return required.some((requiredRole) =>
|
|
74
|
+
granted.some((grantedRole) => roleGrantsAccess(grantedRole, requiredRole)),
|
|
75
|
+
);
|
|
76
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
|
|
3
|
+
import { ROLE_NAME_PATTERN } from './matching.js';
|
|
4
|
+
|
|
5
|
+
export * from './matching.js';
|
|
6
|
+
|
|
7
|
+
export const RoleNameSchema = z.string().trim().regex(ROLE_NAME_PATTERN);
|
|
8
|
+
|
|
9
|
+
export type RoleName = z.infer<typeof RoleNameSchema>;
|
|
10
|
+
|
|
11
|
+
/** @deprecated Use {@link RoleNameSchema}. */
|
|
12
|
+
export const RoleSchema = RoleNameSchema;
|
|
13
|
+
|
|
14
|
+
/** @deprecated Use {@link RoleName}. */
|
|
15
|
+
export type Role = z.infer<typeof RoleSchema>;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
|
|
3
|
+
export const SessionSchema = z.object({
|
|
4
|
+
id: z.string(),
|
|
5
|
+
deviceName: z.string().nullable().optional(),
|
|
6
|
+
ipAddress: z.string().nullable().optional(),
|
|
7
|
+
userAgent: z.string().nullable().optional(),
|
|
8
|
+
lastUsedAt: z.string(),
|
|
9
|
+
expiresAt: z.string(),
|
|
10
|
+
current: z.boolean(),
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
export type Session = z.infer<typeof SessionSchema>;
|
|
14
|
+
|
|
15
|
+
export const SessionIdParamsSchema = z.object({
|
|
16
|
+
id: z.string(),
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
export type SessionIdParams = z.infer<typeof SessionIdParamsSchema>;
|
|
20
|
+
|
|
21
|
+
export const SessionListResponseSchema = z.object({
|
|
22
|
+
sessions: z.array(SessionSchema),
|
|
23
|
+
total: z.number(),
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
export type SessionListResponse = z.infer<typeof SessionListResponseSchema>;
|