@seedgrid/fe-subdomain-tenancy 2026.7.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.
Files changed (40) hide show
  1. package/README.md +75 -0
  2. package/dist/client/index.d.ts +2 -0
  3. package/dist/client/index.d.ts.map +1 -0
  4. package/dist/client/index.js +3 -0
  5. package/dist/client/tenant-provider.d.ts +11 -0
  6. package/dist/client/tenant-provider.d.ts.map +1 -0
  7. package/dist/client/tenant-provider.js +19 -0
  8. package/dist/index.d.ts +4 -0
  9. package/dist/index.d.ts.map +1 -0
  10. package/dist/index.js +5 -0
  11. package/dist/module.d.ts +3 -0
  12. package/dist/module.d.ts.map +1 -0
  13. package/dist/module.js +16 -0
  14. package/dist/module.json +14 -0
  15. package/dist/paths.d.ts +9 -0
  16. package/dist/paths.d.ts.map +1 -0
  17. package/dist/paths.js +10 -0
  18. package/dist/server/create-subdomain-tenancy-server.d.ts +12 -0
  19. package/dist/server/create-subdomain-tenancy-server.d.ts.map +1 -0
  20. package/dist/server/create-subdomain-tenancy-server.js +18 -0
  21. package/dist/server/index.d.ts +2 -0
  22. package/dist/server/index.d.ts.map +1 -0
  23. package/dist/server/index.js +2 -0
  24. package/dist/subdomain-rules.d.ts +13 -0
  25. package/dist/subdomain-rules.d.ts.map +1 -0
  26. package/dist/subdomain-rules.js +82 -0
  27. package/dist/tenant-signup.d.ts +59 -0
  28. package/dist/tenant-signup.d.ts.map +1 -0
  29. package/dist/tenant-signup.js +5 -0
  30. package/package.json +55 -0
  31. package/scaffold/README.md +70 -0
  32. package/scaffold/src/app/(public)/signup/page.tsx +1165 -0
  33. package/scaffold/src/app/api/locations/cities/route.ts +125 -0
  34. package/scaffold/src/app/api/locations/states/route.ts +120 -0
  35. package/scaffold/src/modules/subdomain-tenancy/index.ts +20 -0
  36. package/scaffold/src/modules/subdomain-tenancy/server/create-public-tenant-action.ts +113 -0
  37. package/scaffold/src/modules/subdomain-tenancy/server/public-api-base-url.ts +28 -0
  38. package/scaffold/src/modules/subdomain-tenancy/server/resend-public-tenant-activation-email-action.ts +118 -0
  39. package/scaffold/src/modules/subdomain-tenancy/server/validate-public-tenant-email-action.ts +138 -0
  40. package/scaffold/src/modules/subdomain-tenancy/signup/types.ts +129 -0
@@ -0,0 +1,125 @@
1
+ import { type NextRequest, NextResponse } from "next/server";
2
+
3
+ const CITIES_API_BASE_URL = "https://cities.seedgrid.com.br";
4
+
5
+ type CityOption = {
6
+ id: string;
7
+ label: string;
8
+ value: string;
9
+ data: {
10
+ cityId: string;
11
+ stateId: string;
12
+ };
13
+ };
14
+
15
+ export async function GET(request: NextRequest) {
16
+ const query = request.nextUrl.searchParams.get("query")?.trim().toLowerCase() ?? "";
17
+ const stateId = request.nextUrl.searchParams.get("stateId")?.trim() ?? "";
18
+ const apiUsername = process.env.SEEDGRID_CITIES_API_USERNAME?.trim();
19
+ const apiKey = process.env.SEEDGRID_CITIES_API_KEY?.trim();
20
+ const authorizationHeader = apiUsername && apiKey
21
+ ? `Basic ${Buffer.from(`${apiUsername}:${apiKey}`).toString("base64")}`
22
+ : "";
23
+
24
+ if (!authorizationHeader || !stateId) {
25
+ return NextResponse.json([], { status: 200 });
26
+ }
27
+
28
+ try {
29
+ const upstream = await fetch(
30
+ `${CITIES_API_BASE_URL}/cities?stateId=${encodeURIComponent(stateId)}`,
31
+ {
32
+ method: "GET",
33
+ headers: {
34
+ accept: "application/json",
35
+ Authorization: authorizationHeader,
36
+ },
37
+ cache: "no-store",
38
+ }
39
+ );
40
+
41
+ if (!upstream.ok) {
42
+ return NextResponse.json([], { status: upstream.status });
43
+ }
44
+
45
+ const payload = await upstream.json();
46
+ const options = normalizeCityOptions(payload).filter((item) => {
47
+ if (!query) {
48
+ return true;
49
+ }
50
+
51
+ return item.label.toLowerCase().includes(query);
52
+ });
53
+
54
+ return NextResponse.json(options, { status: 200 });
55
+ } catch {
56
+ return NextResponse.json([], { status: 200 });
57
+ }
58
+ }
59
+
60
+ function normalizeCityOptions(payload: unknown): CityOption[] {
61
+ const records = Array.isArray(payload)
62
+ ? payload
63
+ : Array.isArray(readProperty(payload, "data"))
64
+ ? (readProperty(payload, "data") as unknown[])
65
+ : [];
66
+
67
+ return records
68
+ .map((record) => {
69
+ const cityId = readNumber(record, "id");
70
+ const stateId =
71
+ readNumber(record, "stateId") ??
72
+ readNumber(record, "estadoId") ??
73
+ readNumber(record, "state_id");
74
+ const cityName =
75
+ readString(record, "name") ??
76
+ readString(record, "nome") ??
77
+ readString(record, "label") ??
78
+ readString(record, "descricao");
79
+
80
+ if (!cityId || !stateId || !cityName) {
81
+ return null;
82
+ }
83
+
84
+ return {
85
+ id: cityId,
86
+ label: cityName,
87
+ value: cityName,
88
+ data: {
89
+ cityId,
90
+ stateId,
91
+ },
92
+ } satisfies CityOption;
93
+ })
94
+ .filter((item): item is CityOption => item !== null);
95
+ }
96
+
97
+ function readProperty(value: unknown, key: string) {
98
+ if (!value || typeof value !== "object") {
99
+ return null;
100
+ }
101
+
102
+ return Reflect.get(value, key);
103
+ }
104
+
105
+ function readString(value: unknown, key: string) {
106
+ const candidate = readProperty(value, key);
107
+
108
+ return typeof candidate === "string" && candidate.trim()
109
+ ? candidate.trim()
110
+ : null;
111
+ }
112
+
113
+ function readNumber(value: unknown, key: string) {
114
+ const candidate = readProperty(value, key);
115
+
116
+ if (typeof candidate === "number" && Number.isFinite(candidate)) {
117
+ return String(candidate);
118
+ }
119
+
120
+ if (typeof candidate === "string" && candidate.trim()) {
121
+ return candidate.trim();
122
+ }
123
+
124
+ return null;
125
+ }
@@ -0,0 +1,120 @@
1
+ import { type NextRequest, NextResponse } from "next/server";
2
+
3
+ const CITIES_API_BASE_URL = "https://cities.seedgrid.com.br";
4
+
5
+ type StateOption = {
6
+ id: string;
7
+ label: string;
8
+ value: string;
9
+ data: {
10
+ stateId: string;
11
+ };
12
+ };
13
+
14
+ export async function GET(request: NextRequest) {
15
+ const query = request.nextUrl.searchParams.get("query")?.trim().toLowerCase() ?? "";
16
+ const apiUsername = process.env.SEEDGRID_CITIES_API_USERNAME?.trim();
17
+ const apiKey = process.env.SEEDGRID_CITIES_API_KEY?.trim();
18
+ const authorizationHeader = apiUsername && apiKey
19
+ ? `Basic ${Buffer.from(`${apiUsername}:${apiKey}`).toString("base64")}`
20
+ : "";
21
+
22
+ if (!authorizationHeader) {
23
+ return NextResponse.json([], { status: 200 });
24
+ }
25
+
26
+ try {
27
+ const upstream = await fetch(`${CITIES_API_BASE_URL}/states?countryId=1`, {
28
+ method: "GET",
29
+ headers: {
30
+ accept: "application/json",
31
+ Authorization: authorizationHeader,
32
+ },
33
+ cache: "no-store",
34
+ });
35
+
36
+ if (!upstream.ok) {
37
+ return NextResponse.json([], { status: upstream.status });
38
+ }
39
+
40
+ const payload = await upstream.json();
41
+ const options = normalizeStateOptions(payload).filter((item) => {
42
+ if (!query) {
43
+ return true;
44
+ }
45
+
46
+ return `${item.value} ${item.label}`.toLowerCase().includes(query);
47
+ });
48
+
49
+ return NextResponse.json(options, { status: 200 });
50
+ } catch {
51
+ return NextResponse.json([], { status: 200 });
52
+ }
53
+ }
54
+
55
+ function normalizeStateOptions(payload: unknown): StateOption[] {
56
+ const records = Array.isArray(payload)
57
+ ? payload
58
+ : Array.isArray(readProperty(payload, "data"))
59
+ ? (readProperty(payload, "data") as unknown[])
60
+ : [];
61
+
62
+ return records
63
+ .map((record) => {
64
+ const stateId = readNumber(record, "id");
65
+ const stateCode =
66
+ readString(record, "uf") ??
67
+ readString(record, "code") ??
68
+ readString(record, "abbreviation") ??
69
+ readString(record, "sigla");
70
+ const stateName =
71
+ readString(record, "name") ??
72
+ readString(record, "nome") ??
73
+ readString(record, "label") ??
74
+ readString(record, "descricao");
75
+
76
+ if (!stateId || !stateCode || !stateName) {
77
+ return null;
78
+ }
79
+
80
+ return {
81
+ id: stateId,
82
+ label: `${stateCode} - ${stateName}`,
83
+ value: stateCode,
84
+ data: {
85
+ stateId,
86
+ },
87
+ } satisfies StateOption;
88
+ })
89
+ .filter((item): item is StateOption => item !== null);
90
+ }
91
+
92
+ function readProperty(value: unknown, key: string) {
93
+ if (!value || typeof value !== "object") {
94
+ return null;
95
+ }
96
+
97
+ return Reflect.get(value, key);
98
+ }
99
+
100
+ function readString(value: unknown, key: string) {
101
+ const candidate = readProperty(value, key);
102
+
103
+ return typeof candidate === "string" && candidate.trim()
104
+ ? candidate.trim()
105
+ : null;
106
+ }
107
+
108
+ function readNumber(value: unknown, key: string) {
109
+ const candidate = readProperty(value, key);
110
+
111
+ if (typeof candidate === "number" && Number.isFinite(candidate)) {
112
+ return String(candidate);
113
+ }
114
+
115
+ if (typeof candidate === "string" && candidate.trim()) {
116
+ return candidate.trim();
117
+ }
118
+
119
+ return null;
120
+ }
@@ -0,0 +1,20 @@
1
+ // seedgrid:managed
2
+
3
+ export {
4
+ buildCreatePublicTenantPayload,
5
+ createEmptyPublicTenantSignupForm,
6
+ type CreatePublicTenantActionResult,
7
+ type CreatePublicTenantPayload,
8
+ type PublicTenantSignupFormValues,
9
+ } from "./signup/types";
10
+
11
+ // Promovidos pra lib versionada (@seedgrid/fe-subdomain-tenancy) — ver ADR
12
+ // 0004/0005 e o scaffold README do fe-security pro criterio lib vs scaffold
13
+ // usado nesta extracao (mesma regra: import `@/...` real = scaffold).
14
+ export {
15
+ RESERVED_SUBDOMAINS,
16
+ isReservedSubdomain,
17
+ normalizeSubdomain,
18
+ validateSubdomain,
19
+ } from "@seedgrid/fe-subdomain-tenancy";
20
+ export { TenantProvider, useTenantContext } from "@seedgrid/fe-subdomain-tenancy/client";
@@ -0,0 +1,113 @@
1
+ "use server";
2
+
3
+ // seedgrid:managed
4
+
5
+ import { getTranslator } from "@/i18n";
6
+ import { ApiClientError, createApiClient } from "@seedgrid/fe-core";
7
+ import { captureException, runGuarded } from "@/modules/core";
8
+
9
+ import {
10
+ type CreatePublicTenantActionResult,
11
+ type CreatePublicTenantPayload,
12
+ } from "../signup/types";
13
+ import { resolvePublicApiBaseUrl } from "./public-api-base-url";
14
+ import { normalizeSubdomain, validateSubdomain } from "@seedgrid/fe-subdomain-tenancy";
15
+
16
+ export async function createPublicTenantAction(
17
+ payload: CreatePublicTenantPayload
18
+ ): Promise<CreatePublicTenantActionResult> {
19
+ return runGuarded(async () => {
20
+ const t = getTranslator();
21
+ const normalizedSubdomain = normalizeSubdomain(payload.subdomain);
22
+ const subdomainValidation = validateSubdomain(normalizedSubdomain, t);
23
+
24
+ if (subdomainValidation) {
25
+ return {
26
+ ok: false,
27
+ message: subdomainValidation,
28
+ status: 400,
29
+ };
30
+ }
31
+
32
+ const client = createApiClient({
33
+ baseUrl: await resolvePublicApiBaseUrl(),
34
+ defaultCache: "no-store",
35
+ });
36
+
37
+ try {
38
+ await client.post("/public/tenants", {
39
+ body: {
40
+ ...payload,
41
+ subdomain: normalizedSubdomain,
42
+ },
43
+ requireAuth: false,
44
+ retry: {
45
+ attempts: 2,
46
+ baseDelayMs: 350,
47
+ retryOn: [429, 502, 503, 504],
48
+ },
49
+ cache: "no-store",
50
+ });
51
+
52
+ return {
53
+ ok: true,
54
+ tenantSubdomain: normalizedSubdomain,
55
+ rootEmail: payload.rootEmail,
56
+ message: t("multitenancy.signup.actions.create.success"),
57
+ };
58
+ } catch (error) {
59
+ if (error instanceof ApiClientError) {
60
+ return {
61
+ ok: false,
62
+ message: extractApiErrorMessage(error, t),
63
+ status: error.status,
64
+ };
65
+ }
66
+
67
+ await captureException(error, {
68
+ area: "server",
69
+ source: "multitenancy.createPublicTenantAction",
70
+ });
71
+
72
+ return {
73
+ ok: false,
74
+ message: t("multitenancy.signup.actions.create.unavailable"),
75
+ };
76
+ }
77
+ });
78
+ }
79
+
80
+ function extractApiErrorMessage(error: ApiClientError, t: ReturnType<typeof getTranslator>) {
81
+ const body = error.responseBody;
82
+
83
+ if (typeof body === "string" && body.trim()) {
84
+ return body;
85
+ }
86
+
87
+ if (body && typeof body === "object") {
88
+ const message =
89
+ pickObjectString(body, "userMessage") ??
90
+ pickObjectString(body, "message") ??
91
+ pickObjectString(body, "error") ??
92
+ pickObjectString(body, "detail");
93
+
94
+ if (message) {
95
+ return message;
96
+ }
97
+ }
98
+
99
+ if (error.status === 409) {
100
+ return t("multitenancy.signup.actions.create.conflict");
101
+ }
102
+
103
+ if (error.status === 400) {
104
+ return t("multitenancy.signup.actions.create.invalid_payload");
105
+ }
106
+
107
+ return t("multitenancy.signup.actions.create.generic");
108
+ }
109
+
110
+ function pickObjectString(value: object, key: string) {
111
+ const candidate = Reflect.get(value, key);
112
+ return typeof candidate === "string" && candidate.trim() ? candidate : null;
113
+ }
@@ -0,0 +1,28 @@
1
+ "use server";
2
+
3
+ // seedgrid:managed
4
+
5
+ import { headers } from "next/headers";
6
+
7
+ import { resolveApiBaseUrlForHostname } from "@/config/environment";
8
+
9
+ export async function resolvePublicApiBaseUrl() {
10
+ const requestHeaders = await headers();
11
+ const hostname = resolveRequestHostname(requestHeaders);
12
+
13
+ if (!hostname) {
14
+ throw new Error(
15
+ "Could not resolve the public API base URL. Access the app through a configured domain hostname and provide the corresponding host header."
16
+ );
17
+ }
18
+
19
+ return resolveApiBaseUrlForHostname(hostname);
20
+ }
21
+
22
+ function resolveRequestHostname(requestHeaders: Headers) {
23
+ const forwardedHost = requestHeaders.get("x-forwarded-host")?.trim();
24
+ const hostHeader = requestHeaders.get("host")?.trim();
25
+ const candidate = forwardedHost || hostHeader || "";
26
+
27
+ return candidate.replace(/:\d+$/, "");
28
+ }
@@ -0,0 +1,118 @@
1
+ "use server";
2
+
3
+ // seedgrid:managed
4
+
5
+ import { getTranslator } from "@/i18n";
6
+ import { ApiClientError, createApiClient } from "@seedgrid/fe-core";
7
+ import { captureException, runGuarded } from "@/modules/core";
8
+
9
+ import { normalizeSubdomain, validateSubdomain } from "@seedgrid/fe-subdomain-tenancy";
10
+ import { resolvePublicApiBaseUrl } from "./public-api-base-url";
11
+
12
+ type ResendPublicTenantActivationEmailActionResult =
13
+ | {
14
+ ok: true;
15
+ message: string;
16
+ }
17
+ | {
18
+ ok: false;
19
+ message: string;
20
+ status?: number;
21
+ };
22
+
23
+ export async function resendPublicTenantActivationEmailAction(
24
+ subdomain: string
25
+ ): Promise<ResendPublicTenantActivationEmailActionResult> {
26
+ return runGuarded(async () => {
27
+ const t = getTranslator();
28
+ const normalizedSubdomain = normalizeSubdomain(subdomain);
29
+ const subdomainValidation = validateSubdomain(normalizedSubdomain, t);
30
+
31
+ if (subdomainValidation) {
32
+ return {
33
+ ok: false,
34
+ message: subdomainValidation,
35
+ status: 400,
36
+ };
37
+ }
38
+
39
+ const client = createApiClient({
40
+ baseUrl: await resolvePublicApiBaseUrl(),
41
+ defaultCache: "no-store",
42
+ });
43
+
44
+ try {
45
+ const response = await client.post<{ message?: string }>(
46
+ "/public/tenants/resend-email",
47
+ {
48
+ query: {
49
+ subdomain: normalizedSubdomain,
50
+ },
51
+ requireAuth: false,
52
+ retry: {
53
+ attempts: 2,
54
+ baseDelayMs: 350,
55
+ retryOn: [429, 502, 503, 504],
56
+ },
57
+ cache: "no-store",
58
+ }
59
+ );
60
+
61
+ return {
62
+ ok: true,
63
+ message:
64
+ response.message?.trim() ||
65
+ t("multitenancy.signup.actions.resend.success"),
66
+ };
67
+ } catch (error) {
68
+ if (error instanceof ApiClientError) {
69
+ return {
70
+ ok: false,
71
+ message: extractApiErrorMessage(error, t),
72
+ status: error.status,
73
+ };
74
+ }
75
+
76
+ await captureException(error, {
77
+ area: "server",
78
+ source: "multitenancy.resendPublicTenantActivationEmailAction",
79
+ });
80
+
81
+ return {
82
+ ok: false,
83
+ message: t("multitenancy.signup.actions.resend.unavailable"),
84
+ };
85
+ }
86
+ });
87
+ }
88
+
89
+ function extractApiErrorMessage(error: ApiClientError, t: ReturnType<typeof getTranslator>) {
90
+ const body = error.responseBody;
91
+
92
+ if (typeof body === "string" && body.trim()) {
93
+ return body;
94
+ }
95
+
96
+ if (body && typeof body === "object") {
97
+ const message =
98
+ pickObjectString(body, "userMessage") ??
99
+ pickObjectString(body, "message") ??
100
+ pickObjectString(body, "error") ??
101
+ pickObjectString(body, "detail");
102
+
103
+ if (message) {
104
+ return message;
105
+ }
106
+ }
107
+
108
+ if (error.status === 404) {
109
+ return t("multitenancy.signup.actions.resend.not_found");
110
+ }
111
+
112
+ return t("multitenancy.signup.actions.resend.generic");
113
+ }
114
+
115
+ function pickObjectString(value: object, key: string) {
116
+ const candidate = Reflect.get(value, key);
117
+ return typeof candidate === "string" && candidate.trim() ? candidate : null;
118
+ }
@@ -0,0 +1,138 @@
1
+ "use server";
2
+
3
+ // seedgrid:managed
4
+
5
+ import { getTranslator } from "@/i18n";
6
+ import { ApiClientError, createApiClient } from "@seedgrid/fe-core";
7
+ import { captureException, runGuarded } from "@/modules/core";
8
+
9
+ import { normalizeSubdomain, validateSubdomain } from "@seedgrid/fe-subdomain-tenancy";
10
+ import { resolvePublicApiBaseUrl } from "./public-api-base-url";
11
+
12
+ type ValidatePublicTenantEmailActionPayload = {
13
+ subdomain: string;
14
+ code: string;
15
+ };
16
+
17
+ type ValidatePublicTenantEmailActionResult =
18
+ | {
19
+ ok: true;
20
+ message: string;
21
+ }
22
+ | {
23
+ ok: false;
24
+ message: string;
25
+ status?: number;
26
+ };
27
+
28
+ export async function validatePublicTenantEmailAction(
29
+ payload: ValidatePublicTenantEmailActionPayload
30
+ ): Promise<ValidatePublicTenantEmailActionResult> {
31
+ return runGuarded(async () => {
32
+ const t = getTranslator();
33
+ const normalizedSubdomain = normalizeSubdomain(payload.subdomain);
34
+ const subdomainValidation = validateSubdomain(normalizedSubdomain, t);
35
+
36
+ if (subdomainValidation) {
37
+ return {
38
+ ok: false,
39
+ message: subdomainValidation,
40
+ status: 400,
41
+ };
42
+ }
43
+
44
+ const normalizedCode = payload.code.replace(/\D/g, "").slice(0, 6);
45
+
46
+ if (normalizedCode.length !== 6) {
47
+ return {
48
+ ok: false,
49
+ message: t("multitenancy.signup.actions.validate.code_required"),
50
+ status: 400,
51
+ };
52
+ }
53
+
54
+ const client = createApiClient({
55
+ baseUrl: await resolvePublicApiBaseUrl(),
56
+ defaultCache: "no-store",
57
+ });
58
+
59
+ try {
60
+ const response = await client.post<{ message?: string }>(
61
+ "/public/tenants/validate-email",
62
+ {
63
+ body: {
64
+ subdomain: normalizedSubdomain,
65
+ code: normalizedCode,
66
+ },
67
+ requireAuth: false,
68
+ retry: {
69
+ attempts: 2,
70
+ baseDelayMs: 350,
71
+ retryOn: [429, 502, 503, 504],
72
+ },
73
+ cache: "no-store",
74
+ }
75
+ );
76
+
77
+ return {
78
+ ok: true,
79
+ message:
80
+ response.message?.trim() ||
81
+ t("multitenancy.signup.actions.validate.success"),
82
+ };
83
+ } catch (error) {
84
+ if (error instanceof ApiClientError) {
85
+ return {
86
+ ok: false,
87
+ message: extractApiErrorMessage(error, t),
88
+ status: error.status,
89
+ };
90
+ }
91
+
92
+ await captureException(error, {
93
+ area: "server",
94
+ source: "multitenancy.validatePublicTenantEmailAction",
95
+ });
96
+
97
+ return {
98
+ ok: false,
99
+ message: t("multitenancy.signup.actions.validate.unavailable"),
100
+ };
101
+ }
102
+ });
103
+ }
104
+
105
+ function extractApiErrorMessage(error: ApiClientError, t: ReturnType<typeof getTranslator>) {
106
+ const body = error.responseBody;
107
+
108
+ if (typeof body === "string" && body.trim()) {
109
+ return body;
110
+ }
111
+
112
+ if (body && typeof body === "object") {
113
+ const message =
114
+ pickObjectString(body, "userMessage") ??
115
+ pickObjectString(body, "message") ??
116
+ pickObjectString(body, "error") ??
117
+ pickObjectString(body, "detail");
118
+
119
+ if (message) {
120
+ return message;
121
+ }
122
+ }
123
+
124
+ if (error.status === 404) {
125
+ return t("multitenancy.signup.actions.validate.not_found");
126
+ }
127
+
128
+ if (error.status === 400) {
129
+ return t("multitenancy.signup.actions.validate.invalid_code");
130
+ }
131
+
132
+ return t("multitenancy.signup.actions.validate.generic");
133
+ }
134
+
135
+ function pickObjectString(value: object, key: string) {
136
+ const candidate = Reflect.get(value, key);
137
+ return typeof candidate === "string" && candidate.trim() ? candidate : null;
138
+ }