@open-mercato/onboarding 0.6.5-develop.5099.1.85c0aff78c → 0.6.5-develop.5116.1.f0af9e5080
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.
|
@@ -10,9 +10,27 @@ const metadata = {
|
|
|
10
10
|
requireAuth: false
|
|
11
11
|
}
|
|
12
12
|
};
|
|
13
|
+
const ONBOARDING_LOGIN_TENANT_COOKIE = "om_login_tenant";
|
|
13
14
|
const onboardingStatusQuerySchema = z.object({
|
|
14
15
|
tenantId: z.string().uuid()
|
|
15
16
|
});
|
|
17
|
+
function readCookie(req, name) {
|
|
18
|
+
const header = req.headers.get("cookie");
|
|
19
|
+
if (!header) return null;
|
|
20
|
+
for (const part of header.split(";")) {
|
|
21
|
+
const separatorIndex = part.indexOf("=");
|
|
22
|
+
if (separatorIndex === -1) continue;
|
|
23
|
+
const key = part.slice(0, separatorIndex).trim();
|
|
24
|
+
if (key !== name) continue;
|
|
25
|
+
const rawValue = part.slice(separatorIndex + 1).trim();
|
|
26
|
+
try {
|
|
27
|
+
return decodeURIComponent(rawValue);
|
|
28
|
+
} catch {
|
|
29
|
+
return rawValue;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
16
34
|
async function GET(req) {
|
|
17
35
|
const url = new URL(req.url);
|
|
18
36
|
const tenantId = url.searchParams.get("tenantId") || url.searchParams.get("tenant") || "";
|
|
@@ -20,6 +38,10 @@ async function GET(req) {
|
|
|
20
38
|
if (!parsed.success) {
|
|
21
39
|
return NextResponse.json({ ok: false, error: "Invalid tenant id." }, { status: 400 });
|
|
22
40
|
}
|
|
41
|
+
const loginTenantCookie = readCookie(req, ONBOARDING_LOGIN_TENANT_COOKIE);
|
|
42
|
+
if (!loginTenantCookie || loginTenantCookie !== parsed.data.tenantId) {
|
|
43
|
+
return NextResponse.json({ ok: false, error: "Not authorized for this tenant." }, { status: 403 });
|
|
44
|
+
}
|
|
23
45
|
let baseUrl;
|
|
24
46
|
try {
|
|
25
47
|
baseUrl = getSecurityEmailBaseUrl(req);
|
|
@@ -88,6 +110,7 @@ const onboardingStatusDoc = {
|
|
|
88
110
|
],
|
|
89
111
|
errors: [
|
|
90
112
|
{ status: 400, description: "Invalid tenant id or request origin.", schema: onboardingStatusErrorSchema },
|
|
113
|
+
{ status: 403, description: "Caller is not authorized for this tenant.", schema: onboardingStatusErrorSchema },
|
|
91
114
|
{ status: 404, description: "Onboarding request not found.", schema: onboardingStatusErrorSchema },
|
|
92
115
|
{ status: 500, description: "Onboarding status is not configured.", schema: onboardingStatusErrorSchema }
|
|
93
116
|
]
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../../src/modules/onboarding/api/get/onboarding/status.ts"],
|
|
4
|
-
"sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { getSecurityEmailBaseUrl, mapSecurityEmailUrlError } from '@open-mercato/shared/lib/url'\nimport { OnboardingService } from '@open-mercato/onboarding/modules/onboarding/lib/service'\nimport { sendWorkspaceReadyEmail } from '@open-mercato/onboarding/modules/onboarding/lib/ready-email'\nimport type { OpenApiMethodDoc, OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\n\nexport const metadata = {\n path: '/onboarding/onboarding/status',\n GET: {\n requireAuth: false,\n },\n}\n\nconst onboardingStatusQuerySchema = z.object({\n tenantId: z.string().uuid(),\n})\n\nexport async function GET(req: Request) {\n const url = new URL(req.url)\n const tenantId = url.searchParams.get('tenantId') || url.searchParams.get('tenant') || ''\n const parsed = onboardingStatusQuerySchema.safeParse({ tenantId })\n if (!parsed.success) {\n return NextResponse.json({ ok: false, error: 'Invalid tenant id.' }, { status: 400 })\n }\n\n let baseUrl: string\n try {\n baseUrl = getSecurityEmailBaseUrl(req)\n } catch (error) {\n const mapped = mapSecurityEmailUrlError(error, {\n scope: 'onboarding.status',\n configMessage: 'Onboarding status is not configured.',\n })\n if (mapped) return NextResponse.json({ ok: false, error: mapped.body.error }, { status: mapped.status })\n throw error\n }\n const container = await createRequestContainer()\n const em = container.resolve('em') as EntityManager\n const service = new OnboardingService(em)\n const request = await service.findLatestByTenantId(parsed.data.tenantId)\n if (!request) {\n return NextResponse.json({ ok: false, error: 'Onboarding request not found.' }, { status: 404 })\n }\n\n let emailSent = Boolean(request.readyEmailSentAt)\n const ready = request.status === 'completed' && Boolean(request.preparationCompletedAt)\n const loginUrl = ready && request.tenantId ? `${baseUrl}/login?tenant=${encodeURIComponent(request.tenantId)}` : null\n\n if (ready && request.tenantId && !request.readyEmailSentAt) {\n try {\n emailSent = await sendWorkspaceReadyEmail({\n requestId: request.id,\n tenantId: request.tenantId,\n })\n } catch (error) {\n console.error('[onboarding.status] ready email retry failed', {\n requestId: request.id,\n tenantId: request.tenantId,\n organizationId: request.organizationId,\n error,\n })\n }\n }\n\n return NextResponse.json({\n ok: true,\n status: request.status,\n ready,\n emailSent,\n tenantId: request.tenantId ?? parsed.data.tenantId,\n loginUrl,\n })\n}\n\nconst onboardingTag = 'Onboarding'\n\nconst onboardingStatusSuccessSchema = z.object({\n ok: z.literal(true),\n status: z.enum(['pending', 'processing', 'completed', 'expired']),\n ready: z.boolean(),\n emailSent: z.boolean(),\n tenantId: z.string().uuid(),\n loginUrl: z.string().nullable(),\n})\n\nconst onboardingStatusErrorSchema = z.object({\n ok: z.literal(false),\n error: z.string(),\n})\n\nconst onboardingStatusDoc: OpenApiMethodDoc = {\n summary: 'Get onboarding preparation status',\n description: 'Resolves whether a tenant workspace finished deferred onboarding preparation and can be opened.',\n tags: [onboardingTag],\n query: onboardingStatusQuerySchema,\n responses: [\n { status: 200, description: 'Onboarding status resolved.', schema: onboardingStatusSuccessSchema },\n ],\n errors: [\n { status: 400, description: 'Invalid tenant id or request origin.', schema: onboardingStatusErrorSchema },\n { status: 404, description: 'Onboarding request not found.', schema: onboardingStatusErrorSchema },\n { status: 500, description: 'Onboarding status is not configured.', schema: onboardingStatusErrorSchema },\n ],\n}\n\nexport const openApi: OpenApiRouteDoc = {\n tag: onboardingTag,\n summary: 'Onboarding preparation status',\n methods: {\n GET: onboardingStatusDoc,\n },\n}\n\nexport default GET\n"],
|
|
5
|
-
"mappings": "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",
|
|
4
|
+
"sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { getSecurityEmailBaseUrl, mapSecurityEmailUrlError } from '@open-mercato/shared/lib/url'\nimport { OnboardingService } from '@open-mercato/onboarding/modules/onboarding/lib/service'\nimport { sendWorkspaceReadyEmail } from '@open-mercato/onboarding/modules/onboarding/lib/ready-email'\nimport type { OpenApiMethodDoc, OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\n\nexport const metadata = {\n path: '/onboarding/onboarding/status',\n GET: {\n requireAuth: false,\n },\n}\n\nconst ONBOARDING_LOGIN_TENANT_COOKIE = 'om_login_tenant'\n\nconst onboardingStatusQuerySchema = z.object({\n tenantId: z.string().uuid(),\n})\n\nfunction readCookie(req: Request, name: string): string | null {\n const header = req.headers.get('cookie')\n if (!header) return null\n for (const part of header.split(';')) {\n const separatorIndex = part.indexOf('=')\n if (separatorIndex === -1) continue\n const key = part.slice(0, separatorIndex).trim()\n if (key !== name) continue\n const rawValue = part.slice(separatorIndex + 1).trim()\n try {\n return decodeURIComponent(rawValue)\n } catch {\n return rawValue\n }\n }\n return null\n}\n\nexport async function GET(req: Request) {\n const url = new URL(req.url)\n const tenantId = url.searchParams.get('tenantId') || url.searchParams.get('tenant') || ''\n const parsed = onboardingStatusQuerySchema.safeParse({ tenantId })\n if (!parsed.success) {\n return NextResponse.json({ ok: false, error: 'Invalid tenant id.' }, { status: 400 })\n }\n\n const loginTenantCookie = readCookie(req, ONBOARDING_LOGIN_TENANT_COOKIE)\n if (!loginTenantCookie || loginTenantCookie !== parsed.data.tenantId) {\n return NextResponse.json({ ok: false, error: 'Not authorized for this tenant.' }, { status: 403 })\n }\n\n let baseUrl: string\n try {\n baseUrl = getSecurityEmailBaseUrl(req)\n } catch (error) {\n const mapped = mapSecurityEmailUrlError(error, {\n scope: 'onboarding.status',\n configMessage: 'Onboarding status is not configured.',\n })\n if (mapped) return NextResponse.json({ ok: false, error: mapped.body.error }, { status: mapped.status })\n throw error\n }\n const container = await createRequestContainer()\n const em = container.resolve('em') as EntityManager\n const service = new OnboardingService(em)\n const request = await service.findLatestByTenantId(parsed.data.tenantId)\n if (!request) {\n return NextResponse.json({ ok: false, error: 'Onboarding request not found.' }, { status: 404 })\n }\n\n let emailSent = Boolean(request.readyEmailSentAt)\n const ready = request.status === 'completed' && Boolean(request.preparationCompletedAt)\n const loginUrl = ready && request.tenantId ? `${baseUrl}/login?tenant=${encodeURIComponent(request.tenantId)}` : null\n\n if (ready && request.tenantId && !request.readyEmailSentAt) {\n try {\n emailSent = await sendWorkspaceReadyEmail({\n requestId: request.id,\n tenantId: request.tenantId,\n })\n } catch (error) {\n console.error('[onboarding.status] ready email retry failed', {\n requestId: request.id,\n tenantId: request.tenantId,\n organizationId: request.organizationId,\n error,\n })\n }\n }\n\n return NextResponse.json({\n ok: true,\n status: request.status,\n ready,\n emailSent,\n tenantId: request.tenantId ?? parsed.data.tenantId,\n loginUrl,\n })\n}\n\nconst onboardingTag = 'Onboarding'\n\nconst onboardingStatusSuccessSchema = z.object({\n ok: z.literal(true),\n status: z.enum(['pending', 'processing', 'completed', 'expired']),\n ready: z.boolean(),\n emailSent: z.boolean(),\n tenantId: z.string().uuid(),\n loginUrl: z.string().nullable(),\n})\n\nconst onboardingStatusErrorSchema = z.object({\n ok: z.literal(false),\n error: z.string(),\n})\n\nconst onboardingStatusDoc: OpenApiMethodDoc = {\n summary: 'Get onboarding preparation status',\n description: 'Resolves whether a tenant workspace finished deferred onboarding preparation and can be opened.',\n tags: [onboardingTag],\n query: onboardingStatusQuerySchema,\n responses: [\n { status: 200, description: 'Onboarding status resolved.', schema: onboardingStatusSuccessSchema },\n ],\n errors: [\n { status: 400, description: 'Invalid tenant id or request origin.', schema: onboardingStatusErrorSchema },\n { status: 403, description: 'Caller is not authorized for this tenant.', schema: onboardingStatusErrorSchema },\n { status: 404, description: 'Onboarding request not found.', schema: onboardingStatusErrorSchema },\n { status: 500, description: 'Onboarding status is not configured.', schema: onboardingStatusErrorSchema },\n ],\n}\n\nexport const openApi: OpenApiRouteDoc = {\n tag: onboardingTag,\n summary: 'Onboarding preparation status',\n methods: {\n GET: onboardingStatusDoc,\n },\n}\n\nexport default GET\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@open-mercato/onboarding",
|
|
3
|
-
"version": "0.6.5-develop.
|
|
3
|
+
"version": "0.6.5-develop.5116.1.f0af9e5080",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"scripts": {
|
|
@@ -69,10 +69,10 @@
|
|
|
69
69
|
}
|
|
70
70
|
},
|
|
71
71
|
"peerDependencies": {
|
|
72
|
-
"@open-mercato/shared": "0.6.5-develop.
|
|
72
|
+
"@open-mercato/shared": "0.6.5-develop.5116.1.f0af9e5080"
|
|
73
73
|
},
|
|
74
74
|
"devDependencies": {
|
|
75
|
-
"@open-mercato/shared": "0.6.5-develop.
|
|
75
|
+
"@open-mercato/shared": "0.6.5-develop.5116.1.f0af9e5080",
|
|
76
76
|
"@types/jest": "^30.0.0",
|
|
77
77
|
"jest": "^30.4.2",
|
|
78
78
|
"ts-jest": "^29.4.11"
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { OnboardingRequest } from '../modules/onboarding/data/entities'
|
|
2
|
+
|
|
3
|
+
const findLatestByTenantId = jest.fn()
|
|
4
|
+
const sendWorkspaceReadyEmail = jest.fn()
|
|
5
|
+
|
|
6
|
+
jest.mock('@open-mercato/shared/lib/di/container', () => ({
|
|
7
|
+
createRequestContainer: jest.fn(async () => ({
|
|
8
|
+
resolve: (name: string) => {
|
|
9
|
+
if (name === 'em') return {}
|
|
10
|
+
throw new Error(`unexpected resolve(${name})`)
|
|
11
|
+
},
|
|
12
|
+
})),
|
|
13
|
+
}))
|
|
14
|
+
|
|
15
|
+
jest.mock('@open-mercato/shared/lib/url', () => ({
|
|
16
|
+
getSecurityEmailBaseUrl: () => 'https://app.example.com',
|
|
17
|
+
mapSecurityEmailUrlError: () => null,
|
|
18
|
+
}))
|
|
19
|
+
|
|
20
|
+
jest.mock('@open-mercato/onboarding/modules/onboarding/lib/service', () => ({
|
|
21
|
+
OnboardingService: jest.fn().mockImplementation(() => ({
|
|
22
|
+
findLatestByTenantId,
|
|
23
|
+
})),
|
|
24
|
+
}))
|
|
25
|
+
|
|
26
|
+
jest.mock('@open-mercato/onboarding/modules/onboarding/lib/ready-email', () => ({
|
|
27
|
+
sendWorkspaceReadyEmail,
|
|
28
|
+
}))
|
|
29
|
+
|
|
30
|
+
import { GET } from '../modules/onboarding/api/get/onboarding/status'
|
|
31
|
+
|
|
32
|
+
const TENANT_ID = '11111111-1111-4111-8111-111111111111'
|
|
33
|
+
const OTHER_TENANT_ID = '22222222-2222-4222-8222-222222222222'
|
|
34
|
+
|
|
35
|
+
function makeRequest(overrides: Record<string, unknown> = {}) {
|
|
36
|
+
return Object.assign(new OnboardingRequest(), {
|
|
37
|
+
id: 'req-1',
|
|
38
|
+
status: 'completed',
|
|
39
|
+
tenantId: TENANT_ID,
|
|
40
|
+
organizationId: 'org-1',
|
|
41
|
+
preparationCompletedAt: new Date(),
|
|
42
|
+
readyEmailSentAt: new Date(),
|
|
43
|
+
...overrides,
|
|
44
|
+
})
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function buildRequest(args: { tenantId: string; cookie?: string }) {
|
|
48
|
+
const headers = new Headers()
|
|
49
|
+
if (args.cookie !== undefined) headers.set('cookie', args.cookie)
|
|
50
|
+
return new Request(`https://app.example.com/api/onboarding/onboarding/status?tenantId=${args.tenantId}`, {
|
|
51
|
+
headers,
|
|
52
|
+
})
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
describe('onboarding status endpoint authorization', () => {
|
|
56
|
+
beforeEach(() => {
|
|
57
|
+
findLatestByTenantId.mockReset()
|
|
58
|
+
sendWorkspaceReadyEmail.mockReset()
|
|
59
|
+
findLatestByTenantId.mockResolvedValue(makeRequest())
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
it('returns 403 and does not look up tenant state when no om_login_tenant cookie is present', async () => {
|
|
63
|
+
const res = await GET(buildRequest({ tenantId: TENANT_ID }))
|
|
64
|
+
expect(res.status).toBe(403)
|
|
65
|
+
const body = await res.json()
|
|
66
|
+
expect(body).toEqual({ ok: false, error: 'Not authorized for this tenant.' })
|
|
67
|
+
expect(findLatestByTenantId).not.toHaveBeenCalled()
|
|
68
|
+
expect(sendWorkspaceReadyEmail).not.toHaveBeenCalled()
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
it('returns 403 when the om_login_tenant cookie is for a different tenant', async () => {
|
|
72
|
+
const res = await GET(
|
|
73
|
+
buildRequest({ tenantId: TENANT_ID, cookie: `om_login_tenant=${OTHER_TENANT_ID}` }),
|
|
74
|
+
)
|
|
75
|
+
expect(res.status).toBe(403)
|
|
76
|
+
expect(findLatestByTenantId).not.toHaveBeenCalled()
|
|
77
|
+
expect(sendWorkspaceReadyEmail).not.toHaveBeenCalled()
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
it('does not trigger the ready email side effect for an unauthorized caller', async () => {
|
|
81
|
+
findLatestByTenantId.mockResolvedValue(
|
|
82
|
+
makeRequest({ readyEmailSentAt: null }),
|
|
83
|
+
)
|
|
84
|
+
const res = await GET(
|
|
85
|
+
buildRequest({ tenantId: TENANT_ID, cookie: `om_login_tenant=${OTHER_TENANT_ID}` }),
|
|
86
|
+
)
|
|
87
|
+
expect(res.status).toBe(403)
|
|
88
|
+
expect(sendWorkspaceReadyEmail).not.toHaveBeenCalled()
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
it('returns the status when the om_login_tenant cookie matches the requested tenant', async () => {
|
|
92
|
+
const res = await GET(
|
|
93
|
+
buildRequest({ tenantId: TENANT_ID, cookie: `other=foo; om_login_tenant=${TENANT_ID}` }),
|
|
94
|
+
)
|
|
95
|
+
expect(res.status).toBe(200)
|
|
96
|
+
const body = await res.json()
|
|
97
|
+
expect(body.ok).toBe(true)
|
|
98
|
+
expect(body.tenantId).toBe(TENANT_ID)
|
|
99
|
+
expect(body.ready).toBe(true)
|
|
100
|
+
expect(body.loginUrl).toBe(`https://app.example.com/login?tenant=${TENANT_ID}`)
|
|
101
|
+
expect(findLatestByTenantId).toHaveBeenCalledWith(TENANT_ID)
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
it('returns 404 for an authorized caller when no onboarding record exists', async () => {
|
|
105
|
+
findLatestByTenantId.mockResolvedValue(null)
|
|
106
|
+
const res = await GET(
|
|
107
|
+
buildRequest({ tenantId: TENANT_ID, cookie: `om_login_tenant=${TENANT_ID}` }),
|
|
108
|
+
)
|
|
109
|
+
expect(res.status).toBe(404)
|
|
110
|
+
})
|
|
111
|
+
})
|
|
@@ -14,10 +14,30 @@ export const metadata = {
|
|
|
14
14
|
},
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
+
const ONBOARDING_LOGIN_TENANT_COOKIE = 'om_login_tenant'
|
|
18
|
+
|
|
17
19
|
const onboardingStatusQuerySchema = z.object({
|
|
18
20
|
tenantId: z.string().uuid(),
|
|
19
21
|
})
|
|
20
22
|
|
|
23
|
+
function readCookie(req: Request, name: string): string | null {
|
|
24
|
+
const header = req.headers.get('cookie')
|
|
25
|
+
if (!header) return null
|
|
26
|
+
for (const part of header.split(';')) {
|
|
27
|
+
const separatorIndex = part.indexOf('=')
|
|
28
|
+
if (separatorIndex === -1) continue
|
|
29
|
+
const key = part.slice(0, separatorIndex).trim()
|
|
30
|
+
if (key !== name) continue
|
|
31
|
+
const rawValue = part.slice(separatorIndex + 1).trim()
|
|
32
|
+
try {
|
|
33
|
+
return decodeURIComponent(rawValue)
|
|
34
|
+
} catch {
|
|
35
|
+
return rawValue
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return null
|
|
39
|
+
}
|
|
40
|
+
|
|
21
41
|
export async function GET(req: Request) {
|
|
22
42
|
const url = new URL(req.url)
|
|
23
43
|
const tenantId = url.searchParams.get('tenantId') || url.searchParams.get('tenant') || ''
|
|
@@ -26,6 +46,11 @@ export async function GET(req: Request) {
|
|
|
26
46
|
return NextResponse.json({ ok: false, error: 'Invalid tenant id.' }, { status: 400 })
|
|
27
47
|
}
|
|
28
48
|
|
|
49
|
+
const loginTenantCookie = readCookie(req, ONBOARDING_LOGIN_TENANT_COOKIE)
|
|
50
|
+
if (!loginTenantCookie || loginTenantCookie !== parsed.data.tenantId) {
|
|
51
|
+
return NextResponse.json({ ok: false, error: 'Not authorized for this tenant.' }, { status: 403 })
|
|
52
|
+
}
|
|
53
|
+
|
|
29
54
|
let baseUrl: string
|
|
30
55
|
try {
|
|
31
56
|
baseUrl = getSecurityEmailBaseUrl(req)
|
|
@@ -101,6 +126,7 @@ const onboardingStatusDoc: OpenApiMethodDoc = {
|
|
|
101
126
|
],
|
|
102
127
|
errors: [
|
|
103
128
|
{ status: 400, description: 'Invalid tenant id or request origin.', schema: onboardingStatusErrorSchema },
|
|
129
|
+
{ status: 403, description: 'Caller is not authorized for this tenant.', schema: onboardingStatusErrorSchema },
|
|
104
130
|
{ status: 404, description: 'Onboarding request not found.', schema: onboardingStatusErrorSchema },
|
|
105
131
|
{ status: 500, description: 'Onboarding status is not configured.', schema: onboardingStatusErrorSchema },
|
|
106
132
|
],
|