@stratum-hq/lib 0.6.0 → 0.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.
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
-- Migration 019: Real Postgres row-level security for the SHARED_RLS strategy.
|
|
2
|
+
--
|
|
3
|
+
-- Backs the `isolation_strategy = 'SHARED_RLS'` literal (001_init.sql) with
|
|
4
|
+
-- database-enforced tenant isolation. This is a SECOND, independent layer; the
|
|
5
|
+
-- application control-plane authorization stays the primary enforcement. See
|
|
6
|
+
-- docs/adr/0001-postgres-rls-defense-in-depth.md for the full design.
|
|
7
|
+
--
|
|
8
|
+
-- Context model:
|
|
9
|
+
-- * Tenant context: SET LOCAL app.current_tenant_id (transaction scoped).
|
|
10
|
+
-- * System bypass: SET LOCAL app.bypass_rls = 'on' (control-plane path).
|
|
11
|
+
-- Both are set with set_config(..., true) / SET LOCAL so they cannot leak
|
|
12
|
+
-- across pooled connections.
|
|
13
|
+
--
|
|
14
|
+
-- Every policy is fail closed: with no context set,
|
|
15
|
+
-- NULLIF(current_setting('app.current_tenant_id', true), '')::uuid -> NULL,
|
|
16
|
+
-- so the scope predicate is NULL (not true) and zero rows are visible; a write
|
|
17
|
+
-- with no context is rejected. current_setting(name, true) returns NULL instead
|
|
18
|
+
-- of raising when the GUC is unset.
|
|
19
|
+
--
|
|
20
|
+
-- ENABLE + FORCE: FORCE applies the policy to the table owner too. Note that a
|
|
21
|
+
-- Postgres SUPERUSER or a role with BYPASSRLS always skips RLS regardless of
|
|
22
|
+
-- FORCE; the application role must be neither (see docker/init-db.sql:
|
|
23
|
+
-- stratum_app is NOSUPERUSER NOBYPASSRLS).
|
|
24
|
+
--
|
|
25
|
+
-- DROP POLICY IF EXISTS before CREATE makes the migration safe to re-run.
|
|
26
|
+
|
|
27
|
+
-- Reusable predicate (documented once; inlined per policy below because Postgres
|
|
28
|
+
-- policies cannot reference a shared expression):
|
|
29
|
+
-- current_setting('app.bypass_rls', true) = 'on'
|
|
30
|
+
-- OR tenant_id = NULLIF(current_setting('app.current_tenant_id', true), '')::uuid
|
|
31
|
+
|
|
32
|
+
-- ---------------------------------------------------------------------------
|
|
33
|
+
-- Direct tenant_id tables: exact-tenant scope.
|
|
34
|
+
-- ---------------------------------------------------------------------------
|
|
35
|
+
|
|
36
|
+
-- config_entries
|
|
37
|
+
ALTER TABLE config_entries ENABLE ROW LEVEL SECURITY;
|
|
38
|
+
ALTER TABLE config_entries FORCE ROW LEVEL SECURITY;
|
|
39
|
+
DROP POLICY IF EXISTS tenant_isolation ON config_entries;
|
|
40
|
+
CREATE POLICY tenant_isolation ON config_entries FOR ALL
|
|
41
|
+
USING (
|
|
42
|
+
current_setting('app.bypass_rls', true) = 'on'
|
|
43
|
+
OR tenant_id = NULLIF(current_setting('app.current_tenant_id', true), '')::uuid
|
|
44
|
+
)
|
|
45
|
+
WITH CHECK (
|
|
46
|
+
current_setting('app.bypass_rls', true) = 'on'
|
|
47
|
+
OR tenant_id = NULLIF(current_setting('app.current_tenant_id', true), '')::uuid
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
-- permission_policies
|
|
51
|
+
ALTER TABLE permission_policies ENABLE ROW LEVEL SECURITY;
|
|
52
|
+
ALTER TABLE permission_policies FORCE ROW LEVEL SECURITY;
|
|
53
|
+
DROP POLICY IF EXISTS tenant_isolation ON permission_policies;
|
|
54
|
+
CREATE POLICY tenant_isolation ON permission_policies FOR ALL
|
|
55
|
+
USING (
|
|
56
|
+
current_setting('app.bypass_rls', true) = 'on'
|
|
57
|
+
OR tenant_id = NULLIF(current_setting('app.current_tenant_id', true), '')::uuid
|
|
58
|
+
)
|
|
59
|
+
WITH CHECK (
|
|
60
|
+
current_setting('app.bypass_rls', true) = 'on'
|
|
61
|
+
OR tenant_id = NULLIF(current_setting('app.current_tenant_id', true), '')::uuid
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
-- audit_logs (tenant_id nullable: ON DELETE SET NULL; NULL rows visible only under bypass)
|
|
65
|
+
ALTER TABLE audit_logs ENABLE ROW LEVEL SECURITY;
|
|
66
|
+
ALTER TABLE audit_logs FORCE ROW LEVEL SECURITY;
|
|
67
|
+
DROP POLICY IF EXISTS tenant_isolation ON audit_logs;
|
|
68
|
+
CREATE POLICY tenant_isolation ON audit_logs FOR ALL
|
|
69
|
+
USING (
|
|
70
|
+
current_setting('app.bypass_rls', true) = 'on'
|
|
71
|
+
OR tenant_id = NULLIF(current_setting('app.current_tenant_id', true), '')::uuid
|
|
72
|
+
)
|
|
73
|
+
WITH CHECK (
|
|
74
|
+
current_setting('app.bypass_rls', true) = 'on'
|
|
75
|
+
OR tenant_id = NULLIF(current_setting('app.current_tenant_id', true), '')::uuid
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
-- webhook_events
|
|
79
|
+
ALTER TABLE webhook_events ENABLE ROW LEVEL SECURITY;
|
|
80
|
+
ALTER TABLE webhook_events FORCE ROW LEVEL SECURITY;
|
|
81
|
+
DROP POLICY IF EXISTS tenant_isolation ON webhook_events;
|
|
82
|
+
CREATE POLICY tenant_isolation ON webhook_events FOR ALL
|
|
83
|
+
USING (
|
|
84
|
+
current_setting('app.bypass_rls', true) = 'on'
|
|
85
|
+
OR tenant_id = NULLIF(current_setting('app.current_tenant_id', true), '')::uuid
|
|
86
|
+
)
|
|
87
|
+
WITH CHECK (
|
|
88
|
+
current_setting('app.bypass_rls', true) = 'on'
|
|
89
|
+
OR tenant_id = NULLIF(current_setting('app.current_tenant_id', true), '')::uuid
|
|
90
|
+
);
|
|
91
|
+
|
|
92
|
+
-- webhooks (tenant_id nullable)
|
|
93
|
+
ALTER TABLE webhooks ENABLE ROW LEVEL SECURITY;
|
|
94
|
+
ALTER TABLE webhooks FORCE ROW LEVEL SECURITY;
|
|
95
|
+
DROP POLICY IF EXISTS tenant_isolation ON webhooks;
|
|
96
|
+
CREATE POLICY tenant_isolation ON webhooks FOR ALL
|
|
97
|
+
USING (
|
|
98
|
+
current_setting('app.bypass_rls', true) = 'on'
|
|
99
|
+
OR tenant_id = NULLIF(current_setting('app.current_tenant_id', true), '')::uuid
|
|
100
|
+
)
|
|
101
|
+
WITH CHECK (
|
|
102
|
+
current_setting('app.bypass_rls', true) = 'on'
|
|
103
|
+
OR tenant_id = NULLIF(current_setting('app.current_tenant_id', true), '')::uuid
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
-- consent_records
|
|
107
|
+
ALTER TABLE consent_records ENABLE ROW LEVEL SECURITY;
|
|
108
|
+
ALTER TABLE consent_records FORCE ROW LEVEL SECURITY;
|
|
109
|
+
DROP POLICY IF EXISTS tenant_isolation ON consent_records;
|
|
110
|
+
CREATE POLICY tenant_isolation ON consent_records FOR ALL
|
|
111
|
+
USING (
|
|
112
|
+
current_setting('app.bypass_rls', true) = 'on'
|
|
113
|
+
OR tenant_id = NULLIF(current_setting('app.current_tenant_id', true), '')::uuid
|
|
114
|
+
)
|
|
115
|
+
WITH CHECK (
|
|
116
|
+
current_setting('app.bypass_rls', true) = 'on'
|
|
117
|
+
OR tenant_id = NULLIF(current_setting('app.current_tenant_id', true), '')::uuid
|
|
118
|
+
);
|
|
119
|
+
|
|
120
|
+
-- abac_policies
|
|
121
|
+
ALTER TABLE abac_policies ENABLE ROW LEVEL SECURITY;
|
|
122
|
+
ALTER TABLE abac_policies FORCE ROW LEVEL SECURITY;
|
|
123
|
+
DROP POLICY IF EXISTS tenant_isolation ON abac_policies;
|
|
124
|
+
CREATE POLICY tenant_isolation ON abac_policies FOR ALL
|
|
125
|
+
USING (
|
|
126
|
+
current_setting('app.bypass_rls', true) = 'on'
|
|
127
|
+
OR tenant_id = NULLIF(current_setting('app.current_tenant_id', true), '')::uuid
|
|
128
|
+
)
|
|
129
|
+
WITH CHECK (
|
|
130
|
+
current_setting('app.bypass_rls', true) = 'on'
|
|
131
|
+
OR tenant_id = NULLIF(current_setting('app.current_tenant_id', true), '')::uuid
|
|
132
|
+
);
|
|
133
|
+
|
|
134
|
+
-- api_keys (auth table; tenant_id nullable. Read by the control plane before a
|
|
135
|
+
-- tenant context exists, so it is safe only because the control plane bypasses.
|
|
136
|
+
-- Global keys (tenant_id NULL) are visible only under bypass.)
|
|
137
|
+
ALTER TABLE api_keys ENABLE ROW LEVEL SECURITY;
|
|
138
|
+
ALTER TABLE api_keys FORCE ROW LEVEL SECURITY;
|
|
139
|
+
DROP POLICY IF EXISTS tenant_isolation ON api_keys;
|
|
140
|
+
CREATE POLICY tenant_isolation ON api_keys FOR ALL
|
|
141
|
+
USING (
|
|
142
|
+
current_setting('app.bypass_rls', true) = 'on'
|
|
143
|
+
OR tenant_id = NULLIF(current_setting('app.current_tenant_id', true), '')::uuid
|
|
144
|
+
)
|
|
145
|
+
WITH CHECK (
|
|
146
|
+
current_setting('app.bypass_rls', true) = 'on'
|
|
147
|
+
OR tenant_id = NULLIF(current_setting('app.current_tenant_id', true), '')::uuid
|
|
148
|
+
);
|
|
149
|
+
|
|
150
|
+
-- roles (auth table; tenant_id nullable for global roles, visible only under bypass)
|
|
151
|
+
ALTER TABLE roles ENABLE ROW LEVEL SECURITY;
|
|
152
|
+
ALTER TABLE roles FORCE ROW LEVEL SECURITY;
|
|
153
|
+
DROP POLICY IF EXISTS tenant_isolation ON roles;
|
|
154
|
+
CREATE POLICY tenant_isolation ON roles FOR ALL
|
|
155
|
+
USING (
|
|
156
|
+
current_setting('app.bypass_rls', true) = 'on'
|
|
157
|
+
OR tenant_id = NULLIF(current_setting('app.current_tenant_id', true), '')::uuid
|
|
158
|
+
)
|
|
159
|
+
WITH CHECK (
|
|
160
|
+
current_setting('app.bypass_rls', true) = 'on'
|
|
161
|
+
OR tenant_id = NULLIF(current_setting('app.current_tenant_id', true), '')::uuid
|
|
162
|
+
);
|
|
163
|
+
|
|
164
|
+
-- ---------------------------------------------------------------------------
|
|
165
|
+
-- tenants registry: self-scope on id. The ancestry source; no tenant_id column.
|
|
166
|
+
-- A data-plane context reads only its own registry row and cannot enumerate the
|
|
167
|
+
-- tree. All tree / ancestry mutations are control-plane and run under bypass.
|
|
168
|
+
-- ---------------------------------------------------------------------------
|
|
169
|
+
ALTER TABLE tenants ENABLE ROW LEVEL SECURITY;
|
|
170
|
+
ALTER TABLE tenants FORCE ROW LEVEL SECURITY;
|
|
171
|
+
DROP POLICY IF EXISTS tenant_isolation ON tenants;
|
|
172
|
+
CREATE POLICY tenant_isolation ON tenants FOR ALL
|
|
173
|
+
USING (
|
|
174
|
+
current_setting('app.bypass_rls', true) = 'on'
|
|
175
|
+
OR id = NULLIF(current_setting('app.current_tenant_id', true), '')::uuid
|
|
176
|
+
)
|
|
177
|
+
WITH CHECK (
|
|
178
|
+
current_setting('app.bypass_rls', true) = 'on'
|
|
179
|
+
OR id = NULLIF(current_setting('app.current_tenant_id', true), '')::uuid
|
|
180
|
+
);
|
|
181
|
+
|
|
182
|
+
-- ---------------------------------------------------------------------------
|
|
183
|
+
-- Indirect tables (no tenant_id): scoped through their tenant-bearing parent.
|
|
184
|
+
-- The parent subquery is itself RLS-filtered under a tenant context, which
|
|
185
|
+
-- composes correctly. No policy recursion: neither parent references back.
|
|
186
|
+
-- ---------------------------------------------------------------------------
|
|
187
|
+
|
|
188
|
+
-- webhook_deliveries -> webhook_events.tenant_id
|
|
189
|
+
ALTER TABLE webhook_deliveries ENABLE ROW LEVEL SECURITY;
|
|
190
|
+
ALTER TABLE webhook_deliveries FORCE ROW LEVEL SECURITY;
|
|
191
|
+
DROP POLICY IF EXISTS tenant_isolation ON webhook_deliveries;
|
|
192
|
+
CREATE POLICY tenant_isolation ON webhook_deliveries FOR ALL
|
|
193
|
+
USING (
|
|
194
|
+
current_setting('app.bypass_rls', true) = 'on'
|
|
195
|
+
OR EXISTS (
|
|
196
|
+
SELECT 1 FROM webhook_events we
|
|
197
|
+
WHERE we.id = webhook_deliveries.event_id
|
|
198
|
+
AND we.tenant_id = NULLIF(current_setting('app.current_tenant_id', true), '')::uuid
|
|
199
|
+
)
|
|
200
|
+
)
|
|
201
|
+
WITH CHECK (
|
|
202
|
+
current_setting('app.bypass_rls', true) = 'on'
|
|
203
|
+
OR EXISTS (
|
|
204
|
+
SELECT 1 FROM webhook_events we
|
|
205
|
+
WHERE we.id = webhook_deliveries.event_id
|
|
206
|
+
AND we.tenant_id = NULLIF(current_setting('app.current_tenant_id', true), '')::uuid
|
|
207
|
+
)
|
|
208
|
+
);
|
|
209
|
+
|
|
210
|
+
-- principal_roles -> roles.tenant_id (global-role assignments visible only under bypass)
|
|
211
|
+
ALTER TABLE principal_roles ENABLE ROW LEVEL SECURITY;
|
|
212
|
+
ALTER TABLE principal_roles FORCE ROW LEVEL SECURITY;
|
|
213
|
+
DROP POLICY IF EXISTS tenant_isolation ON principal_roles;
|
|
214
|
+
CREATE POLICY tenant_isolation ON principal_roles FOR ALL
|
|
215
|
+
USING (
|
|
216
|
+
current_setting('app.bypass_rls', true) = 'on'
|
|
217
|
+
OR EXISTS (
|
|
218
|
+
SELECT 1 FROM roles r
|
|
219
|
+
WHERE r.id = principal_roles.role_id
|
|
220
|
+
AND r.tenant_id = NULLIF(current_setting('app.current_tenant_id', true), '')::uuid
|
|
221
|
+
)
|
|
222
|
+
)
|
|
223
|
+
WITH CHECK (
|
|
224
|
+
current_setting('app.bypass_rls', true) = 'on'
|
|
225
|
+
OR EXISTS (
|
|
226
|
+
SELECT 1 FROM roles r
|
|
227
|
+
WHERE r.id = principal_roles.role_id
|
|
228
|
+
AND r.tenant_id = NULLIF(current_setting('app.current_tenant_id', true), '')::uuid
|
|
229
|
+
)
|
|
230
|
+
);
|
|
231
|
+
|
|
232
|
+
-- ---------------------------------------------------------------------------
|
|
233
|
+
-- Intentionally NOT protected (documented in the ADR):
|
|
234
|
+
-- * regions -- global infrastructure catalog, no tenant_id, not tenant-private
|
|
235
|
+
-- * _migrations -- internal migration bookkeeping
|
|
236
|
+
-- ---------------------------------------------------------------------------
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"pool-helpers.d.ts","sourceRoot":"","sources":["../src/pool-helpers.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,IAAI,CAAC;
|
|
1
|
+
{"version":3,"file":"pool-helpers.d.ts","sourceRoot":"","sources":["../src/pool-helpers.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,IAAI,CAAC;AAyBpB,wBAAsB,UAAU,CAAC,CAAC,EAChC,IAAI,EAAE,EAAE,CAAC,IAAI,EACb,EAAE,EAAE,CAAC,MAAM,EAAE,EAAE,CAAC,UAAU,KAAK,OAAO,CAAC,CAAC,CAAC,GACxC,OAAO,CAAC,CAAC,CAAC,CAcZ;AAED,wBAAsB,eAAe,CAAC,CAAC,EACrC,IAAI,EAAE,EAAE,CAAC,IAAI,EACb,EAAE,EAAE,CAAC,MAAM,EAAE,EAAE,CAAC,UAAU,KAAK,OAAO,CAAC,CAAC,CAAC,GACxC,OAAO,CAAC,CAAC,CAAC,CAcZ"}
|
package/dist/pool-helpers.js
CHANGED
|
@@ -2,10 +2,39 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.withClient = withClient;
|
|
4
4
|
exports.withTransaction = withTransaction;
|
|
5
|
+
// The Stratum library is the trusted CONTROL PLANE: it manages the whole tenant
|
|
6
|
+
// tree (authenticating keys before any tenant is known, resolving inherited
|
|
7
|
+
// config up the ancestry chain, listing descendants, moving subtrees, cascade
|
|
8
|
+
// ops). By construction it reads and writes across tenant boundaries, so it runs
|
|
9
|
+
// under the RLS bypass.
|
|
10
|
+
//
|
|
11
|
+
// Both helpers open a transaction and issue `SET LOCAL app.bypass_rls = 'on'`
|
|
12
|
+
// before running the caller's work. SET LOCAL (set_config third arg `true`) is
|
|
13
|
+
// transaction scoped, so the flag cannot leak into a later request that reuses
|
|
14
|
+
// the same pooled connection. Row-level security stays enforced on the separate
|
|
15
|
+
// data-plane path (packages/db-adapters, withTenantContext); see
|
|
16
|
+
// docs/adr/0001-postgres-rls-defense-in-depth.md.
|
|
17
|
+
//
|
|
18
|
+
// This is the single chokepoint for all lib database access: every service goes
|
|
19
|
+
// through withClient / withTransaction, so no service function needs to change.
|
|
20
|
+
async function enterBypass(client) {
|
|
21
|
+
// SET LOCAL (not session SET) so the flag is transaction scoped and cannot
|
|
22
|
+
// leak across pooled connections. Equivalent to
|
|
23
|
+
// set_config('app.bypass_rls', 'on', true).
|
|
24
|
+
await client.query("SET LOCAL app.bypass_rls = 'on'");
|
|
25
|
+
}
|
|
5
26
|
async function withClient(pool, fn) {
|
|
6
27
|
const client = await pool.connect();
|
|
7
28
|
try {
|
|
8
|
-
|
|
29
|
+
await client.query("BEGIN");
|
|
30
|
+
await enterBypass(client);
|
|
31
|
+
const result = await fn(client);
|
|
32
|
+
await client.query("COMMIT");
|
|
33
|
+
return result;
|
|
34
|
+
}
|
|
35
|
+
catch (err) {
|
|
36
|
+
await client.query("ROLLBACK");
|
|
37
|
+
throw err;
|
|
9
38
|
}
|
|
10
39
|
finally {
|
|
11
40
|
client.release();
|
|
@@ -15,6 +44,7 @@ async function withTransaction(pool, fn) {
|
|
|
15
44
|
const client = await pool.connect();
|
|
16
45
|
try {
|
|
17
46
|
await client.query("BEGIN");
|
|
47
|
+
await enterBypass(client);
|
|
18
48
|
const result = await fn(client);
|
|
19
49
|
await client.query("COMMIT");
|
|
20
50
|
return result;
|
package/dist/pool-helpers.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"pool-helpers.js","sourceRoot":"","sources":["../src/pool-helpers.ts"],"names":[],"mappings":";;
|
|
1
|
+
{"version":3,"file":"pool-helpers.js","sourceRoot":"","sources":["../src/pool-helpers.ts"],"names":[],"mappings":";;AAyBA,gCAiBC;AAED,0CAiBC;AA3DD,gFAAgF;AAChF,4EAA4E;AAC5E,8EAA8E;AAC9E,iFAAiF;AACjF,wBAAwB;AACxB,EAAE;AACF,8EAA8E;AAC9E,+EAA+E;AAC/E,+EAA+E;AAC/E,gFAAgF;AAChF,iEAAiE;AACjE,kDAAkD;AAClD,EAAE;AACF,gFAAgF;AAChF,gFAAgF;AAEhF,KAAK,UAAU,WAAW,CAAC,MAAqB;IAC9C,2EAA2E;IAC3E,gDAAgD;IAChD,4CAA4C;IAC5C,MAAM,MAAM,CAAC,KAAK,CAAC,iCAAiC,CAAC,CAAC;AACxD,CAAC;AAEM,KAAK,UAAU,UAAU,CAC9B,IAAa,EACb,EAAyC;IAEzC,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,OAAO,EAAE,CAAC;IACpC,IAAI,CAAC;QACH,MAAM,MAAM,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;QAC5B,MAAM,WAAW,CAAC,MAAM,CAAC,CAAC;QAC1B,MAAM,MAAM,GAAG,MAAM,EAAE,CAAC,MAAM,CAAC,CAAC;QAChC,MAAM,MAAM,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC;QAC7B,OAAO,MAAM,CAAC;IAChB,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,MAAM,MAAM,CAAC,KAAK,CAAC,UAAU,CAAC,CAAC;QAC/B,MAAM,GAAG,CAAC;IACZ,CAAC;YAAS,CAAC;QACT,MAAM,CAAC,OAAO,EAAE,CAAC;IACnB,CAAC;AACH,CAAC;AAEM,KAAK,UAAU,eAAe,CACnC,IAAa,EACb,EAAyC;IAEzC,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,OAAO,EAAE,CAAC;IACpC,IAAI,CAAC;QACH,MAAM,MAAM,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;QAC5B,MAAM,WAAW,CAAC,MAAM,CAAC,CAAC;QAC1B,MAAM,MAAM,GAAG,MAAM,EAAE,CAAC,MAAM,CAAC,CAAC;QAChC,MAAM,MAAM,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC;QAC7B,OAAO,MAAM,CAAC;IAChB,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,MAAM,MAAM,CAAC,KAAK,CAAC,UAAU,CAAC,CAAC;QAC/B,MAAM,GAAG,CAAC;IACZ,CAAC;YAAS,CAAC;QACT,MAAM,CAAC,OAAO,EAAE,CAAC;IACnB,CAAC;AACH,CAAC"}
|