@happyvertical/smrt-tenancy 0.30.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 (70) hide show
  1. package/AGENTS.md +71 -0
  2. package/CLAUDE.md +1 -0
  3. package/LICENSE +7 -0
  4. package/README.md +122 -0
  5. package/dist/__smrt-register__.d.ts +2 -0
  6. package/dist/__smrt-register__.d.ts.map +1 -0
  7. package/dist/adapters/cli.d.ts +178 -0
  8. package/dist/adapters/cli.d.ts.map +1 -0
  9. package/dist/adapters/express.d.ts +115 -0
  10. package/dist/adapters/express.d.ts.map +1 -0
  11. package/dist/adapters/index.d.ts +22 -0
  12. package/dist/adapters/index.d.ts.map +1 -0
  13. package/dist/adapters/index.js +7 -0
  14. package/dist/adapters/index.js.map +1 -0
  15. package/dist/adapters/sveltekit.d.ts +123 -0
  16. package/dist/adapters/sveltekit.d.ts.map +1 -0
  17. package/dist/chunks/context-B5CKsmMi.js +190 -0
  18. package/dist/chunks/context-B5CKsmMi.js.map +1 -0
  19. package/dist/chunks/sveltekit-9eRH1RLw.js +153 -0
  20. package/dist/chunks/sveltekit-9eRH1RLw.js.map +1 -0
  21. package/dist/chunks/testing-C_tV23JW.js +487 -0
  22. package/dist/chunks/testing-C_tV23JW.js.map +1 -0
  23. package/dist/context.d.ts +435 -0
  24. package/dist/context.d.ts.map +1 -0
  25. package/dist/decorators.d.ts +126 -0
  26. package/dist/decorators.d.ts.map +1 -0
  27. package/dist/enabled-state.d.ts +25 -0
  28. package/dist/enabled-state.d.ts.map +1 -0
  29. package/dist/entry-point.d.ts +83 -0
  30. package/dist/entry-point.d.ts.map +1 -0
  31. package/dist/fields.d.ts +104 -0
  32. package/dist/fields.d.ts.map +1 -0
  33. package/dist/index.d.ts +9 -0
  34. package/dist/index.d.ts.map +1 -0
  35. package/dist/index.js +108 -0
  36. package/dist/index.js.map +1 -0
  37. package/dist/interceptor.d.ts +156 -0
  38. package/dist/interceptor.d.ts.map +1 -0
  39. package/dist/manifest.json +11 -0
  40. package/dist/playground.d.ts +2 -0
  41. package/dist/playground.d.ts.map +1 -0
  42. package/dist/playground.js +80 -0
  43. package/dist/playground.js.map +1 -0
  44. package/dist/registry.d.ts +145 -0
  45. package/dist/registry.d.ts.map +1 -0
  46. package/dist/smrt-knowledge.json +65 -0
  47. package/dist/svelte/components/TenantCard.svelte +272 -0
  48. package/dist/svelte/components/TenantCard.svelte.d.ts +18 -0
  49. package/dist/svelte/components/TenantCard.svelte.d.ts.map +1 -0
  50. package/dist/svelte/components/TenantSwitcher.svelte +68 -0
  51. package/dist/svelte/components/TenantSwitcher.svelte.d.ts +11 -0
  52. package/dist/svelte/components/TenantSwitcher.svelte.d.ts.map +1 -0
  53. package/dist/svelte/i18n.d.ts +5 -0
  54. package/dist/svelte/i18n.d.ts.map +1 -0
  55. package/dist/svelte/i18n.js +9 -0
  56. package/dist/svelte/index.d.ts +15 -0
  57. package/dist/svelte/index.d.ts.map +1 -0
  58. package/dist/svelte/index.js +19 -0
  59. package/dist/svelte/playground.d.ts +70 -0
  60. package/dist/svelte/playground.d.ts.map +1 -0
  61. package/dist/svelte/playground.js +75 -0
  62. package/dist/testing.d.ts +145 -0
  63. package/dist/testing.d.ts.map +1 -0
  64. package/dist/testing.js +11 -0
  65. package/dist/testing.js.map +1 -0
  66. package/dist/ui.d.ts +21 -0
  67. package/dist/ui.d.ts.map +1 -0
  68. package/dist/ui.js +33 -0
  69. package/dist/ui.js.map +1 -0
  70. package/package.json +99 -0
@@ -0,0 +1,104 @@
1
+ /**
2
+ * Tenancy Field Types and Utilities
3
+ *
4
+ * This module provides types and utility functions for tenant ID fields.
5
+ * The actual field decorator is in decorators.ts.
6
+ *
7
+ * @see https://github.com/happyvertical/smrt/issues/675
8
+ * @see https://github.com/happyvertical/smrt/issues/829
9
+ */
10
+ /**
11
+ * Options for the `@tenantId()` property decorator.
12
+ *
13
+ * Controls how the decorated field interacts with the tenancy interceptor.
14
+ * All options default to the strictest safe values: auto-filter on, required,
15
+ * auto-populate on, not nullable.
16
+ *
17
+ * @see tenantId
18
+ * @see TenantScopedOptions
19
+ */
20
+ export interface TenantIdFieldOptions {
21
+ /**
22
+ * Auto-filter queries by this field
23
+ * @default true
24
+ */
25
+ autoFilter?: boolean;
26
+ /**
27
+ * Require this field to have a value on save
28
+ * @default true
29
+ */
30
+ required?: boolean;
31
+ /**
32
+ * Auto-populate from context on create if not set
33
+ * @default true
34
+ */
35
+ autoPopulate?: boolean;
36
+ /**
37
+ * Allow null values (for global resources)
38
+ * @default false
39
+ */
40
+ nullable?: boolean;
41
+ }
42
+ export declare const TENANT_ID_SYMBOL: unique symbol;
43
+ /**
44
+ * Internal field descriptor stored in `ObjectRegistry` when `@tenantId()` is
45
+ * applied to a property.
46
+ *
47
+ * Consumers should use `isTenantIdField()` and `getTenantIdFieldOptions()`
48
+ * to inspect these descriptors rather than reading the raw properties directly.
49
+ *
50
+ * @see isTenantIdField
51
+ * @see getTenantIdFieldOptions
52
+ */
53
+ export interface TenantIdFieldDefinition {
54
+ /** Field type marker */
55
+ type: 'foreignKey';
56
+ /** Reference to Tenant class (placeholder - actual class resolved at runtime) */
57
+ reference: 'Tenant';
58
+ /** SQL type */
59
+ sqlType: 'UUID';
60
+ /** Field is required */
61
+ required: boolean;
62
+ /** Field allows null */
63
+ nullable: boolean;
64
+ /** Tenancy-specific options */
65
+ __tenancy: TenantIdFieldOptions & {
66
+ isTenantIdField: true;
67
+ };
68
+ }
69
+ /**
70
+ * Return `true` if the given field definition was produced by the `@tenantId()`
71
+ * decorator (i.e., it has an `__tenancy.isTenantIdField` marker).
72
+ *
73
+ * Used internally by the interceptor and code generators to locate the tenant
74
+ * ID field on a class without knowing its property name in advance.
75
+ *
76
+ * @param field - A raw field definition object, typically from `ObjectRegistry`.
77
+ * @returns `true` if `field` is a tenant ID field definition, `false` otherwise.
78
+ *
79
+ * @example
80
+ * ```typescript
81
+ * const fields = ObjectRegistry.getFields('Document');
82
+ * const tenantField = Object.entries(fields).find(([, def]) => isTenantIdField(def));
83
+ * ```
84
+ *
85
+ * @see getTenantIdFieldOptions
86
+ * @see TenantIdFieldDefinition
87
+ */
88
+ export declare function isTenantIdField(field: unknown): boolean;
89
+ /**
90
+ * Extract the `TenantIdFieldOptions` from a field definition.
91
+ *
92
+ * Returns the tenancy-specific options (autoFilter, required, autoPopulate,
93
+ * nullable) stored inside the field descriptor's `__tenancy` property.
94
+ * Returns `null` if the field was not produced by `@tenantId()`.
95
+ *
96
+ * @param field - A raw field definition object, typically from `ObjectRegistry`.
97
+ * @returns The `TenantIdFieldOptions` if the field is a tenant ID field,
98
+ * `null` otherwise.
99
+ *
100
+ * @see isTenantIdField
101
+ * @see TenantIdFieldOptions
102
+ */
103
+ export declare function getTenantIdFieldOptions(field: unknown): TenantIdFieldOptions | null;
104
+ //# sourceMappingURL=fields.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fields.d.ts","sourceRoot":"","sources":["../src/fields.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH;;;;;;;;;GASG;AACH,MAAM,WAAW,oBAAoB;IACnC;;;OAGG;IACH,UAAU,CAAC,EAAE,OAAO,CAAC;IAErB;;;OAGG;IACH,QAAQ,CAAC,EAAE,OAAO,CAAC;IAEnB;;;OAGG;IACH,YAAY,CAAC,EAAE,OAAO,CAAC;IAEvB;;;OAGG;IACH,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAGD,eAAO,MAAM,gBAAgB,eAAqB,CAAC;AAEnD;;;;;;;;;GASG;AACH,MAAM,WAAW,uBAAuB;IACtC,wBAAwB;IACxB,IAAI,EAAE,YAAY,CAAC;IACnB,iFAAiF;IACjF,SAAS,EAAE,QAAQ,CAAC;IACpB,eAAe;IACf,OAAO,EAAE,MAAM,CAAC;IAChB,wBAAwB;IACxB,QAAQ,EAAE,OAAO,CAAC;IAClB,wBAAwB;IACxB,QAAQ,EAAE,OAAO,CAAC;IAClB,+BAA+B;IAC/B,SAAS,EAAE,oBAAoB,GAAG;QAAE,eAAe,EAAE,IAAI,CAAA;KAAE,CAAC;CAC7D;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAOvD;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,uBAAuB,CACrC,KAAK,EAAE,OAAO,GACb,oBAAoB,GAAG,IAAI,CAM7B"}
@@ -0,0 +1,9 @@
1
+ export { type CliContextOptions, createCliContext, createExpressMiddleware, createSvelteKitHandle, type ExpressMiddlewareOptions, type SvelteKitHandleOptions, } from './adapters/index.js';
2
+ export { enterTenantContext, getCurrentTenant, getTenantId, hasTenantContext, isSuperAdminBypass, isSystemContext, type MinimalTenantContext, requireTenant, requireTenantId, TenantContext, type TenantContextData, TenantContextError, TenantIsolationError, withSuperAdminBypass, withSystemContext, withTenant, withTenantSync, } from './context.js';
3
+ export { TenantScoped, type TenantScopedOptions, tenantId, } from './decorators.js';
4
+ export { runTenantScopedEntryPoint, type TenantEntryPointOptions, } from './entry-point.js';
5
+ export { getTenantIdFieldOptions, isTenantIdField, type TenantIdFieldDefinition, type TenantIdFieldOptions, } from './fields.js';
6
+ export { createTenantInterceptor, disableTenancy, enableTenancy, isTenancyEnabled, type RawQueryPolicy, type TenantInterceptorOptions, } from './interceptor.js';
7
+ export { clearTenantScopedRegistry, getAllTenantScopedClasses, getTenantScopedConfig, isTenantScopedClass, registerTenantScopedClass, type TenantScopedConfig, unregisterTenantScopedClass, } from './registry.js';
8
+ export { assertTenantContextRequired, assertTenantIsolationViolation, createTestTenantContext, resetTenancy, type SetupTestTenancyOptions, setupTestTenancy, testTenantIsolation, } from './testing.js';
9
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AAKH,OAAO,wBAAwB,CAAC;AAKhC,OAAO,EACL,KAAK,iBAAiB,EAEtB,gBAAgB,EAEhB,uBAAuB,EAEvB,qBAAqB,EACrB,KAAK,wBAAwB,EAC7B,KAAK,sBAAsB,GAC5B,MAAM,qBAAqB,CAAC;AAI7B,OAAO,EACL,kBAAkB,EAElB,gBAAgB,EAChB,WAAW,EACX,gBAAgB,EAChB,kBAAkB,EAElB,eAAe,EACf,KAAK,oBAAoB,EACzB,aAAa,EACb,eAAe,EAEf,aAAa,EAEb,KAAK,iBAAiB,EAEtB,kBAAkB,EAClB,oBAAoB,EACpB,oBAAoB,EACpB,iBAAiB,EAEjB,UAAU,EACV,cAAc,GACf,MAAM,cAAc,CAAC;AAKtB,OAAO,EAEL,YAAY,EACZ,KAAK,mBAAmB,EAExB,QAAQ,GACT,MAAM,iBAAiB,CAAC;AAIzB,OAAO,EACL,yBAAyB,EACzB,KAAK,uBAAuB,GAC7B,MAAM,kBAAkB,CAAC;AAI1B,OAAO,EACL,uBAAuB,EACvB,eAAe,EACf,KAAK,uBAAuB,EAC5B,KAAK,oBAAoB,GAC1B,MAAM,aAAa,CAAC;AAIrB,OAAO,EAEL,uBAAuB,EACvB,cAAc,EAEd,aAAa,EACb,gBAAgB,EAChB,KAAK,cAAc,EAEnB,KAAK,wBAAwB,GAC9B,MAAM,kBAAkB,CAAC;AAI1B,OAAO,EACL,yBAAyB,EACzB,yBAAyB,EACzB,qBAAqB,EACrB,mBAAmB,EACnB,yBAAyB,EACzB,KAAK,kBAAkB,EACvB,2BAA2B,GAC5B,MAAM,eAAe,CAAC;AAKvB,OAAO,EACL,2BAA2B,EAC3B,8BAA8B,EAC9B,uBAAuB,EACvB,YAAY,EACZ,KAAK,uBAAuB,EAC5B,gBAAgB,EAChB,mBAAmB,GACpB,MAAM,cAAc,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,108 @@
1
+ import { ObjectRegistry, applyPendingDecoratorRegistrations, registerCompatibleFieldDecorator } from "@happyvertical/smrt-core";
2
+ import { c, a, b } from "./chunks/sveltekit-9eRH1RLw.js";
3
+ import { T, a as a2, b as b2, e, g, c as c2, h, i, d, r, f, w, j, k, l } from "./chunks/context-B5CKsmMi.js";
4
+ import { r as registerTenantScopedClass } from "./chunks/testing-C_tV23JW.js";
5
+ import { a as a3, b as b3, c as c3, d as d2, e as e2, f as f2, g as g2, h as h2, i as i2, j as j2, k as k2, l as l2, m, s, t, u } from "./chunks/testing-C_tV23JW.js";
6
+ ObjectRegistry.registerPackageManifest(
7
+ new URL("./manifest.json", import.meta.url)
8
+ );
9
+ function TenantScoped(options = {}) {
10
+ return (target, decoratorContext) => {
11
+ applyPendingDecoratorRegistrations(target, decoratorContext);
12
+ const className = target.name;
13
+ const config = {
14
+ mode: options.mode ?? "required",
15
+ field: options.field ?? "tenantId",
16
+ autoFilter: options.autoFilter ?? true,
17
+ autoPopulate: options.autoPopulate ?? true,
18
+ allowSuperAdminBypass: options.allowSuperAdminBypass ?? false
19
+ };
20
+ registerTenantScopedClass(className, config);
21
+ return target;
22
+ };
23
+ }
24
+ function tenantId(options = {}) {
25
+ const opts = {
26
+ autoFilter: true,
27
+ required: true,
28
+ autoPopulate: true,
29
+ nullable: false,
30
+ ...options
31
+ };
32
+ return ((targetOrValue, propertyKeyOrContext) => {
33
+ registerCompatibleFieldDecorator(
34
+ targetOrValue,
35
+ propertyKeyOrContext,
36
+ (className, propertyKey) => {
37
+ ObjectRegistry.registerFieldDecorator(className, propertyKey, {
38
+ type: "foreignKey",
39
+ related: "Tenant",
40
+ sqlType: "UUID",
41
+ required: opts.required,
42
+ nullable: opts.nullable,
43
+ __tenancy: {
44
+ ...opts,
45
+ isTenantIdField: true
46
+ }
47
+ });
48
+ }
49
+ );
50
+ });
51
+ }
52
+ function isTenantIdField(field) {
53
+ if (!field || typeof field !== "object") {
54
+ return false;
55
+ }
56
+ const def = field;
57
+ const tenancy = def.__tenancy;
58
+ return tenancy?.isTenantIdField === true;
59
+ }
60
+ function getTenantIdFieldOptions(field) {
61
+ if (!isTenantIdField(field)) {
62
+ return null;
63
+ }
64
+ const def = field;
65
+ return def.__tenancy;
66
+ }
67
+ export {
68
+ T as TenantContext,
69
+ a2 as TenantContextError,
70
+ b2 as TenantIsolationError,
71
+ TenantScoped,
72
+ a3 as assertTenantContextRequired,
73
+ b3 as assertTenantIsolationViolation,
74
+ c3 as clearTenantScopedRegistry,
75
+ c as createCliContext,
76
+ a as createExpressMiddleware,
77
+ b as createSvelteKitHandle,
78
+ d2 as createTenantInterceptor,
79
+ e2 as createTestTenantContext,
80
+ f2 as disableTenancy,
81
+ g2 as enableTenancy,
82
+ e as enterTenantContext,
83
+ h2 as getAllTenantScopedClasses,
84
+ g as getCurrentTenant,
85
+ c2 as getTenantId,
86
+ getTenantIdFieldOptions,
87
+ i2 as getTenantScopedConfig,
88
+ h as hasTenantContext,
89
+ i as isSuperAdminBypass,
90
+ d as isSystemContext,
91
+ j2 as isTenancyEnabled,
92
+ isTenantIdField,
93
+ k2 as isTenantScopedClass,
94
+ registerTenantScopedClass,
95
+ r as requireTenant,
96
+ f as requireTenantId,
97
+ l2 as resetTenancy,
98
+ m as runTenantScopedEntryPoint,
99
+ s as setupTestTenancy,
100
+ tenantId,
101
+ t as testTenantIsolation,
102
+ u as unregisterTenantScopedClass,
103
+ w as withSuperAdminBypass,
104
+ j as withSystemContext,
105
+ k as withTenant,
106
+ l as withTenantSync
107
+ };
108
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sources":["../src/__smrt-register__.ts","../src/decorators.ts","../src/fields.ts"],"sourcesContent":["/**\n * Self-registers this package's build-time manifest before any @smrt() decorator\n * in the package fires. Fixes issue #1132: in consumer runtimes (tsx, SvelteKit\n * SSR, plain `vite dev`) the decorator's synchronous manifest lookup previously\n * missed because no step populated the global manifest cache — classes got\n * registered with zero fields and `save()` / `toJSON()` silently dropped every\n * declared property.\n *\n * Import this module as the first statement in `src/index.ts` so its top-level\n * side effect runs ahead of any class module's @smrt() decorator.\n *\n * Silent no-op in dev/test, where the vitest plugin already populates manifests\n * via a different path. Only needs to succeed in the published dist output.\n *\n * @see https://github.com/happyvertical/smrt/issues/1132\n */\nimport { ObjectRegistry } from '@happyvertical/smrt-core';\n\n// `new URL('./manifest.json', import.meta.url)` resolves at runtime to the\n// manifest sitting next to this module's compiled output. Vite warns at build\n// time that it cannot pre-resolve the URL; that is the intended behavior —\n// the URL must resolve to dist/manifest.json at runtime, not be inlined.\nObjectRegistry.registerPackageManifest(\n new URL('./manifest.json', import.meta.url),\n);\n","/**\n * Tenancy Decorators\n *\n * Provides class and property decorators for tenant-scoped SMRT objects.\n *\n * @example\n * ```typescript\n * import { smrt, SmrtObject } from '@happyvertical/smrt-core';\n * import { TenantScoped, tenantId } from '@happyvertical/smrt-tenancy';\n *\n * @smrt()\n * @TenantScoped({ mode: 'optional' })\n * class Document extends SmrtObject {\n * @tenantId({ nullable: true })\n * tenantId: string | null = null; // null = global document\n *\n * title: string = '';\n * }\n * ```\n *\n * @see https://github.com/happyvertical/smrt/issues/675\n * @see https://github.com/happyvertical/smrt/issues/829\n */\n\nimport {\n applyPendingDecoratorRegistrations,\n type CompatiblePropertyDecorator,\n type CompatiblePropertyDecoratorContext,\n type LegacyPropertyDecoratorTarget,\n ObjectRegistry,\n registerCompatibleFieldDecorator,\n} from '@happyvertical/smrt-core';\nimport type { TenantIdFieldOptions } from './fields.js';\nimport {\n registerTenantScopedClass,\n type TenantScopedConfig,\n} from './registry.js';\n\n/**\n * Options accepted by the `@TenantScoped()` class decorator.\n *\n * All fields are optional; defaults match the most restrictive safe behaviour\n * (required mode, auto-filter and auto-populate enabled, no super-admin bypass).\n *\n * @see TenantScoped\n * @see TenantScopedConfig\n */\nexport interface TenantScopedOptions {\n /**\n * Tenancy mode for this class\n * - 'required': Must have tenant context for all operations (default)\n * - 'optional': Works with or without tenant context\n */\n mode?: 'required' | 'optional';\n\n /**\n * Field name containing tenant ID\n * @default 'tenantId'\n */\n field?: string;\n\n /**\n * Auto-filter all queries by tenant\n * @default true\n */\n autoFilter?: boolean;\n\n /**\n * Auto-populate tenant ID from context on create\n * @default true\n */\n autoPopulate?: boolean;\n\n /**\n * Allow super admin bypass for this class\n * @default false - must be explicitly enabled\n */\n allowSuperAdminBypass?: boolean;\n}\n\n/**\n * Mark a class as tenant-scoped\n *\n * This decorator registers the class with the tenancy system so that:\n * - list()/get() queries are automatically filtered by tenant\n * - save() validates tenant ID matches current context\n * - delete() validates tenant ownership\n * - Raw SQL queries trigger policy enforcement\n *\n * @param options - Configuration options\n *\n * @example Basic usage (required tenancy)\n * ```typescript\n * @smrt()\n * @TenantScoped()\n * class Document extends SmrtObject {\n * @tenantId()\n * tenantId: string = '';\n *\n * title: string = '';\n * }\n * ```\n *\n * @example With super admin bypass enabled\n * ```typescript\n * @smrt()\n * @TenantScoped({ allowSuperAdminBypass: true })\n * class AuditLog extends SmrtObject {\n * @tenantId()\n * tenantId: string = '';\n *\n * action: string = '';\n * }\n * ```\n *\n * @example Optional tenancy (works with or without context)\n * ```typescript\n * @smrt()\n * @TenantScoped({ mode: 'optional' })\n * class GlobalConfig extends SmrtObject {\n * @tenantId({ nullable: true })\n * tenantId: string | null = null; // null = global, string = tenant-specific\n *\n * key: string = '';\n * value: string = '';\n * }\n * ```\n */\nexport function TenantScoped(options: TenantScopedOptions = {}) {\n return <T extends Function>(\n target: T,\n decoratorContext?: ClassDecoratorContext,\n ): T => {\n applyPendingDecoratorRegistrations(target, decoratorContext);\n\n const className = target.name;\n\n // Merge with defaults\n const config: Partial<TenantScopedConfig> = {\n mode: options.mode ?? 'required',\n field: options.field ?? 'tenantId',\n autoFilter: options.autoFilter ?? true,\n autoPopulate: options.autoPopulate ?? true,\n allowSuperAdminBypass: options.allowSuperAdminBypass ?? false,\n };\n\n // Register with the tenancy system\n registerTenantScopedClass(className, config);\n\n // Return the class unchanged\n return target;\n };\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Property Decorator: @tenantId\n// ─────────────────────────────────────────────────────────────────────────────\n\n/**\n * Tenant ID property decorator\n *\n * Marks a property as the tenant identifier field. This decorator registers\n * the field metadata with ObjectRegistry, keeping the property value clean\n * (no descriptor objects that could be accidentally saved to the database).\n *\n * @param options - Field options (nullable, autoFilter, autoPopulate, etc.)\n * @returns Property decorator\n *\n * @example Basic usage (required tenancy)\n * ```typescript\n * @smrt()\n * @TenantScoped()\n * class Document extends SmrtObject {\n * @tenantId()\n * tenantId: string = '';\n *\n * title: string = '';\n * }\n * ```\n *\n * @example Nullable tenant ID (for global resources)\n * ```typescript\n * @smrt()\n * @TenantScoped({ mode: 'optional' })\n * class GlobalConfig extends SmrtObject {\n * @tenantId({ nullable: true })\n * tenantId: string | null = null; // null = global, string = tenant-specific\n *\n * key: string = '';\n * }\n * ```\n *\n * @see https://github.com/happyvertical/smrt/issues/829 - Why decorators over field helpers\n */\nexport function tenantId(options: TenantIdFieldOptions = {}) {\n const opts = {\n autoFilter: true,\n required: true,\n autoPopulate: true,\n nullable: false,\n ...options,\n };\n\n return ((\n targetOrValue: LegacyPropertyDecoratorTarget | undefined,\n propertyKeyOrContext: CompatiblePropertyDecoratorContext<any, any>,\n ) => {\n registerCompatibleFieldDecorator(\n targetOrValue,\n propertyKeyOrContext,\n (className, propertyKey) => {\n ObjectRegistry.registerFieldDecorator(className, propertyKey, {\n type: 'foreignKey',\n related: 'Tenant',\n sqlType: 'UUID',\n required: opts.required,\n nullable: opts.nullable,\n __tenancy: {\n ...opts,\n isTenantIdField: true,\n },\n });\n },\n );\n }) as CompatiblePropertyDecorator;\n}\n","/**\n * Tenancy Field Types and Utilities\n *\n * This module provides types and utility functions for tenant ID fields.\n * The actual field decorator is in decorators.ts.\n *\n * @see https://github.com/happyvertical/smrt/issues/675\n * @see https://github.com/happyvertical/smrt/issues/829\n */\n\n/**\n * Options for the `@tenantId()` property decorator.\n *\n * Controls how the decorated field interacts with the tenancy interceptor.\n * All options default to the strictest safe values: auto-filter on, required,\n * auto-populate on, not nullable.\n *\n * @see tenantId\n * @see TenantScopedOptions\n */\nexport interface TenantIdFieldOptions {\n /**\n * Auto-filter queries by this field\n * @default true\n */\n autoFilter?: boolean;\n\n /**\n * Require this field to have a value on save\n * @default true\n */\n required?: boolean;\n\n /**\n * Auto-populate from context on create if not set\n * @default true\n */\n autoPopulate?: boolean;\n\n /**\n * Allow null values (for global resources)\n * @default false\n */\n nullable?: boolean;\n}\n\n// Symbol to identify tenantId fields\nexport const TENANT_ID_SYMBOL = Symbol('tenantId');\n\n/**\n * Internal field descriptor stored in `ObjectRegistry` when `@tenantId()` is\n * applied to a property.\n *\n * Consumers should use `isTenantIdField()` and `getTenantIdFieldOptions()`\n * to inspect these descriptors rather than reading the raw properties directly.\n *\n * @see isTenantIdField\n * @see getTenantIdFieldOptions\n */\nexport interface TenantIdFieldDefinition {\n /** Field type marker */\n type: 'foreignKey';\n /** Reference to Tenant class (placeholder - actual class resolved at runtime) */\n reference: 'Tenant';\n /** SQL type */\n sqlType: 'UUID';\n /** Field is required */\n required: boolean;\n /** Field allows null */\n nullable: boolean;\n /** Tenancy-specific options */\n __tenancy: TenantIdFieldOptions & { isTenantIdField: true };\n}\n\n/**\n * Return `true` if the given field definition was produced by the `@tenantId()`\n * decorator (i.e., it has an `__tenancy.isTenantIdField` marker).\n *\n * Used internally by the interceptor and code generators to locate the tenant\n * ID field on a class without knowing its property name in advance.\n *\n * @param field - A raw field definition object, typically from `ObjectRegistry`.\n * @returns `true` if `field` is a tenant ID field definition, `false` otherwise.\n *\n * @example\n * ```typescript\n * const fields = ObjectRegistry.getFields('Document');\n * const tenantField = Object.entries(fields).find(([, def]) => isTenantIdField(def));\n * ```\n *\n * @see getTenantIdFieldOptions\n * @see TenantIdFieldDefinition\n */\nexport function isTenantIdField(field: unknown): boolean {\n if (!field || typeof field !== 'object') {\n return false;\n }\n const def = field as Record<string, unknown>;\n const tenancy = def.__tenancy as Record<string, unknown> | undefined;\n return tenancy?.isTenantIdField === true;\n}\n\n/**\n * Extract the `TenantIdFieldOptions` from a field definition.\n *\n * Returns the tenancy-specific options (autoFilter, required, autoPopulate,\n * nullable) stored inside the field descriptor's `__tenancy` property.\n * Returns `null` if the field was not produced by `@tenantId()`.\n *\n * @param field - A raw field definition object, typically from `ObjectRegistry`.\n * @returns The `TenantIdFieldOptions` if the field is a tenant ID field,\n * `null` otherwise.\n *\n * @see isTenantIdField\n * @see TenantIdFieldOptions\n */\nexport function getTenantIdFieldOptions(\n field: unknown,\n): TenantIdFieldOptions | null {\n if (!isTenantIdField(field)) {\n return null;\n }\n const def = field as { __tenancy: TenantIdFieldOptions };\n return def.__tenancy;\n}\n"],"names":[],"mappings":";;;;;AAsBA,eAAe;AAAA,EACb,IAAA,IAAA,mBAAA,YAAA,GAAA;AACF;ACwGO,SAAS,aAAa,UAA+B,IAAI;AAC9D,SAAO,CACL,QACA,qBACM;AACN,uCAAmC,QAAQ,gBAAgB;AAE3D,UAAM,YAAY,OAAO;AAGzB,UAAM,SAAsC;AAAA,MAC1C,MAAM,QAAQ,QAAQ;AAAA,MACtB,OAAO,QAAQ,SAAS;AAAA,MACxB,YAAY,QAAQ,cAAc;AAAA,MAClC,cAAc,QAAQ,gBAAgB;AAAA,MACtC,uBAAuB,QAAQ,yBAAyB;AAAA,IAAA;AAI1D,8BAA0B,WAAW,MAAM;AAG3C,WAAO;AAAA,EACT;AACF;AA0CO,SAAS,SAAS,UAAgC,IAAI;AAC3D,QAAM,OAAO;AAAA,IACX,YAAY;AAAA,IACZ,UAAU;AAAA,IACV,cAAc;AAAA,IACd,UAAU;AAAA,IACV,GAAG;AAAA,EAAA;AAGL,UAAQ,CACN,eACA,yBACG;AACH;AAAA,MACE;AAAA,MACA;AAAA,MACA,CAAC,WAAW,gBAAgB;AAC1B,uBAAe,uBAAuB,WAAW,aAAa;AAAA,UAC5D,MAAM;AAAA,UACN,SAAS;AAAA,UACT,SAAS;AAAA,UACT,UAAU,KAAK;AAAA,UACf,UAAU,KAAK;AAAA,UACf,WAAW;AAAA,YACT,GAAG;AAAA,YACH,iBAAiB;AAAA,UAAA;AAAA,QACnB,CACD;AAAA,MACH;AAAA,IAAA;AAAA,EAEJ;AACF;ACpIO,SAAS,gBAAgB,OAAyB;AACvD,MAAI,CAAC,SAAS,OAAO,UAAU,UAAU;AACvC,WAAO;AAAA,EACT;AACA,QAAM,MAAM;AACZ,QAAM,UAAU,IAAI;AACpB,SAAO,SAAS,oBAAoB;AACtC;AAgBO,SAAS,wBACd,OAC6B;AAC7B,MAAI,CAAC,gBAAgB,KAAK,GAAG;AAC3B,WAAO;AAAA,EACT;AACA,QAAM,MAAM;AACZ,SAAO,IAAI;AACb;"}
@@ -0,0 +1,156 @@
1
+ import { CollectionInterceptor, DispatchBus, InterceptorContext } from '@happyvertical/smrt-core';
2
+ import { isTenancyEnabled } from './enabled-state.js';
3
+ /**
4
+ * Policy controlling what happens when raw SQL is executed against a
5
+ * tenant-scoped class without an explicit bypass.
6
+ *
7
+ * - `'throw'` — Raises a `TenantIsolationError` (most secure; default).
8
+ * - `'warn'` — Logs a `console.warn` but allows the query to proceed (useful
9
+ * during migration periods).
10
+ * - `'allow'` — Silently allows the query; not recommended for production.
11
+ *
12
+ * @see TenantInterceptorOptions.rawQueryPolicy
13
+ * @see enableTenancy
14
+ */
15
+ export type RawQueryPolicy = 'throw' | 'warn' | 'allow';
16
+ /**
17
+ * Configuration options accepted by `createTenantInterceptor()` and
18
+ * `enableTenancy()`.
19
+ *
20
+ * All options are optional; reasonable defaults are applied. The callback
21
+ * hooks (`onRawQuery`, `onMissingContext`, `onIsolationViolation`) are useful
22
+ * for logging and alerting without altering the enforcement behaviour.
23
+ *
24
+ * @see createTenantInterceptor
25
+ * @see enableTenancy
26
+ */
27
+ export interface TenantInterceptorOptions {
28
+ /**
29
+ * Policy for raw SQL queries on tenant-scoped classes
30
+ * - 'throw': Throw error (most secure, default)
31
+ * - 'warn': Log warning but allow (for migration)
32
+ * - 'allow': Silently allow (not recommended for production)
33
+ * @default 'throw'
34
+ */
35
+ rawQueryPolicy?: RawQueryPolicy;
36
+ /**
37
+ * Called when a raw query is attempted on a tenant-scoped class
38
+ * Useful for logging/auditing
39
+ */
40
+ onRawQuery?: (className: string, sql: string, context: InterceptorContext) => void;
41
+ /**
42
+ * Called when tenant context is missing for a tenant-scoped operation
43
+ */
44
+ onMissingContext?: (className: string, operation: string, context: InterceptorContext) => void;
45
+ /**
46
+ * Called when an isolation violation is detected
47
+ */
48
+ onIsolationViolation?: (className: string, expectedTenantId: string, actualTenantId: string, context: InterceptorContext) => void;
49
+ /**
50
+ * DispatchBus instance for emitting provisioning events on lifecycle changes.
51
+ * When provided along with directoryClasses, afterSave/afterDelete hooks
52
+ * emit dispatches like `directory.membership.created`.
53
+ */
54
+ dispatchBus?: DispatchBus;
55
+ /**
56
+ * Class names to emit directory dispatches for on save/delete lifecycle events.
57
+ * Only classes listed here will trigger dispatch emissions.
58
+ * @example ['Tenant', 'Membership', 'User']
59
+ */
60
+ directoryClasses?: string[];
61
+ }
62
+ /**
63
+ * Create a `CollectionInterceptor` that enforces tenant isolation on all
64
+ * `SmrtCollection` operations.
65
+ *
66
+ * The returned interceptor hooks into the smrt-core `GlobalInterceptors`
67
+ * pipeline at priority 100 (runs before all other interceptors) and
68
+ * handles the following lifecycle hooks:
69
+ *
70
+ * | Hook | Behaviour |
71
+ * |---------------|-----------|
72
+ * | `beforeList` | Injects tenant filter into `WHERE`; validates explicit filters. |
73
+ * | `beforeGet` | Converts ID lookups to `{ id, tenantId }` filter objects. |
74
+ * | `beforeSave` | Auto-populates `tenantId`; validates existing values. |
75
+ * | `beforeDelete`| Validates the instance's `tenantId` matches context. |
76
+ * | `beforeQuery` | Enforces `rawQueryPolicy` on raw SQL calls. |
77
+ * | `afterSave` | Emits `directory.<class>.created/updated` via `dispatchBus`. |
78
+ * | `afterDelete` | Emits `directory.<class>.deleted` via `dispatchBus`. |
79
+ *
80
+ * Use `enableTenancy()` to register the interceptor globally. Call this
81
+ * directly only when you need multiple interceptor instances (e.g., for
82
+ * isolated tests or feature flags).
83
+ *
84
+ * @param options - Configuration for the interceptor.
85
+ * @returns A `CollectionInterceptor` ready to be registered with
86
+ * `GlobalInterceptors.register()`.
87
+ *
88
+ * @example
89
+ * ```typescript
90
+ * import { createTenantInterceptor } from '@happyvertical/smrt-tenancy';
91
+ * import { GlobalInterceptors } from '@happyvertical/smrt-core';
92
+ *
93
+ * const interceptor = createTenantInterceptor({ rawQueryPolicy: 'warn' });
94
+ * GlobalInterceptors.register(interceptor);
95
+ * ```
96
+ *
97
+ * @see enableTenancy
98
+ * @see TenantInterceptorOptions
99
+ */
100
+ export declare function createTenantInterceptor(options?: TenantInterceptorOptions): CollectionInterceptor;
101
+ /**
102
+ * Enable tenant enforcement globally
103
+ *
104
+ * Call this once at application startup to enable automatic tenant isolation.
105
+ *
106
+ * @param options - Configuration options
107
+ *
108
+ * @example
109
+ * ```typescript
110
+ * // In your app initialization
111
+ * import { enableTenancy } from '@happyvertical/smrt-tenancy';
112
+ *
113
+ * enableTenancy({
114
+ * rawQueryPolicy: 'throw',
115
+ * onMissingContext: (className, operation) => {
116
+ * console.error(`Missing tenant context for ${operation} on ${className}`);
117
+ * }
118
+ * });
119
+ * ```
120
+ */
121
+ export declare function enableTenancy(options?: TenantInterceptorOptions): void;
122
+ /**
123
+ * Disable global tenant enforcement.
124
+ *
125
+ * Unregisters the interceptor previously installed by `enableTenancy()` and
126
+ * resets the internal enabled flag so `enableTenancy()` can be called again.
127
+ * Idempotent — safe to call even when tenancy was never enabled.
128
+ *
129
+ * Common use-cases:
130
+ * - Test teardown (via `resetTenancy()`).
131
+ * - Temporarily disabling tenancy before reconfiguring with new options.
132
+ *
133
+ * @example
134
+ * ```typescript
135
+ * afterAll(() => {
136
+ * disableTenancy();
137
+ * });
138
+ * ```
139
+ *
140
+ * @see enableTenancy
141
+ * @see isTenancyEnabled
142
+ * @see resetTenancy
143
+ */
144
+ export declare function disableTenancy(): void;
145
+ /**
146
+ * Return `true` if tenant enforcement is currently active.
147
+ *
148
+ * Reflects whether `enableTenancy()` has been called and the interceptor has not
149
+ * yet been removed by `disableTenancy()`. Re-exported from `enabled-state.ts`
150
+ * (the shared leaf module) so the public API surface is unchanged.
151
+ *
152
+ * @see enableTenancy
153
+ * @see disableTenancy
154
+ */
155
+ export { isTenancyEnabled };
156
+ //# sourceMappingURL=interceptor.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"interceptor.d.ts","sourceRoot":"","sources":["../src/interceptor.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAIH,OAAO,EACL,KAAK,qBAAqB,EAC1B,KAAK,WAAW,EAEhB,KAAK,kBAAkB,EAMxB,MAAM,0BAA0B,CAAC;AASlC,OAAO,EAAE,gBAAgB,EAAqB,MAAM,oBAAoB,CAAC;AAMzE;;;;;;;;;;;GAWG;AACH,MAAM,MAAM,cAAc,GAAG,OAAO,GAAG,MAAM,GAAG,OAAO,CAAC;AAExD;;;;;;;;;;GAUG;AACH,MAAM,WAAW,wBAAwB;IACvC;;;;;;OAMG;IACH,cAAc,CAAC,EAAE,cAAc,CAAC;IAEhC;;;OAGG;IACH,UAAU,CAAC,EAAE,CACX,SAAS,EAAE,MAAM,EACjB,GAAG,EAAE,MAAM,EACX,OAAO,EAAE,kBAAkB,KACxB,IAAI,CAAC;IAEV;;OAEG;IACH,gBAAgB,CAAC,EAAE,CACjB,SAAS,EAAE,MAAM,EACjB,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,kBAAkB,KACxB,IAAI,CAAC;IAEV;;OAEG;IACH,oBAAoB,CAAC,EAAE,CACrB,SAAS,EAAE,MAAM,EACjB,gBAAgB,EAAE,MAAM,EACxB,cAAc,EAAE,MAAM,EACtB,OAAO,EAAE,kBAAkB,KACxB,IAAI,CAAC;IAEV;;;;OAIG;IACH,WAAW,CAAC,EAAE,WAAW,CAAC;IAE1B;;;;OAIG;IACH,gBAAgB,CAAC,EAAE,MAAM,EAAE,CAAC;CAC7B;AA2CD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqCG;AACH,wBAAgB,uBAAuB,CACrC,OAAO,GAAE,wBAA6B,GACrC,qBAAqB,CA6ZvB;AAWD;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,aAAa,CAAC,OAAO,GAAE,wBAA6B,GAAG,IAAI,CAuB1E;AAED;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAgB,cAAc,IAAI,IAAI,CAarC;AAED;;;;;;;;;GASG;AACH,OAAO,EAAE,gBAAgB,EAAE,CAAC"}
@@ -0,0 +1,11 @@
1
+ {
2
+ "version": "1.0.0",
3
+ "timestamp": 1782177071262,
4
+ "packageName": "@happyvertical/smrt-tenancy",
5
+ "packageVersion": "0.30.0",
6
+ "objects": {},
7
+ "moduleType": "smrt",
8
+ "smrtDependencies": [
9
+ "@happyvertical/smrt-core"
10
+ ]
11
+ }
@@ -0,0 +1,2 @@
1
+ export { default } from './svelte/playground.js';
2
+ //# sourceMappingURL=playground.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"playground.d.ts","sourceRoot":"","sources":["../src/playground.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,wBAAwB,CAAC"}
@@ -0,0 +1,80 @@
1
+ import { TENANCY_MODULE_META } from "./ui.js";
2
+ const noop = () => {
3
+ };
4
+ const sampleTenants = [
5
+ {
6
+ id: "tenant-riverstone",
7
+ name: "Riverstone Newsroom",
8
+ slug: "riverstone-newsroom",
9
+ status: "active"
10
+ },
11
+ {
12
+ id: "tenant-harbor",
13
+ name: "Harbor Labs",
14
+ slug: "harbor-labs",
15
+ status: "suspended"
16
+ }
17
+ ];
18
+ const sampleMemberships = [
19
+ {
20
+ id: "membership-riverstone",
21
+ tenantId: "tenant-riverstone"
22
+ },
23
+ {
24
+ id: "membership-harbor",
25
+ tenantId: "tenant-harbor"
26
+ }
27
+ ];
28
+ const loadTenantCard = () => import("./svelte/components/TenantCard.svelte");
29
+ const loadTenantSwitcher = () => import("./svelte/components/TenantSwitcher.svelte");
30
+ const playground = {
31
+ packageName: "@happyvertical/smrt-tenancy",
32
+ displayName: TENANCY_MODULE_META.displayName,
33
+ description: TENANCY_MODULE_META.description,
34
+ moduleMeta: TENANCY_MODULE_META,
35
+ entries: [
36
+ {
37
+ id: "tenant-card",
38
+ title: "Tenant Card",
39
+ description: "Tenant identity and status card with optional membership count and actions.",
40
+ loadComponent: loadTenantCard,
41
+ order: 1,
42
+ props: {
43
+ tenant: sampleTenants[0],
44
+ memberCount: 18,
45
+ selected: true,
46
+ actions: true,
47
+ onclick: noop,
48
+ onedit: noop,
49
+ ondelete: noop
50
+ },
51
+ modes: {
52
+ mock: {
53
+ label: "Mock"
54
+ }
55
+ }
56
+ },
57
+ {
58
+ id: "tenant-switcher",
59
+ title: "Tenant Switcher",
60
+ description: "Membership-aware tenant selector used for multi-tenant application shells.",
61
+ loadComponent: loadTenantSwitcher,
62
+ order: 2,
63
+ props: {
64
+ memberships: sampleMemberships,
65
+ tenants: new Map(sampleTenants.map((tenant) => [tenant.id, tenant])),
66
+ currentTenantId: "tenant-riverstone",
67
+ onchange: noop
68
+ },
69
+ modes: {
70
+ mock: {
71
+ label: "Mock"
72
+ }
73
+ }
74
+ }
75
+ ]
76
+ };
77
+ export {
78
+ playground as default
79
+ };
80
+ //# sourceMappingURL=playground.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"playground.js","sources":["../src/svelte/playground.ts"],"sourcesContent":["import { TENANCY_MODULE_META } from '../ui.js';\n\nconst noop = () => {};\n\nconst sampleTenants = [\n {\n id: 'tenant-riverstone',\n name: 'Riverstone Newsroom',\n slug: 'riverstone-newsroom',\n status: 'active',\n },\n {\n id: 'tenant-harbor',\n name: 'Harbor Labs',\n slug: 'harbor-labs',\n status: 'suspended',\n },\n];\n\nconst sampleMemberships = [\n {\n id: 'membership-riverstone',\n tenantId: 'tenant-riverstone',\n },\n {\n id: 'membership-harbor',\n tenantId: 'tenant-harbor',\n },\n];\n\nconst loadTenantCard = () => import('./components/TenantCard.svelte');\nconst loadTenantSwitcher = () => import('./components/TenantSwitcher.svelte');\n\nexport default {\n packageName: '@happyvertical/smrt-tenancy',\n displayName: TENANCY_MODULE_META.displayName,\n description: TENANCY_MODULE_META.description,\n moduleMeta: TENANCY_MODULE_META,\n entries: [\n {\n id: 'tenant-card',\n title: 'Tenant Card',\n description:\n 'Tenant identity and status card with optional membership count and actions.',\n loadComponent: loadTenantCard,\n order: 1,\n props: {\n tenant: sampleTenants[0],\n memberCount: 18,\n selected: true,\n actions: true,\n onclick: noop,\n onedit: noop,\n ondelete: noop,\n },\n modes: {\n mock: {\n label: 'Mock',\n },\n },\n },\n {\n id: 'tenant-switcher',\n title: 'Tenant Switcher',\n description:\n 'Membership-aware tenant selector used for multi-tenant application shells.',\n loadComponent: loadTenantSwitcher,\n order: 2,\n props: {\n memberships: sampleMemberships,\n tenants: new Map(sampleTenants.map((tenant) => [tenant.id, tenant])),\n currentTenantId: 'tenant-riverstone',\n onchange: noop,\n },\n modes: {\n mock: {\n label: 'Mock',\n },\n },\n },\n ],\n};\n"],"names":[],"mappings":";AAEA,MAAM,OAAO,MAAM;AAAC;AAEpB,MAAM,gBAAgB;AAAA,EACpB;AAAA,IACE,IAAI;AAAA,IACJ,MAAM;AAAA,IACN,MAAM;AAAA,IACN,QAAQ;AAAA,EAAA;AAAA,EAEV;AAAA,IACE,IAAI;AAAA,IACJ,MAAM;AAAA,IACN,MAAM;AAAA,IACN,QAAQ;AAAA,EAAA;AAEZ;AAEA,MAAM,oBAAoB;AAAA,EACxB;AAAA,IACE,IAAI;AAAA,IACJ,UAAU;AAAA,EAAA;AAAA,EAEZ;AAAA,IACE,IAAI;AAAA,IACJ,UAAU;AAAA,EAAA;AAEd;AAEA,MAAM,iBAAiB,MAAM,OAAO,uCAAgC;AACpE,MAAM,qBAAqB,MAAM,OAAO,2CAAoC;AAE5E,MAAA,aAAe;AAAA,EACb,aAAa;AAAA,EACb,aAAa,oBAAoB;AAAA,EACjC,aAAa,oBAAoB;AAAA,EACjC,YAAY;AAAA,EACZ,SAAS;AAAA,IACP;AAAA,MACE,IAAI;AAAA,MACJ,OAAO;AAAA,MACP,aACE;AAAA,MACF,eAAe;AAAA,MACf,OAAO;AAAA,MACP,OAAO;AAAA,QACL,QAAQ,cAAc,CAAC;AAAA,QACvB,aAAa;AAAA,QACb,UAAU;AAAA,QACV,SAAS;AAAA,QACT,SAAS;AAAA,QACT,QAAQ;AAAA,QACR,UAAU;AAAA,MAAA;AAAA,MAEZ,OAAO;AAAA,QACL,MAAM;AAAA,UACJ,OAAO;AAAA,QAAA;AAAA,MACT;AAAA,IACF;AAAA,IAEF;AAAA,MACE,IAAI;AAAA,MACJ,OAAO;AAAA,MACP,aACE;AAAA,MACF,eAAe;AAAA,MACf,OAAO;AAAA,MACP,OAAO;AAAA,QACL,aAAa;AAAA,QACb,SAAS,IAAI,IAAI,cAAc,IAAI,CAAC,WAAW,CAAC,OAAO,IAAI,MAAM,CAAC,CAAC;AAAA,QACnE,iBAAiB;AAAA,QACjB,UAAU;AAAA,MAAA;AAAA,MAEZ,OAAO;AAAA,QACL,MAAM;AAAA,UACJ,OAAO;AAAA,QAAA;AAAA,MACT;AAAA,IACF;AAAA,EACF;AAEJ;"}