@nebutra/tenant 0.1.0 → 0.1.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.
@@ -0,0 +1,87 @@
1
+ import type { TenantContext } from "./types";
2
+ /**
3
+ * Execute a function with a tenant context set.
4
+ *
5
+ * All async operations within `fn` will have access to the same tenant context
6
+ * via `getCurrentTenant()` without passing it as a parameter.
7
+ *
8
+ * @param context The tenant context to set
9
+ * @param fn The function to execute
10
+ * @returns The return value of `fn`
11
+ *
12
+ * @example
13
+ * ```ts
14
+ * const result = await runWithTenant({ id: "org_123" }, async () => {
15
+ * const tenant = getCurrentTenant();
16
+ * await doSomething(tenant.id);
17
+ * });
18
+ * ```
19
+ */
20
+ export declare function runWithTenant<T>(context: TenantContext, fn: () => Promise<T> | T): Promise<T>;
21
+ /**
22
+ * Get the current tenant context from AsyncLocalStorage.
23
+ *
24
+ * Throws `TenantRequiredError` if no tenant context is set.
25
+ *
26
+ * @throws TenantRequiredError if tenant context is not available
27
+ * @returns The current tenant context
28
+ *
29
+ * @example
30
+ * ```ts
31
+ * const tenant = getCurrentTenant();
32
+ * console.log(tenant.id); // "org_123"
33
+ * ```
34
+ */
35
+ export declare function getCurrentTenant(): TenantContext;
36
+ /**
37
+ * Get the current tenant context, or null if not set.
38
+ *
39
+ * Useful for optional tenant contexts (public routes, webhooks, etc).
40
+ *
41
+ * @returns The current tenant context, or null if not set
42
+ *
43
+ * @example
44
+ * ```ts
45
+ * const tenant = getTenantOrNull();
46
+ * if (tenant) {
47
+ * console.log(`Processing for tenant: ${tenant.id}`);
48
+ * } else {
49
+ * console.log("Public route, no tenant");
50
+ * }
51
+ * ```
52
+ */
53
+ export declare function getTenantOrNull(): TenantContext | null;
54
+ /**
55
+ * Assertion helper that ensures a tenant context is present.
56
+ *
57
+ * Throws a structured error if tenant is missing. Useful as a safety
58
+ * check at the start of tenant-specific handlers.
59
+ *
60
+ * @param tenant The tenant context (or null)
61
+ * @param context Optional context message for the error
62
+ * @throws TenantRequiredError if tenant is null or undefined
63
+ *
64
+ * @example
65
+ * ```ts
66
+ * const tenant = getTenantOrNull();
67
+ * requireTenant(tenant, "DELETE /api/documents/:id");
68
+ * // Safe to use tenant.id from here on
69
+ * ```
70
+ */
71
+ export declare function requireTenant(tenant: TenantContext | null | undefined, context?: string): asserts tenant is TenantContext;
72
+ /**
73
+ * Get the current tenant ID as a shorthand.
74
+ *
75
+ * Throws if tenant context is not set.
76
+ *
77
+ * @returns The current tenant's ID
78
+ * @throws TenantRequiredError if tenant context is not available
79
+ */
80
+ export declare function getCurrentTenantId(): string;
81
+ /**
82
+ * Get the current tenant ID, or null if not set.
83
+ *
84
+ * @returns The current tenant's ID, or null
85
+ */
86
+ export declare function getTenantIdOrNull(): string | null;
87
+ //# sourceMappingURL=context.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"context.d.ts","sourceRoot":"","sources":["../src/context.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AAW7C;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,aAAa,CAAC,CAAC,EAAE,OAAO,EAAE,aAAa,EAAE,EAAE,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,GAAG,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAS7F;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,gBAAgB,IAAI,aAAa,CAWhD;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,eAAe,IAAI,aAAa,GAAG,IAAI,CAEtD;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,aAAa,CAC3B,MAAM,EAAE,aAAa,GAAG,IAAI,GAAG,SAAS,EACxC,OAAO,CAAC,EAAE,MAAM,GACf,OAAO,CAAC,MAAM,IAAI,aAAa,CASjC;AAED;;;;;;;GAOG;AACH,wBAAgB,kBAAkB,IAAI,MAAM,CAE3C;AAED;;;;GAIG;AACH,wBAAgB,iBAAiB,IAAI,MAAM,GAAG,IAAI,CAEjD"}
@@ -0,0 +1,125 @@
1
+ import { AsyncLocalStorage } from "node:async_hooks";
2
+ import { logger } from "@nebutra/logger";
3
+ import { TenantRequiredError } from "./types";
4
+ /**
5
+ * AsyncLocalStorage-based tenant context — request-scoped, zero-copy across async boundaries.
6
+ *
7
+ * Each incoming request gets its own isolated context, even across await boundaries.
8
+ * No risk of tenant data leaking between requests or threads.
9
+ */
10
+ const tenantStorage = new AsyncLocalStorage();
11
+ /**
12
+ * Execute a function with a tenant context set.
13
+ *
14
+ * All async operations within `fn` will have access to the same tenant context
15
+ * via `getCurrentTenant()` without passing it as a parameter.
16
+ *
17
+ * @param context The tenant context to set
18
+ * @param fn The function to execute
19
+ * @returns The return value of `fn`
20
+ *
21
+ * @example
22
+ * ```ts
23
+ * const result = await runWithTenant({ id: "org_123" }, async () => {
24
+ * const tenant = getCurrentTenant();
25
+ * await doSomething(tenant.id);
26
+ * });
27
+ * ```
28
+ */
29
+ export function runWithTenant(context, fn) {
30
+ return tenantStorage.run(context, () => {
31
+ // Support both sync and async functions
32
+ const result = fn();
33
+ if (result instanceof Promise) {
34
+ return result;
35
+ }
36
+ return Promise.resolve(result);
37
+ });
38
+ }
39
+ /**
40
+ * Get the current tenant context from AsyncLocalStorage.
41
+ *
42
+ * Throws `TenantRequiredError` if no tenant context is set.
43
+ *
44
+ * @throws TenantRequiredError if tenant context is not available
45
+ * @returns The current tenant context
46
+ *
47
+ * @example
48
+ * ```ts
49
+ * const tenant = getCurrentTenant();
50
+ * console.log(tenant.id); // "org_123"
51
+ * ```
52
+ */
53
+ export function getCurrentTenant() {
54
+ const context = tenantStorage.getStore();
55
+ if (!context) {
56
+ logger.warn("getCurrentTenant() called without active tenant context");
57
+ throw new TenantRequiredError("No tenant context found. Ensure middleware called runWithTenant().");
58
+ }
59
+ return context;
60
+ }
61
+ /**
62
+ * Get the current tenant context, or null if not set.
63
+ *
64
+ * Useful for optional tenant contexts (public routes, webhooks, etc).
65
+ *
66
+ * @returns The current tenant context, or null if not set
67
+ *
68
+ * @example
69
+ * ```ts
70
+ * const tenant = getTenantOrNull();
71
+ * if (tenant) {
72
+ * console.log(`Processing for tenant: ${tenant.id}`);
73
+ * } else {
74
+ * console.log("Public route, no tenant");
75
+ * }
76
+ * ```
77
+ */
78
+ export function getTenantOrNull() {
79
+ return tenantStorage.getStore() ?? null;
80
+ }
81
+ /**
82
+ * Assertion helper that ensures a tenant context is present.
83
+ *
84
+ * Throws a structured error if tenant is missing. Useful as a safety
85
+ * check at the start of tenant-specific handlers.
86
+ *
87
+ * @param tenant The tenant context (or null)
88
+ * @param context Optional context message for the error
89
+ * @throws TenantRequiredError if tenant is null or undefined
90
+ *
91
+ * @example
92
+ * ```ts
93
+ * const tenant = getTenantOrNull();
94
+ * requireTenant(tenant, "DELETE /api/documents/:id");
95
+ * // Safe to use tenant.id from here on
96
+ * ```
97
+ */
98
+ export function requireTenant(tenant, context) {
99
+ if (!tenant) {
100
+ const message = context
101
+ ? `Tenant context required for: ${context}`
102
+ : "Tenant context is required";
103
+ logger.warn(message);
104
+ throw new TenantRequiredError(message);
105
+ }
106
+ }
107
+ /**
108
+ * Get the current tenant ID as a shorthand.
109
+ *
110
+ * Throws if tenant context is not set.
111
+ *
112
+ * @returns The current tenant's ID
113
+ * @throws TenantRequiredError if tenant context is not available
114
+ */
115
+ export function getCurrentTenantId() {
116
+ return getCurrentTenant().id;
117
+ }
118
+ /**
119
+ * Get the current tenant ID, or null if not set.
120
+ *
121
+ * @returns The current tenant's ID, or null
122
+ */
123
+ export function getTenantIdOrNull() {
124
+ return getTenantOrNull()?.id ?? null;
125
+ }
@@ -0,0 +1,9 @@
1
+ export { getCurrentTenant, getCurrentTenantId, getTenantIdOrNull, getTenantOrNull, requireTenant, runWithTenant, } from "./context";
2
+ export { compose, fromApiKey, fromHeader, fromJwtClaim, fromPath, fromSubdomain, } from "./resolvers";
3
+ export type { AuthSessionLike, SessionGetter } from "./resolvers/from-auth-session";
4
+ export { fromAuthSession } from "./resolvers/from-auth-session";
5
+ export type { IsolationStrategy, PlanTier, TenantConfig, TenantContext, TenantInfo, TenantResolver, } from "./types";
6
+ export { TenantConfigSchema, TenantContextSchema, TenantInfoSchema, TenantIsolationError, TenantRequiredError, } from "./types";
7
+ export type { RlsPolicyCommand, RlsPolicySqlOptions } from "./isolation";
8
+ export { createTenantPrismaProxy, generateRlsPolicySql, getTenantDatabaseUrl, getTenantSchema, TenantAwarePrismaClient, withRls, } from "./isolation";
9
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAKA,OAAO,EACL,gBAAgB,EAChB,kBAAkB,EAClB,iBAAiB,EACjB,eAAe,EACf,aAAa,EACb,aAAa,GACd,MAAM,WAAW,CAAC;AAEnB,OAAO,EACL,OAAO,EACP,UAAU,EACV,UAAU,EACV,YAAY,EACZ,QAAQ,EACR,aAAa,GACd,MAAM,aAAa,CAAC;AACrB,YAAY,EAAE,eAAe,EAAE,aAAa,EAAE,MAAM,+BAA+B,CAAC;AACpF,OAAO,EAAE,eAAe,EAAE,MAAM,+BAA+B,CAAC;AAEhE,YAAY,EACV,iBAAiB,EACjB,QAAQ,EACR,YAAY,EACZ,aAAa,EACb,UAAU,EACV,cAAc,GACf,MAAM,SAAS,CAAC;AACjB,OAAO,EACL,kBAAkB,EAClB,mBAAmB,EACnB,gBAAgB,EAChB,oBAAoB,EACpB,mBAAmB,GACpB,MAAM,SAAS,CAAC;AAKjB,YAAY,EAAE,gBAAgB,EAAE,mBAAmB,EAAE,MAAM,aAAa,CAAC;AAEzE,OAAO,EACL,uBAAuB,EACvB,oBAAoB,EACpB,oBAAoB,EACpB,eAAe,EACf,uBAAuB,EACvB,OAAO,GACR,MAAM,aAAa,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,13 @@
1
+ // =============================================================================
2
+ // @nebutra/tenant — Multi-tenancy context and isolation
3
+ // =============================================================================
4
+ // Re-export context functions
5
+ export { getCurrentTenant, getCurrentTenantId, getTenantIdOrNull, getTenantOrNull, requireTenant, runWithTenant, } from "./context";
6
+ // Re-export resolvers
7
+ export { compose, fromApiKey, fromHeader, fromJwtClaim, fromPath, fromSubdomain, } from "./resolvers";
8
+ export { fromAuthSession } from "./resolvers/from-auth-session";
9
+ export { TenantConfigSchema, TenantContextSchema, TenantInfoSchema, TenantIsolationError, TenantRequiredError, } from "./types";
10
+ // Re-export isolation helpers
11
+ export { createTenantPrismaProxy, generateRlsPolicySql, getTenantDatabaseUrl, getTenantSchema, TenantAwarePrismaClient, withRls, } from "./isolation";
12
+ // Re-export React hooks (as subpath export ./react)
13
+ // These are exported via package.json "exports" for tree-shaking
@@ -0,0 +1,157 @@
1
+ import type { IsolationStrategy } from "./types";
2
+ /** Minimal Prisma-like client interface for RLS extension support. */
3
+ interface PrismaLikeClient {
4
+ $extends?: (extension: unknown) => unknown;
5
+ $executeRaw?: (query: TemplateStringsArray, ...values: unknown[]) => Promise<number>;
6
+ $queryRaw?: (query: TemplateStringsArray, ...values: unknown[]) => Promise<unknown[]>;
7
+ }
8
+ export type RlsPolicyCommand = "ALL" | "SELECT" | "INSERT" | "UPDATE" | "DELETE";
9
+ export interface RlsPolicySqlOptions {
10
+ /** Tables that contain the tenant discriminator column. Sorted for deterministic output. */
11
+ tables: string[];
12
+ /** Optional schema qualifier. Defaults to unqualified table names. */
13
+ schema?: string;
14
+ /** Tenant discriminator column name. */
15
+ tenantColumn?: string;
16
+ /** PostgreSQL policy name prefix. Policy names are generated as `${prefix}_${table}`. */
17
+ policyPrefix?: string;
18
+ /** Policy command. Defaults to ALL for read/write tenant isolation. */
19
+ command?: RlsPolicyCommand;
20
+ /** Tenant expression used by generated policies. */
21
+ tenantExpression?: string;
22
+ /** Emit FORCE ROW LEVEL SECURITY after enabling RLS. Defaults to true. */
23
+ forceRls?: boolean;
24
+ }
25
+ /**
26
+ * Generate deterministic PostgreSQL Row-Level Security DDL for shared-schema tenancy.
27
+ *
28
+ * The generated SQL is intentionally migration-tool friendly: stable table sorting,
29
+ * explicit policy replacement, and no database connection side effects.
30
+ */
31
+ export declare function generateRlsPolicySql(options: RlsPolicySqlOptions): string;
32
+ /**
33
+ * Apply Prisma client extension that sets RLS (Row-Level Security) context.
34
+ *
35
+ * Works with PostgreSQL RLS policies that check `app.current_tenant_id`.
36
+ * This is the standard pattern for shared-schema multi-tenancy.
37
+ *
38
+ * The Prisma client middleware intercepts all queries and sets the
39
+ * application-level variable before executing.
40
+ *
41
+ * @param prisma The Prisma client to extend
42
+ * @param tenantId The tenant ID to set in RLS context
43
+ * @returns The extended Prisma client
44
+ *
45
+ * @example
46
+ * ```ts
47
+ * import { PrismaClient } from "@prisma/client";
48
+ * import { withRls } from "@nebutra/tenant/isolation";
49
+ * import { getCurrentTenant } from "@nebutra/tenant";
50
+ *
51
+ * const prisma = new PrismaClient();
52
+ *
53
+ * // In a request handler:
54
+ * const tenant = getCurrentTenant();
55
+ * const client = withRls(prisma, tenant.id);
56
+ *
57
+ * // All queries now include RLS enforcement:
58
+ * const users = await client.user.findMany();
59
+ * // SQL: SELECT * FROM users WHERE current_setting('app.current_tenant_id') = user.tenant_id
60
+ * ```
61
+ */
62
+ export declare function withRls<P extends PrismaLikeClient>(prisma: P, tenantId: string): P;
63
+ /**
64
+ * Get the PostgreSQL schema name for schema-per-tenant strategy.
65
+ *
66
+ * Converts a tenant ID to a safe PostgreSQL schema name.
67
+ * Schema names must start with a letter and contain only alphanumerics and underscores.
68
+ *
69
+ * @param tenantId The tenant ID
70
+ * @returns The schema name (e.g., "org_acme_corp_public")
71
+ * @throws TenantIsolationError if tenant ID is invalid
72
+ *
73
+ * @example
74
+ * ```ts
75
+ * const schemaName = getTenantSchema("acme-corp");
76
+ * // Returns: "org_acme_corp_public"
77
+ * ```
78
+ */
79
+ export declare function getTenantSchema(tenantId: string): string;
80
+ /**
81
+ * Get the PostgreSQL connection string for database-per-tenant strategy.
82
+ *
83
+ * Constructs a connection URL with the tenant-specific database name.
84
+ * Assumes the base connection URL is available and tenant databases follow
85
+ * a naming pattern (e.g., `nebutra_acme_corp`).
86
+ *
87
+ * @param tenantId The tenant ID
88
+ * @param baseUrl Optional base connection URL (defaults to process.env.DATABASE_URL)
89
+ * @returns The tenant-specific database URL
90
+ * @throws TenantIsolationError if base URL is invalid
91
+ *
92
+ * @example
93
+ * ```ts
94
+ * const dbUrl = getTenantDatabaseUrl("acme-corp");
95
+ * // Returns: "postgresql://user:pass@localhost/nebutra_acme_corp"
96
+ * ```
97
+ */
98
+ export declare function getTenantDatabaseUrl(tenantId: string, baseUrl?: string): string;
99
+ /**
100
+ * Wrapper for a Prisma client that automatically applies tenant filtering.
101
+ *
102
+ * This is useful for schema-per-tenant or database-per-tenant strategies
103
+ * where you want to ensure tenant isolation at the client level.
104
+ *
105
+ * @example
106
+ * ```ts
107
+ * import { TenantAwarePrismaClient } from "@nebutra/tenant/isolation";
108
+ * import { getCurrentTenant } from "@nebutra/tenant";
109
+ *
110
+ * export function getTenantPrisma() {
111
+ * const tenant = getCurrentTenant();
112
+ * return new TenantAwarePrismaClient(prisma, tenant.id);
113
+ * }
114
+ * ```
115
+ */
116
+ export declare class TenantAwarePrismaClient {
117
+ private prisma;
118
+ private tenantId;
119
+ constructor(prisma: PrismaLikeClient, tenantId: string);
120
+ /**
121
+ * Get the underlying Prisma client with RLS context applied.
122
+ */
123
+ get client(): PrismaLikeClient;
124
+ /**
125
+ * Get the schema name for this tenant (schema-per-tenant strategy).
126
+ */
127
+ getSchema(): string;
128
+ /**
129
+ * Get the database URL for this tenant (database-per-tenant strategy).
130
+ */
131
+ getDatabaseUrl(baseUrl?: string): string;
132
+ /**
133
+ * Execute a raw SQL query with tenant context.
134
+ */
135
+ executeRaw(query: string): Promise<number>;
136
+ /**
137
+ * Execute a raw query and return results.
138
+ */
139
+ queryRaw(query: string): Promise<unknown[]>;
140
+ }
141
+ /**
142
+ * Create a tenant-aware Prisma proxy that applies isolation based on strategy.
143
+ *
144
+ * @param prisma The base Prisma client
145
+ * @param tenantId The tenant ID
146
+ * @param strategy The isolation strategy (default: "shared_schema")
147
+ * @returns A proxy or wrapper for the Prisma client
148
+ *
149
+ * @example
150
+ * ```ts
151
+ * const prisma = createTenantPrismaProxy(client, "acme-corp", "shared_schema");
152
+ * const users = await prisma.user.findMany(); // Filtered by RLS
153
+ * ```
154
+ */
155
+ export declare function createTenantPrismaProxy(prisma: PrismaLikeClient, tenantId: string, strategy?: IsolationStrategy): PrismaLikeClient;
156
+ export {};
157
+ //# sourceMappingURL=isolation.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"isolation.d.ts","sourceRoot":"","sources":["../src/isolation.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,SAAS,CAAC;AAOjD,sEAAsE;AACtE,UAAU,gBAAgB;IACxB,QAAQ,CAAC,EAAE,CAAC,SAAS,EAAE,OAAO,KAAK,OAAO,CAAC;IAC3C,WAAW,CAAC,EAAE,CAAC,KAAK,EAAE,oBAAoB,EAAE,GAAG,MAAM,EAAE,OAAO,EAAE,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;IACrF,SAAS,CAAC,EAAE,CAAC,KAAK,EAAE,oBAAoB,EAAE,GAAG,MAAM,EAAE,OAAO,EAAE,KAAK,OAAO,CAAC,OAAO,EAAE,CAAC,CAAC;CACvF;AAED,MAAM,MAAM,gBAAgB,GAAG,KAAK,GAAG,QAAQ,GAAG,QAAQ,GAAG,QAAQ,GAAG,QAAQ,CAAC;AAEjF,MAAM,WAAW,mBAAmB;IAClC,4FAA4F;IAC5F,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,sEAAsE;IACtE,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,wCAAwC;IACxC,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,yFAAyF;IACzF,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,uEAAuE;IACvE,OAAO,CAAC,EAAE,gBAAgB,CAAC;IAC3B,oDAAoD;IACpD,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,0EAA0E;IAC1E,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAyCD;;;;;GAKG;AACH,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,mBAAmB,GAAG,MAAM,CA2DzE;AAMD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,wBAAgB,OAAO,CAAC,CAAC,SAAS,gBAAgB,EAAE,MAAM,EAAE,CAAC,EAAE,QAAQ,EAAE,MAAM,GAAG,CAAC,CAiClF;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,eAAe,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAmBxD;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,oBAAoB,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,GAAG,MAAM,CA6B/E;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,qBAAa,uBAAuB;IAEhC,OAAO,CAAC,MAAM;IACd,OAAO,CAAC,QAAQ;gBADR,MAAM,EAAE,gBAAgB,EACxB,QAAQ,EAAE,MAAM;IAK1B;;OAEG;IACH,IAAI,MAAM,qBAET;IAED;;OAEG;IACH,SAAS,IAAI,MAAM;IAInB;;OAEG;IACH,cAAc,CAAC,OAAO,CAAC,EAAE,MAAM,GAAG,MAAM;IAIxC;;OAEG;IACG,UAAU,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAwBhD;;OAEG;IACG,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,EAAE,CAAC;CAoBlD;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,uBAAuB,CACrC,MAAM,EAAE,gBAAgB,EACxB,QAAQ,EAAE,MAAM,EAChB,QAAQ,GAAE,iBAAmC,GAC5C,gBAAgB,CAmBlB"}