@shipfox/api-auth-dto 18.0.0 → 19.0.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/.turbo/turbo-build.log +1 -1
- package/CHANGELOG.md +11 -0
- package/dist/schemas/admin.d.ts +33 -0
- package/dist/schemas/admin.d.ts.map +1 -1
- package/dist/schemas/admin.js +47 -0
- package/dist/schemas/admin.js.map +1 -1
- package/dist/schemas/index.d.ts +1 -1
- package/dist/schemas/index.d.ts.map +1 -1
- package/dist/schemas/index.js +1 -1
- package/dist/schemas/index.js.map +1 -1
- package/dist/tsconfig.test.tsbuildinfo +1 -1
- package/package.json +1 -1
- package/src/schemas/admin.ts +92 -0
- package/src/schemas/administrator-user-directory.test.ts +148 -0
- package/src/schemas/index.ts +4 -0
- package/tsconfig.build.tsbuildinfo +1 -1
package/package.json
CHANGED
package/src/schemas/admin.ts
CHANGED
|
@@ -13,6 +13,22 @@ export function isAdminRole(value: unknown): value is AdminRole {
|
|
|
13
13
|
const timestampSchema = z.string().datetime();
|
|
14
14
|
const identifierEmailSchema = z.string().email();
|
|
15
15
|
const CONTROL_OR_FORMAT_CHARACTER_RE = /[\p{Cc}\p{Cf}]/u;
|
|
16
|
+
const DIRECTORY_CURSOR_MAX_LENGTH = 512;
|
|
17
|
+
const DIRECTORY_SEARCH_MAX_LENGTH = 128;
|
|
18
|
+
const DIRECTORY_SEARCH_MAX_UTF16_LENGTH = DIRECTORY_SEARCH_MAX_LENGTH * 2;
|
|
19
|
+
const DIRECTORY_PAGE_SIZE_MAX = 100;
|
|
20
|
+
const DIRECTORY_LIMIT_PATTERN = /^\d+$/u;
|
|
21
|
+
|
|
22
|
+
function hasAtMostCodePoints(value: string, maxLength: number): boolean {
|
|
23
|
+
let length = 0;
|
|
24
|
+
for (const _codePoint of value) {
|
|
25
|
+
length += 1;
|
|
26
|
+
if (length > maxLength) return false;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return true;
|
|
30
|
+
}
|
|
31
|
+
|
|
16
32
|
const reasonSchema = z
|
|
17
33
|
.string()
|
|
18
34
|
.min(1)
|
|
@@ -87,6 +103,82 @@ export const administratorUserSummarySchema = administratorUserIdentitySchema.ex
|
|
|
87
103
|
|
|
88
104
|
export type AdministratorUserSummaryDto = z.infer<typeof administratorUserSummarySchema>;
|
|
89
105
|
|
|
106
|
+
const directoryCursorSchema = z
|
|
107
|
+
.string()
|
|
108
|
+
.min(1)
|
|
109
|
+
.max(DIRECTORY_CURSOR_MAX_LENGTH)
|
|
110
|
+
.refine((value) => !CONTROL_OR_FORMAT_CHARACTER_RE.test(value), {
|
|
111
|
+
message: 'must not contain control or format characters',
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
const directorySearchSchema = z
|
|
115
|
+
.string()
|
|
116
|
+
.max(DIRECTORY_SEARCH_MAX_UTF16_LENGTH)
|
|
117
|
+
.optional()
|
|
118
|
+
.transform((value) => {
|
|
119
|
+
if (
|
|
120
|
+
typeof value === 'string' &&
|
|
121
|
+
!CONTROL_OR_FORMAT_CHARACTER_RE.test(value) &&
|
|
122
|
+
value.trim().length === 0
|
|
123
|
+
) {
|
|
124
|
+
return undefined;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return value;
|
|
128
|
+
})
|
|
129
|
+
.pipe(
|
|
130
|
+
z
|
|
131
|
+
.string()
|
|
132
|
+
.refine((value) => !CONTROL_OR_FORMAT_CHARACTER_RE.test(value), {
|
|
133
|
+
message: 'must not contain control or format characters',
|
|
134
|
+
})
|
|
135
|
+
.transform((value) => value.trim())
|
|
136
|
+
.pipe(
|
|
137
|
+
z
|
|
138
|
+
.string()
|
|
139
|
+
.min(1)
|
|
140
|
+
.refine((value) => hasAtMostCodePoints(value, DIRECTORY_SEARCH_MAX_LENGTH), {
|
|
141
|
+
message: `String must contain at most ${DIRECTORY_SEARCH_MAX_LENGTH} character(s)`,
|
|
142
|
+
}),
|
|
143
|
+
)
|
|
144
|
+
.optional(),
|
|
145
|
+
);
|
|
146
|
+
|
|
147
|
+
const checkedBooleanSchema = z.preprocess((value) => {
|
|
148
|
+
if (value === 'true') return true;
|
|
149
|
+
if (value === 'false') return false;
|
|
150
|
+
return value;
|
|
151
|
+
}, z.boolean());
|
|
152
|
+
|
|
153
|
+
const checkedDirectoryLimitSchema = z.preprocess((value) => {
|
|
154
|
+
if (typeof value === 'number' && Number.isInteger(value)) return value;
|
|
155
|
+
if (typeof value === 'string' && DIRECTORY_LIMIT_PATTERN.test(value)) return Number(value);
|
|
156
|
+
return value;
|
|
157
|
+
}, z.number().int().min(1).max(DIRECTORY_PAGE_SIZE_MAX));
|
|
158
|
+
|
|
159
|
+
export const administratorUserDirectoryQuerySchema = z
|
|
160
|
+
.object({
|
|
161
|
+
search: directorySearchSchema,
|
|
162
|
+
status: userStatusSchema.optional(),
|
|
163
|
+
impersonation_eligible: checkedBooleanSchema.optional(),
|
|
164
|
+
cursor: directoryCursorSchema.optional(),
|
|
165
|
+
limit: checkedDirectoryLimitSchema.default(50),
|
|
166
|
+
})
|
|
167
|
+
.strict();
|
|
168
|
+
|
|
169
|
+
export type AdministratorUserDirectoryQueryDto = z.infer<
|
|
170
|
+
typeof administratorUserDirectoryQuerySchema
|
|
171
|
+
>;
|
|
172
|
+
|
|
173
|
+
export const administratorUserDirectoryResponseSchema = z.object({
|
|
174
|
+
users: z.array(administratorUserSummarySchema).max(DIRECTORY_PAGE_SIZE_MAX),
|
|
175
|
+
next_cursor: directoryCursorSchema.nullable(),
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
export type AdministratorUserDirectoryResponseDto = z.infer<
|
|
179
|
+
typeof administratorUserDirectoryResponseSchema
|
|
180
|
+
>;
|
|
181
|
+
|
|
90
182
|
const correlationIdSchema = z.string().min(1).max(255);
|
|
91
183
|
|
|
92
184
|
export const suspendAdministratorUserBodySchema = z.object({
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import {describe, expect, it} from '@shipfox/vitest/vi';
|
|
2
|
+
import {
|
|
3
|
+
administratorUserDirectoryQuerySchema,
|
|
4
|
+
administratorUserDirectoryResponseSchema,
|
|
5
|
+
} from './admin.js';
|
|
6
|
+
|
|
7
|
+
const user = {
|
|
8
|
+
id: '11111111-1111-4111-8111-111111111111',
|
|
9
|
+
email: 'alex@example.com',
|
|
10
|
+
name: 'Alex Shipfox',
|
|
11
|
+
status: 'active' as const,
|
|
12
|
+
email_verified_at: '2026-08-31T12:00:00.000Z',
|
|
13
|
+
created_at: '2026-08-01T12:00:00.000Z',
|
|
14
|
+
admin_role: null,
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const parseQuery = (query: unknown) => administratorUserDirectoryQuerySchema.safeParse(query);
|
|
18
|
+
|
|
19
|
+
const parseResponse = (users: unknown[]) =>
|
|
20
|
+
administratorUserDirectoryResponseSchema.safeParse({users, next_cursor: null});
|
|
21
|
+
|
|
22
|
+
describe('administrator user directory query schema', () => {
|
|
23
|
+
it('defaults the page size and accepts every optional field', () => {
|
|
24
|
+
const query = administratorUserDirectoryQuerySchema.parse({
|
|
25
|
+
search: ' alex ',
|
|
26
|
+
status: 'suspended',
|
|
27
|
+
impersonation_eligible: 'true',
|
|
28
|
+
cursor: 'eyJjcmVhdGVkX2F0IjoiMjAyNi0wOC0wMSJ9',
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
expect(query).toEqual({
|
|
32
|
+
search: 'alex',
|
|
33
|
+
status: 'suspended',
|
|
34
|
+
impersonation_eligible: true,
|
|
35
|
+
cursor: 'eyJjcmVhdGVkX2F0IjoiMjAyNi0wOC0wMSJ9',
|
|
36
|
+
limit: 50,
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('accepts valid UUIDs as exact search values', () => {
|
|
41
|
+
const uuid = '22222222-2222-4222-8222-222222222222';
|
|
42
|
+
|
|
43
|
+
expect(administratorUserDirectoryQuerySchema.parse({search: uuid}).search).toBe(uuid);
|
|
44
|
+
expect(administratorUserDirectoryQuerySchema.parse({search: ` ${uuid} `}).search).toBe(uuid);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('accepts text searches from one through 128 visible code points', () => {
|
|
48
|
+
expect(parseQuery({search: 'a'}).success).toBe(true);
|
|
49
|
+
expect(parseQuery({search: 'a'.repeat(128)}).success).toBe(true);
|
|
50
|
+
expect(parseQuery({search: 'a'.repeat(129)}).success).toBe(false);
|
|
51
|
+
expect(parseQuery({search: '😀'.repeat(128)}).success).toBe(true);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('treats an empty search as omitted', () => {
|
|
55
|
+
expect(parseQuery({search: ''}).success).toBe(true);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('rejects control and format characters in searches', () => {
|
|
59
|
+
for (const search of ['alex\nshipfox', 'alex\u0000shipfox', 'alex\u200Bshipfox', '\n', '\t']) {
|
|
60
|
+
expect(parseQuery({search})).toMatchObject({success: false});
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('keeps wildcard characters literal and trims only outer whitespace', () => {
|
|
65
|
+
const query = administratorUserDirectoryQuerySchema.parse({
|
|
66
|
+
search: ' 100%_\\ alex shipfox ',
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
expect(query.search).toBe('100%_\\ alex shipfox');
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('treats a blank search as omitted', () => {
|
|
73
|
+
expect(administratorUserDirectoryQuerySchema.parse({search: ' '})).toEqual({limit: 50});
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('accepts each known status and rejects unknown statuses', () => {
|
|
77
|
+
for (const status of ['active', 'suspended', 'deleted']) {
|
|
78
|
+
expect(parseQuery({status}).success).toBe(true);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
expect(parseQuery({status: 'pending'}).success).toBe(false);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('coerces only checked boolean query values', () => {
|
|
85
|
+
expect(
|
|
86
|
+
administratorUserDirectoryQuerySchema.parse({impersonation_eligible: 'true'}),
|
|
87
|
+
).toMatchObject({impersonation_eligible: true});
|
|
88
|
+
expect(
|
|
89
|
+
administratorUserDirectoryQuerySchema.parse({impersonation_eligible: 'false'}),
|
|
90
|
+
).toMatchObject({impersonation_eligible: false});
|
|
91
|
+
expect(
|
|
92
|
+
administratorUserDirectoryQuerySchema.parse({impersonation_eligible: true}),
|
|
93
|
+
).toMatchObject({impersonation_eligible: true});
|
|
94
|
+
|
|
95
|
+
for (const value of ['1', '0', 'yes', 'TRUE', '']) {
|
|
96
|
+
expect(parseQuery({impersonation_eligible: value}).success).toBe(false);
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('bounds opaque cursors and rejects control or format characters', () => {
|
|
101
|
+
expect(parseQuery({cursor: 'a'}).success).toBe(true);
|
|
102
|
+
expect(parseQuery({cursor: 'not a cursor'}).success).toBe(true);
|
|
103
|
+
expect(parseQuery({cursor: 'a'.repeat(512)}).success).toBe(true);
|
|
104
|
+
expect(parseQuery({cursor: 'a'.repeat(513)}).success).toBe(false);
|
|
105
|
+
|
|
106
|
+
for (const cursor of ['cursor\nvalue', 'cursor\u0000value', 'cursor\u200Bvalue']) {
|
|
107
|
+
expect(parseQuery({cursor}).success).toBe(false);
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it('coerces page sizes and enforces both bounds', () => {
|
|
112
|
+
expect(administratorUserDirectoryQuerySchema.parse({limit: '1'}).limit).toBe(1);
|
|
113
|
+
expect(administratorUserDirectoryQuerySchema.parse({limit: '100'}).limit).toBe(100);
|
|
114
|
+
expect(parseQuery({limit: '0'}).success).toBe(false);
|
|
115
|
+
expect(parseQuery({limit: '101'}).success).toBe(false);
|
|
116
|
+
expect(parseQuery({limit: '1.5'}).success).toBe(false);
|
|
117
|
+
expect(parseQuery({limit: true}).success).toBe(false);
|
|
118
|
+
expect(parseQuery({limit: 'abc'}).success).toBe(false);
|
|
119
|
+
expect(parseQuery({limit: '1e2'}).success).toBe(false);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it('rejects unknown query fields', () => {
|
|
123
|
+
expect(parseQuery({unexpected: 'value'}).success).toBe(false);
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
describe('administrator user directory response schema', () => {
|
|
128
|
+
it('accepts an empty response with no next page', () => {
|
|
129
|
+
expect(parseResponse([])).toMatchObject({success: true});
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it('accepts a complete user summary and a nullable next cursor', () => {
|
|
133
|
+
const result = administratorUserDirectoryResponseSchema.parse({
|
|
134
|
+
users: [{...user, admin_role: 'admin-owner'}],
|
|
135
|
+
next_cursor: 'eyJjdXJzb3IiOiJuZXh0In0',
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
expect(result).toEqual({
|
|
139
|
+
users: [{...user, admin_role: 'admin-owner'}],
|
|
140
|
+
next_cursor: 'eyJjdXJzb3IiOiJuZXh0In0',
|
|
141
|
+
});
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it('accepts 100 users and rejects a 101st user', () => {
|
|
145
|
+
expect(parseResponse(Array.from({length: 100}, () => user)).success).toBe(true);
|
|
146
|
+
expect(parseResponse(Array.from({length: 101}, () => user)).success).toBe(false);
|
|
147
|
+
});
|
|
148
|
+
});
|
package/src/schemas/index.ts
CHANGED
|
@@ -12,6 +12,8 @@ export {
|
|
|
12
12
|
type AdminBootstrapStateDto,
|
|
13
13
|
type AdminGrantDto,
|
|
14
14
|
type AdministratorGrantSummaryDto,
|
|
15
|
+
type AdministratorUserDirectoryQueryDto,
|
|
16
|
+
type AdministratorUserDirectoryResponseDto,
|
|
15
17
|
type AdministratorUserIdentityDto,
|
|
16
18
|
type AdministratorUserLookupQueryDto,
|
|
17
19
|
type AdministratorUserMutationResponseDto,
|
|
@@ -20,6 +22,8 @@ export {
|
|
|
20
22
|
adminBootstrapStateSchema,
|
|
21
23
|
adminGrantDtoSchema,
|
|
22
24
|
administratorGrantSummarySchema,
|
|
25
|
+
administratorUserDirectoryQuerySchema,
|
|
26
|
+
administratorUserDirectoryResponseSchema,
|
|
23
27
|
administratorUserIdentitySchema,
|
|
24
28
|
administratorUserLookupQuerySchema,
|
|
25
29
|
administratorUserMutationResponseSchema,
|