@stardeck-customer-apps/controls 0.0.0-stage → 0.3.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,436 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/server/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ createControlsClient: () => createControlsClient,
24
+ getControl: () => getControl,
25
+ getControls: () => getControls,
26
+ refreshControls: () => refreshControls,
27
+ resetControlsState: () => resetControlsState,
28
+ setControl: () => setControl,
29
+ setUsageProvider: () => setUsageProvider
30
+ });
31
+ module.exports = __toCommonJS(index_exports);
32
+
33
+ // src/controls.ts
34
+ var import_zod = require("zod");
35
+ var CONTROLS = {
36
+ "flag.paymentsSetupStep": { schema: import_zod.z.boolean(), appWrite: false },
37
+ "plan.locations": {
38
+ schema: import_zod.z.object({
39
+ /** Locations the plan includes. null = agreed per deal. */
40
+ included: import_zod.z.number().int().positive().nullable(),
41
+ /** Hard cap. Platform plan: 1; others: null (no cap, extras are billed). */
42
+ max: import_zod.z.number().int().positive().nullable(),
43
+ /** Monthly price of each location past `included`. null = not offered. */
44
+ extraMonthly: import_zod.z.object({
45
+ amount: import_zod.z.number().nonnegative(),
46
+ currency: import_zod.z.enum(["THB", "USD"])
47
+ }).nullable()
48
+ }),
49
+ appWrite: false
50
+ },
51
+ "plan.multiEntity": { schema: import_zod.z.boolean(), appWrite: false },
52
+ "payments.provider": { schema: import_zod.z.enum(["beam", "stripe"]).nullable(), appWrite: true },
53
+ "payments.methods": {
54
+ schema: import_zod.z.object({ card: import_zod.z.boolean(), promptPay: import_zod.z.boolean() }),
55
+ appWrite: true
56
+ },
57
+ "pos.pro": {
58
+ schema: import_zod.z.object({
59
+ /** POS Pro locations paid for. null = unlimited (legacy plans, internal orgs). */
60
+ locations: import_zod.z.number().int().nonnegative().nullable()
61
+ }),
62
+ appWrite: false
63
+ },
64
+ /**
65
+ * The Accounting Module edition the org pays for, each an add-on next to its plan. null means
66
+ * no Accounting plan: the app has no Accounting. `{ ok: false }` means unknown right now:
67
+ * refuse new accounting records and keep existing ones readable.
68
+ */
69
+ "plan.accountingEdition": {
70
+ schema: import_zod.z.enum(["small", "medium"]).nullable(),
71
+ appWrite: false
72
+ }
73
+ };
74
+ var CONTROL_KEYS = Object.keys(CONTROLS);
75
+ function parseOne(entry, schema) {
76
+ if (typeof entry !== "object" || entry === null) {
77
+ return { ok: false, reason: "not provided by the platform" };
78
+ }
79
+ const record = entry;
80
+ if (record.ok === false) {
81
+ return {
82
+ ok: false,
83
+ reason: typeof record.reason === "string" ? record.reason : "unavailable"
84
+ };
85
+ }
86
+ if (record.ok !== true) {
87
+ return { ok: false, reason: "malformed control" };
88
+ }
89
+ const parsed = schema.safeParse(record.value);
90
+ if (!parsed.success) {
91
+ return { ok: false, reason: `invalid value: ${parsed.error.issues[0]?.message ?? "unknown"}` };
92
+ }
93
+ return { ok: true, value: parsed.data };
94
+ }
95
+ function parseControls(raw) {
96
+ const source = typeof raw === "object" && raw !== null && !Array.isArray(raw) ? raw : {};
97
+ const result = {};
98
+ for (const key of CONTROL_KEYS) {
99
+ result[key] = parseOne(source[key], CONTROLS[key].schema);
100
+ }
101
+ return result;
102
+ }
103
+ function unavailableControls(reason) {
104
+ const result = {};
105
+ for (const key of CONTROL_KEYS) result[key] = { ok: false, reason };
106
+ return result;
107
+ }
108
+
109
+ // src/server/client.ts
110
+ var import_server = require("@stardeck-customer-apps/kernel/server");
111
+
112
+ // src/server/state.ts
113
+ var CONTROLS_STATE_SYMBOL = /* @__PURE__ */ Symbol.for("stardeck.controls.state");
114
+ function createControlsState() {
115
+ const state = {
116
+ cache: null,
117
+ inflight: null,
118
+ failure: null,
119
+ generation: 0,
120
+ usageProvider: null,
121
+ configKey: null,
122
+ warnedNotConfigured: false,
123
+ reset() {
124
+ state.cache = null;
125
+ state.inflight = null;
126
+ state.failure = null;
127
+ state.generation += 1;
128
+ }
129
+ };
130
+ return state;
131
+ }
132
+ function getGlobalControlsState() {
133
+ const holder = globalThis;
134
+ holder[CONTROLS_STATE_SYMBOL] ??= createControlsState();
135
+ return holder[CONTROLS_STATE_SYMBOL];
136
+ }
137
+
138
+ // src/server/client.ts
139
+ var DEFAULT_TTL_MS = 6e4;
140
+ var FAILURE_BACKOFF_MS = 5e3;
141
+ var DEFAULT_STALE_TIMEOUT_MS = 1500;
142
+ var DEFAULT_SYNC_TIMEOUT_MS = 3e3;
143
+ var DEFAULT_WRITE_TIMEOUT_MS = 1e4;
144
+ var USAGE_TIMEOUT_MS = 1e3;
145
+ var USER_TOKEN_HEADER = "X-Stardeck-User-Token";
146
+ var WRITE_ERROR_CODES = /* @__PURE__ */ new Set([
147
+ "NOT_WRITABLE",
148
+ "INVALID_VALUE",
149
+ "UNAUTHENTICATED",
150
+ "FORBIDDEN",
151
+ "WRITE_NOT_ALLOWED_HERE",
152
+ "PROVIDER_NOT_READY",
153
+ "UNAVAILABLE"
154
+ ]);
155
+ function describeError(error) {
156
+ return error instanceof Error ? error.message : String(error);
157
+ }
158
+ function isRecord(value) {
159
+ return typeof value === "object" && value !== null && !Array.isArray(value);
160
+ }
161
+ function sanitizeUsage(usage) {
162
+ const clean = {};
163
+ if (Number.isInteger(usage.locations) && usage.locations >= 0) {
164
+ clean.locations = usage.locations;
165
+ }
166
+ return clean;
167
+ }
168
+ function parseWriteError(status, body) {
169
+ const error = isRecord(body) && isRecord(body.error) ? body.error : null;
170
+ if (error && typeof error.code === "string" && WRITE_ERROR_CODES.has(error.code)) {
171
+ return {
172
+ code: error.code,
173
+ message: typeof error.message === "string" ? error.message : `Request failed (${status})`,
174
+ ...typeof error.connectUrl === "string" ? { connectUrl: error.connectUrl } : {}
175
+ };
176
+ }
177
+ return {
178
+ code: "UNAVAILABLE",
179
+ message: `The platform answered ${status} without a usable error`
180
+ };
181
+ }
182
+ async function readJson(response) {
183
+ try {
184
+ return await response.json();
185
+ } catch {
186
+ return null;
187
+ }
188
+ }
189
+ async function withTimeout(promise, ms) {
190
+ let timer;
191
+ const timeout = new Promise((_, reject) => {
192
+ timer = setTimeout(() => reject(new Error(`timed out after ${ms}ms`)), ms);
193
+ });
194
+ try {
195
+ return await Promise.race([promise, timeout]);
196
+ } finally {
197
+ clearTimeout(timer);
198
+ }
199
+ }
200
+ function createControlsClientWithState(config, state) {
201
+ const doFetch = config.fetch ?? ((input, init) => globalThis.fetch(input, init));
202
+ const now = config.now ?? Date.now;
203
+ const ttlMs = config.ttlMs ?? DEFAULT_TTL_MS;
204
+ const staleTimeoutMs = config.staleTimeoutMs ?? DEFAULT_STALE_TIMEOUT_MS;
205
+ const syncTimeoutMs = config.requestTimeoutMs ?? DEFAULT_SYNC_TIMEOUT_MS;
206
+ const writeTimeoutMs = config.requestTimeoutMs ?? DEFAULT_WRITE_TIMEOUT_MS;
207
+ const usageTimeoutMs = Math.min(USAGE_TIMEOUT_MS, syncTimeoutMs);
208
+ const baseUrl = `${config.controlPlaneUrl.replace(/\/+$/, "")}/api/deployments/${encodeURIComponent(config.deploymentId)}/controls`;
209
+ function signedHeaders(extra) {
210
+ return {
211
+ "Content-Type": "application/json",
212
+ "X-Stardeck-Auth": (0, import_server.signDeploymentRequest)(config.deploymentSecret, {
213
+ organizationId: config.organizationId,
214
+ projectId: config.projectId,
215
+ deploymentId: config.deploymentId
216
+ }),
217
+ ...extra
218
+ };
219
+ }
220
+ async function collectUsage() {
221
+ const provider = config.usage ?? state.usageProvider;
222
+ if (!provider) return void 0;
223
+ try {
224
+ return sanitizeUsage(await withTimeout(Promise.resolve().then(provider), usageTimeoutMs));
225
+ } catch (error) {
226
+ console.error(
227
+ `[controls] usage provider failed, syncing without usage: ${describeError(error)}`
228
+ );
229
+ return void 0;
230
+ }
231
+ }
232
+ async function fetchControls() {
233
+ const usage = await collectUsage();
234
+ const body = usage ? { usage } : {};
235
+ const response = await doFetch(baseUrl, {
236
+ method: "POST",
237
+ headers: signedHeaders(),
238
+ body: JSON.stringify(body),
239
+ signal: AbortSignal.timeout(syncTimeoutMs)
240
+ });
241
+ if (!response.ok) {
242
+ throw new Error(`the platform answered ${response.status}`);
243
+ }
244
+ const json = await readJson(response);
245
+ if (!isRecord(json) || !isRecord(json.controls)) {
246
+ throw new Error("the platform answered without a controls map");
247
+ }
248
+ return parseControls(json.controls);
249
+ }
250
+ function recordFailure(generation, reason) {
251
+ if (generation !== state.generation) return;
252
+ const until = now() + FAILURE_BACKOFF_MS;
253
+ if (state.cache) {
254
+ state.cache.expiresAt = until;
255
+ } else {
256
+ state.failure = { until, reason };
257
+ }
258
+ }
259
+ async function runSync() {
260
+ const generation = state.generation;
261
+ try {
262
+ const controls = await fetchControls();
263
+ if (generation === state.generation) {
264
+ state.cache = { controls, expiresAt: now() + ttlMs };
265
+ state.failure = null;
266
+ }
267
+ return controls;
268
+ } catch (error) {
269
+ const reason = `controls sync failed: ${describeError(error)}`;
270
+ console.error(`[controls] refresh failed deployment=${config.deploymentId} ${reason}`);
271
+ recordFailure(generation, reason);
272
+ throw new Error(reason);
273
+ }
274
+ }
275
+ function startSync() {
276
+ if (state.inflight) return state.inflight;
277
+ const promise = runSync();
278
+ state.inflight = promise;
279
+ const clear = () => {
280
+ if (state.inflight === promise) state.inflight = null;
281
+ };
282
+ promise.then(clear, clear);
283
+ return promise;
284
+ }
285
+ async function load() {
286
+ const current = now();
287
+ const cache = state.cache;
288
+ if (cache && current < cache.expiresAt) return cache.controls;
289
+ if (!cache && state.failure && current < state.failure.until) {
290
+ return unavailableControls(state.failure.reason);
291
+ }
292
+ const sync = startSync();
293
+ if (!cache) {
294
+ try {
295
+ return await sync;
296
+ } catch (error) {
297
+ return unavailableControls(describeError(error));
298
+ }
299
+ }
300
+ try {
301
+ return await withTimeout(sync, staleTimeoutMs);
302
+ } catch (error) {
303
+ console.log(
304
+ `[controls] serving last value deployment=${config.deploymentId} (${describeError(error)})`
305
+ );
306
+ return state.cache?.controls ?? cache.controls;
307
+ }
308
+ }
309
+ async function write(key, value, userToken) {
310
+ const body = { value };
311
+ let response;
312
+ try {
313
+ response = await doFetch(`${baseUrl}/${encodeURIComponent(key)}`, {
314
+ method: "PUT",
315
+ headers: signedHeaders({ [USER_TOKEN_HEADER]: userToken }),
316
+ body: JSON.stringify(body),
317
+ signal: AbortSignal.timeout(writeTimeoutMs)
318
+ });
319
+ } catch (error) {
320
+ console.error(`[controls] write failed key=${key}: ${describeError(error)}`);
321
+ return { ok: false, error: { code: "UNAVAILABLE", message: describeError(error) } };
322
+ }
323
+ const json = await readJson(response);
324
+ if (!response.ok) {
325
+ const error = parseWriteError(response.status, json);
326
+ console.log(
327
+ `[controls] write refused key=${key} status=${response.status} code=${error.code}`
328
+ );
329
+ return { ok: false, error };
330
+ }
331
+ if (!isRecord(json) || !isRecord(json.controls)) {
332
+ console.error(`[controls] write answered without a controls map key=${key}`);
333
+ return {
334
+ ok: false,
335
+ error: { code: "UNAVAILABLE", message: "The platform answered without the new controls" }
336
+ };
337
+ }
338
+ const controls = parseControls(json.controls);
339
+ state.generation += 1;
340
+ state.cache = { controls, expiresAt: now() + ttlMs };
341
+ state.failure = null;
342
+ console.log(`[controls] write applied key=${key}`);
343
+ return { ok: true, controls };
344
+ }
345
+ return {
346
+ async get(key) {
347
+ return (await load())[key];
348
+ },
349
+ getAll: load,
350
+ async set(key, value, opts) {
351
+ return write(key, value, opts.userToken);
352
+ },
353
+ async refresh() {
354
+ try {
355
+ return await startSync();
356
+ } catch (error) {
357
+ return state.cache?.controls ?? unavailableControls(describeError(error));
358
+ }
359
+ }
360
+ };
361
+ }
362
+ function createControlsClient(config) {
363
+ return createControlsClientWithState(config, createControlsState());
364
+ }
365
+
366
+ // src/server/index.ts
367
+ var NOT_CONFIGURED = "not configured";
368
+ function readEnvConfig() {
369
+ const controlPlaneUrl = process.env.CONTROL_PLANE_URL;
370
+ const organizationId = process.env.ORGANIZATION_ID;
371
+ const projectId = process.env.PROJECT_ID;
372
+ const deploymentId = process.env.DEPLOYMENT_ID;
373
+ const deploymentSecret = process.env.DEPLOYMENT_SECRET;
374
+ if (!controlPlaneUrl || !organizationId || !projectId || !deploymentId || !deploymentSecret) {
375
+ return null;
376
+ }
377
+ return { controlPlaneUrl, organizationId, projectId, deploymentId, deploymentSecret };
378
+ }
379
+ function envClient() {
380
+ const state = getGlobalControlsState();
381
+ const config = readEnvConfig();
382
+ if (!config) {
383
+ if (!state.warnedNotConfigured) {
384
+ state.warnedNotConfigured = true;
385
+ console.warn(
386
+ "[controls] not configured: CONTROL_PLANE_URL, ORGANIZATION_ID, PROJECT_ID, DEPLOYMENT_ID and DEPLOYMENT_SECRET are required; every control reads as unavailable"
387
+ );
388
+ }
389
+ return null;
390
+ }
391
+ const configKey = `${config.controlPlaneUrl}|${config.deploymentId}|${config.deploymentSecret}`;
392
+ if (state.configKey !== configKey) {
393
+ state.reset();
394
+ state.configKey = configKey;
395
+ }
396
+ return createControlsClientWithState(config, state);
397
+ }
398
+ async function getControl(key) {
399
+ const client = envClient();
400
+ if (!client) return { ok: false, reason: NOT_CONFIGURED };
401
+ return client.get(key);
402
+ }
403
+ async function getControls() {
404
+ const client = envClient();
405
+ return client ? client.getAll() : unavailableControls(NOT_CONFIGURED);
406
+ }
407
+ async function refreshControls() {
408
+ const client = envClient();
409
+ return client ? client.refresh() : unavailableControls(NOT_CONFIGURED);
410
+ }
411
+ async function setControl(key, value, opts) {
412
+ const client = envClient();
413
+ if (!client) {
414
+ return {
415
+ ok: false,
416
+ error: { code: "UNAVAILABLE", message: "Controls are not configured for this app" }
417
+ };
418
+ }
419
+ return client.set(key, value, opts);
420
+ }
421
+ function setUsageProvider(fn) {
422
+ getGlobalControlsState().usageProvider = fn;
423
+ }
424
+ function resetControlsState() {
425
+ getGlobalControlsState().reset();
426
+ }
427
+ // Annotate the CommonJS export names for ESM import in node:
428
+ 0 && (module.exports = {
429
+ createControlsClient,
430
+ getControl,
431
+ getControls,
432
+ refreshControls,
433
+ resetControlsState,
434
+ setControl,
435
+ setUsageProvider
436
+ });
@@ -0,0 +1,156 @@
1
+ import { z } from 'zod';
2
+
3
+ /**
4
+ * The controls list: every value the platform owns and an app reads.
5
+ *
6
+ * Isomorphic and data only. The platform imports this same list, so the
7
+ * platform and the app cannot disagree on a control's shape.
8
+ *
9
+ * Keys are never removed in future versions. An older app keeps asking for the
10
+ * keys it was built with, so a key can be added, never renamed or dropped.
11
+ * `appWrite: true` marks the few controls the app may change on a person's
12
+ * behalf (the platform still checks that person's permissions).
13
+ */
14
+ declare const CONTROLS: {
15
+ readonly "flag.paymentsSetupStep": {
16
+ readonly schema: z.ZodBoolean;
17
+ readonly appWrite: false;
18
+ };
19
+ readonly "plan.locations": {
20
+ readonly schema: z.ZodObject<{
21
+ included: z.ZodNullable<z.ZodNumber>;
22
+ max: z.ZodNullable<z.ZodNumber>;
23
+ extraMonthly: z.ZodNullable<z.ZodObject<{
24
+ amount: z.ZodNumber;
25
+ currency: z.ZodEnum<{
26
+ THB: "THB";
27
+ USD: "USD";
28
+ }>;
29
+ }, z.core.$strip>>;
30
+ }, z.core.$strip>;
31
+ readonly appWrite: false;
32
+ };
33
+ readonly "plan.multiEntity": {
34
+ readonly schema: z.ZodBoolean;
35
+ readonly appWrite: false;
36
+ };
37
+ readonly "payments.provider": {
38
+ readonly schema: z.ZodNullable<z.ZodEnum<{
39
+ beam: "beam";
40
+ stripe: "stripe";
41
+ }>>;
42
+ readonly appWrite: true;
43
+ };
44
+ readonly "payments.methods": {
45
+ readonly schema: z.ZodObject<{
46
+ card: z.ZodBoolean;
47
+ promptPay: z.ZodBoolean;
48
+ }, z.core.$strip>;
49
+ readonly appWrite: true;
50
+ };
51
+ readonly "pos.pro": {
52
+ readonly schema: z.ZodObject<{
53
+ locations: z.ZodNullable<z.ZodNumber>;
54
+ }, z.core.$strip>;
55
+ readonly appWrite: false;
56
+ };
57
+ /**
58
+ * The Accounting Module edition the org pays for, each an add-on next to its plan. null means
59
+ * no Accounting plan: the app has no Accounting. `{ ok: false }` means unknown right now:
60
+ * refuse new accounting records and keep existing ones readable.
61
+ */
62
+ readonly "plan.accountingEdition": {
63
+ readonly schema: z.ZodNullable<z.ZodEnum<{
64
+ small: "small";
65
+ medium: "medium";
66
+ }>>;
67
+ readonly appWrite: false;
68
+ };
69
+ };
70
+ type ControlKey = keyof typeof CONTROLS;
71
+ type ControlValue<K extends ControlKey> = z.infer<(typeof CONTROLS)[K]["schema"]>;
72
+ /** One control as the app sees it. `ok: false` means "unknown right now". */
73
+ type ControlResult<K extends ControlKey> = {
74
+ ok: true;
75
+ value: ControlValue<K>;
76
+ } | {
77
+ ok: false;
78
+ reason: string;
79
+ };
80
+ type Controls = {
81
+ [K in ControlKey]: ControlResult<K>;
82
+ };
83
+ type WritableControlKey = {
84
+ [K in ControlKey]: (typeof CONTROLS)[K]["appWrite"] extends true ? K : never;
85
+ }[ControlKey];
86
+ /** What the app reports to the platform on every sync. Non-negative integers. */
87
+ type ControlsUsage = {
88
+ locations?: number;
89
+ };
90
+ type ControlWriteErrorCode = "NOT_WRITABLE" | "INVALID_VALUE" | "UNAUTHENTICATED" | "FORBIDDEN" | "WRITE_NOT_ALLOWED_HERE" | "PROVIDER_NOT_READY" | "UNAVAILABLE";
91
+ type ControlWriteError = {
92
+ code: ControlWriteErrorCode;
93
+ message: string;
94
+ /** Where the person finishes connecting a provider, when that is the blocker. */
95
+ connectUrl?: string;
96
+ };
97
+
98
+ interface ControlsClientConfig {
99
+ controlPlaneUrl: string;
100
+ organizationId: string;
101
+ projectId: string;
102
+ deploymentId: string;
103
+ deploymentSecret: string;
104
+ ttlMs?: number;
105
+ /** How long a stale cache waits for a refresh before serving the last value. */
106
+ staleTimeoutMs?: number;
107
+ /** Network timeout of one request to the platform (default 3s for a sync, 10s for a write). */
108
+ requestTimeoutMs?: number;
109
+ fetch?: typeof fetch;
110
+ now?: () => number;
111
+ /** Reports usage on each sync. Errors are logged and usage is omitted. */
112
+ usage?: () => Promise<ControlsUsage>;
113
+ }
114
+ type SetControlResult = {
115
+ ok: true;
116
+ controls: Controls;
117
+ } | {
118
+ ok: false;
119
+ error: ControlWriteError;
120
+ };
121
+ interface ControlsClient {
122
+ get<K extends ControlKey>(key: K): Promise<ControlResult<K>>;
123
+ getAll(): Promise<Controls>;
124
+ set<K extends WritableControlKey>(key: K, value: ControlValue<K>, opts: {
125
+ userToken: string;
126
+ }): Promise<SetControlResult>;
127
+ /** Forces a sync now, ignoring the TTL. Resolves with the controls it served. */
128
+ refresh(): Promise<Controls>;
129
+ }
130
+ declare function createControlsClient(config: ControlsClientConfig): ControlsClient;
131
+
132
+ /** One control. `{ ok: false }` when the platform can't be reached or doesn't provide it. */
133
+ declare function getControl<K extends ControlKey>(key: K): Promise<ControlResult<K>>;
134
+ declare function getControls(): Promise<Controls>;
135
+ /**
136
+ * Asks the platform now, ignoring the cache (for example after it refused a payment because the
137
+ * provider changed). Concurrent callers share one request; on failure the last values stay.
138
+ */
139
+ declare function refreshControls(): Promise<Controls>;
140
+ /**
141
+ * Changes a control the app may write, on behalf of the signed-in person. The
142
+ * person's session token travels in `X-Stardeck-User-Token`; the platform
143
+ * checks that person's permissions and refuses the write otherwise.
144
+ */
145
+ declare function setControl<K extends WritableControlKey>(key: K, value: ControlValue<K>, opts: {
146
+ userToken: string;
147
+ }): Promise<SetControlResult>;
148
+ /**
149
+ * Registers what the app reports on every sync (for example its active
150
+ * location count). Called each sync; an error is logged and usage is omitted.
151
+ */
152
+ declare function setUsageProvider(fn: () => Promise<ControlsUsage>): void;
153
+ /** Drops the process cache (used by the testing simulator). */
154
+ declare function resetControlsState(): void;
155
+
156
+ export { type ControlsClient, type ControlsClientConfig, type SetControlResult, createControlsClient, getControl, getControls, refreshControls, resetControlsState, setControl, setUsageProvider };