@happyvertical/smrt-features 0.51.18 → 0.51.20
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.
- package/AGENTS.md +17 -0
- package/README.md +152 -0
- package/dist/__smrt-register__.d.ts +2 -0
- package/dist/__smrt-register__.d.ts.map +1 -0
- package/dist/feature-definition.d.ts +18 -0
- package/dist/feature-definition.d.ts.map +1 -0
- package/dist/feature-definitions.d.ts +13 -0
- package/dist/feature-definitions.d.ts.map +1 -0
- package/dist/feature-override-service.d.ts +41 -0
- package/dist/feature-override-service.d.ts.map +1 -0
- package/dist/feature-override.d.ts +13 -0
- package/dist/feature-override.d.ts.map +1 -0
- package/dist/feature-overrides.d.ts +16 -0
- package/dist/feature-overrides.d.ts.map +1 -0
- package/dist/feature-resolver.d.ts +18 -0
- package/dist/feature-resolver.d.ts.map +1 -0
- package/dist/feature-settings.d.ts +209 -0
- package/dist/feature-settings.d.ts.map +1 -0
- package/dist/feature-sync.d.ts +17 -0
- package/dist/feature-sync.d.ts.map +1 -0
- package/dist/index.d.ts +11 -263
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +155 -2
- package/dist/index.js.map +1 -1
- package/dist/manifest.json +1 -1
- package/dist/smrt-knowledge.json +14 -6
- package/dist/svelte/components/FeatureSettingsPanel.svelte +338 -0
- package/dist/svelte/components/FeatureSettingsPanel.svelte.d.ts +52 -0
- package/dist/svelte/components/FeatureSettingsPanel.svelte.d.ts.map +1 -0
- package/dist/svelte/index.d.ts +17 -0
- package/dist/svelte/index.d.ts.map +1 -0
- package/dist/svelte/index.js +14 -0
- package/dist/svelte/types.d.ts +76 -0
- package/dist/svelte/types.d.ts.map +1 -0
- package/dist/svelte/types.js +37 -0
- package/dist/types.d.ts +107 -131
- package/dist/types.d.ts.map +1 -0
- package/dist/utils.d.ts +28 -0
- package/dist/utils.d.ts.map +1 -0
- package/package.json +26 -7
package/AGENTS.md
CHANGED
|
@@ -8,6 +8,9 @@ Code-first feature flag system with global, app-level, and tenant-hierarchy reso
|
|
|
8
8
|
- `FeatureOverride` (`_smrt_feature_overrides` table) — runtime override state, scoped by `(featureKey, scopeType, scopeId)`
|
|
9
9
|
- `FeatureResolver` — resolves the effective state of a feature in a given context, walking the scope chain
|
|
10
10
|
- `FeatureSyncService` — keeps `_smrt_feature_definitions` in sync with code-registered defaults at boot
|
|
11
|
+
- `FeatureOverrideService` — the only write path; asks a host-supplied authorizer before every write and fails closed (#3013)
|
|
12
|
+
- `FeatureSettingsService` — read/write helper for a management screen: `listFeatureSettings()` returns one row per definition (definition fields, `effectiveEnabled`, `globalEffect`, `tenantEffect`), `setFeatureOverride()` refuses unknown keys and scopes the resolver never reads back, then delegates to `FeatureOverrideService` (#3052)
|
|
13
|
+
- `./svelte` subpath — `FeatureSettingsPanel`, the presentational management panel (#3052)
|
|
11
14
|
|
|
12
15
|
## Resolution chain (priority high → low)
|
|
13
16
|
|
|
@@ -24,6 +27,20 @@ Code-first feature flag system with global, app-level, and tenant-hierarchy reso
|
|
|
24
27
|
- Overrides are write-time validated against the matching definition (effect must be a known `FeatureOverrideEffect`, scope must be valid for the definition's `allowedScopes`)
|
|
25
28
|
- Tenant-hierarchy resolution is an optional integration wired by dependency injection (`FeatureTenantHierarchyProvider`), not a static dependency — `smrt-features` never imports `@happyvertical/smrt-users`. Without a configured provider, tenant-hierarchy resolution falls back to a flat tenant scope. (The consumer that wants hierarchy installs `smrt-users` itself and supplies the provider; `smrt-users` is a dev-only dependency here for tests.)
|
|
26
29
|
|
|
30
|
+
## Management UI (`./svelte`)
|
|
31
|
+
|
|
32
|
+
- Built like every other Svelte-shipping package here (`smrt-tenancy`/`smrt-personas` are the closest models): `vite build --mode library && svelte-package -i src/svelte -o dist/svelte`, an `./svelte` export with a `svelte` condition, `svelte` as an **optional** peer dependency — plus `scripts/prune-svelte-package-artifacts.js` (as `smrt-content` does), because `svelte-package` otherwise copies `__tests__/` and `*.test.*` into the published `dist/svelte`. The root export stays Svelte-free so server-only consumers are unaffected.
|
|
33
|
+
- `FeatureSettingsPanel` is presentational: rows and callbacks in, no fetching, no authorization. `src/svelte/types.ts` deliberately imports nothing from `smrt-core` or the `@smrt()` models, so the browser bundle stays thin; `FeatureSettingsRow` is structurally assignable to `FeatureSettingsView`.
|
|
34
|
+
- Each row is its own `<form>`, so `formAction` gives a working screen with no client JS; `onSave` intercepts the submit instead and reads the row's values straight off the form. The global column is opt-in (`showGlobal` / `globalEditable`) because most apps have no platform-admin tier.
|
|
35
|
+
- Two invariants the selects have to keep, both regression-tested. (1) The tenant column's *Default* option must never name the code default: the tenant scope inherits the global override and, with a `FeatureTenantHierarchyProvider`, ancestor tenants' overrides too, so `defaultEnabled` would tell an operator the opposite of what choosing it does. `inheritedOptionLabel()` names `FeatureSettingsView.inheritedEnabled` when the row carries it and says "Default (inherited)" otherwise; `FeatureSettingsService` sets it only where it is exactly knowable (no tenant override → inherited is the effective state), never derived from `globalEffect`. The global column does name the code default — nothing sits above global. (2) The rows remount on `contextKey ?? features`, so an unsaved choice cannot survive a reload or a tenant switch and be submitted against the new rows.
|
|
36
|
+
- Writes validate the **scope shape** before the authorizer runs: `scopeType` must be `global` or `tenant`, a global write must use `GLOBAL_FEATURE_SCOPE_ID`, and a tenant id must be non-blank and already trimmed. A host authorizer cannot catch this — `scopeType: 'global'` with someone else's tenant id reads as an ordinary global write to an authorizer that only gates the global scope — and the resolver looks rows up by the exact `(featureKey, scopeType, scopeId)` triple, so a row written elsewhere is invisible state, not a setting.
|
|
37
|
+
- **Authorization stays with the consumer.** `FeatureSettingsService` reads are unfiltered and every write goes through `FeatureOverrideService`; constructing the settings service without an `authorize` option makes it read-only (writes fail closed). Never add an authorization decision to this package — hosts differ, and the generated surfaces are closed precisely so that decision has to be theirs.
|
|
38
|
+
- `vitest.config.ts` runs two projects because the halves need incompatible resolution: `server` (node, `smrtVitestPlugin`) and `svelte` (jsdom, `@sveltejs/vite-plugin-svelte`, `conditions: ['browser']`). The browser condition would send `smrt-core` to its browser build, so it must not be global.
|
|
39
|
+
|
|
40
|
+
## Feature keys are persisted identifiers
|
|
41
|
+
|
|
42
|
+
Overrides are stored keyed by the exact `featureKey` string and the resolver looks them up by that string. **Renaming a key orphans every existing override and silently reverts all tenants to the definition default.** Treat a shipped key as immutable; a rename needs a data migration over `_smrt_feature_overrides.feature_key`, not an edit. Renaming the owning package is the usual trigger — keep the old key namespace and let `packageName` track the new package.
|
|
43
|
+
|
|
27
44
|
## Integration with `@happyvertical/smrt-users`
|
|
28
45
|
|
|
29
46
|
When `smrt-users` is present, `FeatureResolver` uses it to walk the tenant hierarchy (parent → grandparent → root). Configure via `FeatureTenantHierarchyProvider`. Without it, tenant lookup is single-level.
|
package/README.md
CHANGED
|
@@ -70,6 +70,158 @@ const service = new FeatureOverrideService(overrides, (request) =>
|
|
|
70
70
|
await service.setOverride(key, 'tenant', session.tenantId, FeatureOverrideEffect.ENABLE);
|
|
71
71
|
```
|
|
72
72
|
|
|
73
|
+
## Management UI
|
|
74
|
+
|
|
75
|
+
The package ships the settings screen so consuming apps do not each rebuild
|
|
76
|
+
it: `FeatureSettingsService` (root export) produces the rows, and
|
|
77
|
+
`FeatureSettingsPanel` (the `./svelte` subpath) renders them. The root export
|
|
78
|
+
stays Svelte-free, so a server-only consumer is unaffected; `svelte` is an
|
|
79
|
+
optional peer dependency.
|
|
80
|
+
|
|
81
|
+
### Consumer recipe
|
|
82
|
+
|
|
83
|
+
**1. Declare the definitions in code.** Feature toggles are declared on the
|
|
84
|
+
`@smrt()` decorator of the class they belong to:
|
|
85
|
+
|
|
86
|
+
```typescript
|
|
87
|
+
@smrt({
|
|
88
|
+
features: {
|
|
89
|
+
drafts: { defaultEnabled: false, label: 'Invoice drafts', description: 'Save invoices as drafts before sending.' },
|
|
90
|
+
},
|
|
91
|
+
})
|
|
92
|
+
export class Invoice extends SmrtObject {}
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
**2. Sync them at startup**, so `_smrt_feature_definitions` matches code:
|
|
96
|
+
|
|
97
|
+
```typescript
|
|
98
|
+
await new FeatureSyncService(dbOptions).syncDefinitions();
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
**3. Load the rows in your server load.** The load is where *your*
|
|
102
|
+
permission check lives — the package makes no authorization decision:
|
|
103
|
+
|
|
104
|
+
```typescript
|
|
105
|
+
// src/routes/settings/features/+page.server.ts
|
|
106
|
+
export async function load(event) {
|
|
107
|
+
const auth = requirePermission(event, APP_PERMISSIONS.tenantAdmin);
|
|
108
|
+
const settings = await FeatureSettingsService.create(dbOptions);
|
|
109
|
+
return {
|
|
110
|
+
tenantId: auth.tenantId,
|
|
111
|
+
features: await settings.listFeatureSettings({
|
|
112
|
+
tenantId: auth.tenantId,
|
|
113
|
+
packageName: '@your/app',
|
|
114
|
+
}),
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
`listFeatureSettings()` returns one `FeatureSettingsRow` per definition —
|
|
120
|
+
the definition fields, `effectiveEnabled` as `FeatureResolver` resolves it
|
|
121
|
+
for that tenant, and `globalEffect` / `tenantEffect` for the raw override row
|
|
122
|
+
at each level. The row is structurally assignable to the panel's
|
|
123
|
+
`FeatureSettingsView`, so no mapping step is needed.
|
|
124
|
+
|
|
125
|
+
**4. Render the panel:**
|
|
126
|
+
|
|
127
|
+
```svelte
|
|
128
|
+
<script lang="ts">
|
|
129
|
+
import { FeatureSettingsPanel } from '@happyvertical/smrt-features/svelte';
|
|
130
|
+
let { data, form } = $props();
|
|
131
|
+
</script>
|
|
132
|
+
|
|
133
|
+
<FeatureSettingsPanel
|
|
134
|
+
features={data.features}
|
|
135
|
+
tenantLabel="Shop"
|
|
136
|
+
contextKey={data.tenantId}
|
|
137
|
+
formAction="?/saveFeature"
|
|
138
|
+
message={form?.message}
|
|
139
|
+
error={form?.error}
|
|
140
|
+
/>
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
The panel is presentational: it takes rows and callbacks, fetches nothing and
|
|
144
|
+
authorizes nothing. Each row renders label, description, key, effective state
|
|
145
|
+
and a **Default / Enable / Disable** control, where *Default* removes the
|
|
146
|
+
override row and returns the feature to whatever the level above resolves to.
|
|
147
|
+
Pass `showGlobal` (and `globalEditable`) only if your app has a platform-admin
|
|
148
|
+
tier; apps without one omit the column entirely. Supply `onSave` instead of
|
|
149
|
+
`formAction` to drive it from a client handler.
|
|
150
|
+
|
|
151
|
+
Two details worth knowing:
|
|
152
|
+
|
|
153
|
+
- The tenant column's *Default* option never names the code default, because
|
|
154
|
+
the tenant scope inherits — from the global override, and, when a
|
|
155
|
+
`FeatureTenantHierarchyProvider` is configured, from ancestor tenants too. It
|
|
156
|
+
names the inherited state when the row carries `inheritedEnabled` (the helper
|
|
157
|
+
sets it whenever it is exactly knowable, i.e. when the tenant has no override
|
|
158
|
+
of its own) and otherwise reads "Default (inherited)". The global column's
|
|
159
|
+
*Default* does name the code default, because the global scope inherits
|
|
160
|
+
nothing.
|
|
161
|
+
- Unsaved edits are discarded whenever `contextKey` changes, or, when you omit
|
|
162
|
+
it, whenever `features` is replaced with a new array. Pass the tenant id as
|
|
163
|
+
`contextKey` so a half-made choice for one tenant can never be submitted
|
|
164
|
+
against another's rows.
|
|
165
|
+
|
|
166
|
+
**5. Wire the write with your own permission check.** `setFeatureOverride()`
|
|
167
|
+
refuses any key without a definition and any scope the resolver would never
|
|
168
|
+
read back — an unknown `scopeType`, a global write under anything but
|
|
169
|
+
`GLOBAL_FEATURE_SCOPE_ID`, or a blank/untrimmed tenant id — then delegates to
|
|
170
|
+
`FeatureOverrideService`, which asks your authorizer and fails closed:
|
|
171
|
+
|
|
172
|
+
```typescript
|
|
173
|
+
export const actions = {
|
|
174
|
+
saveFeature: async (event) => {
|
|
175
|
+
const auth = requirePermission(event, APP_PERMISSIONS.tenantAdmin);
|
|
176
|
+
const form = await event.request.formData();
|
|
177
|
+
const settings = await FeatureSettingsService.create(dbOptions, {
|
|
178
|
+
// Your decision, bound to this caller. Returning anything but `true` denies.
|
|
179
|
+
authorize: (request) =>
|
|
180
|
+
request.scopeType === 'tenant' && request.scopeId === auth.tenantId,
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
try {
|
|
184
|
+
await settings.setTenantFeatureOverride(
|
|
185
|
+
String(form.get('featureKey') ?? ''),
|
|
186
|
+
auth.tenantId,
|
|
187
|
+
featureOverrideEffectFromValue(form.get('tenantEffect')),
|
|
188
|
+
);
|
|
189
|
+
} catch (error) {
|
|
190
|
+
if (error instanceof UnknownFeatureKeyError) return fail(400, { error: 'Unknown feature.' });
|
|
191
|
+
if (error instanceof InvalidFeatureScopeError) return fail(400, { error: 'Invalid scope.' });
|
|
192
|
+
if (error instanceof FeatureOverrideAuthorizationError) return fail(403, { error: 'Not permitted.' });
|
|
193
|
+
throw error;
|
|
194
|
+
}
|
|
195
|
+
return { message: 'Feature overrides saved.' };
|
|
196
|
+
},
|
|
197
|
+
};
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
Constructing the service **without** an `authorize` option makes it
|
|
201
|
+
read-only: every write then fails closed with
|
|
202
|
+
`FeatureOverrideAuthorizationError`. That is the right shape for a load
|
|
203
|
+
function.
|
|
204
|
+
|
|
205
|
+
### What the consumer still owns
|
|
206
|
+
|
|
207
|
+
- the route, its navigation entry, and the page shell;
|
|
208
|
+
- the permission check on both the load and the action;
|
|
209
|
+
- the `authorize` callback (this package never decides who may write);
|
|
210
|
+
- resolving the session's `tenantId` — a tenant id from the URL or a form
|
|
211
|
+
field is a selector, not authorization;
|
|
212
|
+
- calling `FeatureSyncService` at startup.
|
|
213
|
+
|
|
214
|
+
### Feature keys are persisted identifiers
|
|
215
|
+
|
|
216
|
+
Overrides are stored keyed by the exact `featureKey` string and resolved by
|
|
217
|
+
that string at runtime. **Renaming a key orphans every existing override**,
|
|
218
|
+
silently reverting all tenants to the definition default — the change looks
|
|
219
|
+
harmless in a diff and shows up as flags mysteriously resetting. Treat a key
|
|
220
|
+
as immutable once shipped. A rename needs a data migration that rewrites
|
|
221
|
+
`_smrt_feature_overrides.feature_key`, not an edit. (Renaming the owning
|
|
222
|
+
package is the usual way this happens: keep the old key namespace and let
|
|
223
|
+
`packageName` track the new package.)
|
|
224
|
+
|
|
73
225
|
## Documentation
|
|
74
226
|
|
|
75
227
|
- See [`AGENTS.md`](./AGENTS.md) for package-internal patterns
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"__smrt-register__.d.ts","sourceRoot":"","sources":["../src/__smrt-register__.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { SmrtObject } from '@happyvertical/smrt-core';
|
|
2
|
+
import { FeatureDefinitionOptions, FeatureMetadata } from './types.js';
|
|
3
|
+
export declare class FeatureDefinition extends SmrtObject {
|
|
4
|
+
featureKey: string;
|
|
5
|
+
packageName: string;
|
|
6
|
+
qualifiedClassName: string;
|
|
7
|
+
className: string;
|
|
8
|
+
localId: string;
|
|
9
|
+
defaultEnabled: boolean;
|
|
10
|
+
label: string;
|
|
11
|
+
description: string;
|
|
12
|
+
metadata: string;
|
|
13
|
+
visibility: string;
|
|
14
|
+
constructor(options?: FeatureDefinitionOptions);
|
|
15
|
+
getMetadata(): FeatureMetadata;
|
|
16
|
+
setMetadata(metadata: FeatureMetadata | null): void;
|
|
17
|
+
}
|
|
18
|
+
//# sourceMappingURL=feature-definition.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"feature-definition.d.ts","sourceRoot":"","sources":["../src/feature-definition.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAQ,MAAM,0BAA0B,CAAC;AAC5D,OAAO,KAAK,EAAE,wBAAwB,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAG5E,qBASa,iBAAkB,SAAQ,UAAU;IAC/C,UAAU,EAAE,MAAM,CAAM;IACxB,WAAW,EAAE,MAAM,CAAM;IACzB,kBAAkB,EAAE,MAAM,CAAM;IAChC,SAAS,EAAE,MAAM,CAAM;IACvB,OAAO,EAAE,MAAM,CAAM;IACrB,cAAc,EAAE,OAAO,CAAS;IAChC,KAAK,EAAE,MAAM,CAAM;IACnB,WAAW,EAAE,MAAM,CAAM;IACzB,QAAQ,EAAE,MAAM,CAAM;IACtB,UAAU,EAAE,MAAM,CAAY;gBAElB,OAAO,GAAE,wBAA6B;IAuBlD,WAAW,IAAI,eAAe;IAI9B,WAAW,CAAC,QAAQ,EAAE,eAAe,GAAG,IAAI,GAAG,IAAI;CAGpD"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { SmrtCollection } from '@happyvertical/smrt-core';
|
|
2
|
+
import { FeatureDefinition } from './feature-definition.js';
|
|
3
|
+
import { FeatureDefinitionSeed } from './types.js';
|
|
4
|
+
export declare class FeatureDefinitionCollection extends SmrtCollection<FeatureDefinition> {
|
|
5
|
+
static readonly _itemClass: typeof FeatureDefinition;
|
|
6
|
+
findByFeatureKey(featureKey: string): Promise<FeatureDefinition | null>;
|
|
7
|
+
findByPackageName(packageName: string): Promise<FeatureDefinition[]>;
|
|
8
|
+
upsertDefinition(seed: FeatureDefinitionSeed): Promise<{
|
|
9
|
+
definition: FeatureDefinition;
|
|
10
|
+
status: 'created' | 'updated' | 'unchanged';
|
|
11
|
+
}>;
|
|
12
|
+
}
|
|
13
|
+
//# sourceMappingURL=feature-definitions.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"feature-definitions.d.ts","sourceRoot":"","sources":["../src/feature-definitions.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,0BAA0B,CAAC;AAC1D,OAAO,EAAE,iBAAiB,EAAE,MAAM,yBAAyB,CAAC;AAC5D,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,YAAY,CAAC;AAGxD,qBAAa,2BAA4B,SAAQ,cAAc,CAAC,iBAAiB,CAAC;IAChF,MAAM,CAAC,QAAQ,CAAC,UAAU,2BAAqB;IAEzC,gBAAgB,CACpB,UAAU,EAAE,MAAM,GACjB,OAAO,CAAC,iBAAiB,GAAG,IAAI,CAAC;IAQ9B,iBAAiB,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,iBAAiB,EAAE,CAAC;IAMpE,gBAAgB,CAAC,IAAI,EAAE,qBAAqB,GAAG,OAAO,CAAC;QAC3D,UAAU,EAAE,iBAAiB,CAAC;QAC9B,MAAM,EAAE,SAAS,GAAG,SAAS,GAAG,WAAW,CAAC;KAC7C,CAAC;CAyCH"}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { FeatureOverride } from './feature-override.js';
|
|
2
|
+
import { FeatureOverrideCollection } from './feature-overrides.js';
|
|
3
|
+
import { FeatureOverrideEffect, FeatureScopeType } from './types.js';
|
|
4
|
+
/** One override write the host must authorize before it runs (#3013). */
|
|
5
|
+
export interface FeatureOverrideWriteRequest {
|
|
6
|
+
operation: 'set' | 'remove';
|
|
7
|
+
featureKey: string;
|
|
8
|
+
scopeType: FeatureScopeType;
|
|
9
|
+
scopeId: string;
|
|
10
|
+
/** Present for `set`. */
|
|
11
|
+
effect?: FeatureOverrideEffect;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* The host's explicit authorization decision for one override write. Return
|
|
15
|
+
* `true` only when the current caller may write that exact scope; anything
|
|
16
|
+
* else denies.
|
|
17
|
+
*/
|
|
18
|
+
export type FeatureOverrideAuthorizer = (request: FeatureOverrideWriteRequest) => boolean | Promise<boolean>;
|
|
19
|
+
/** Thrown when the authorizer does not return `true`. Nothing is written. */
|
|
20
|
+
export declare class FeatureOverrideAuthorizationError extends Error {
|
|
21
|
+
readonly request: FeatureOverrideWriteRequest;
|
|
22
|
+
readonly status = 403;
|
|
23
|
+
constructor(request: FeatureOverrideWriteRequest);
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Authorized write path for feature overrides (#3013). Override rows are
|
|
27
|
+
* authorization state for every scope and `FeatureOverride` is not
|
|
28
|
+
* tenant-scoped, so generated surfaces are closed. Hosts that let callers
|
|
29
|
+
* change flags construct this service with an authorizer bound to the
|
|
30
|
+
* current caller and write through it; every write asks the authorizer
|
|
31
|
+
* first and fails closed.
|
|
32
|
+
*/
|
|
33
|
+
export declare class FeatureOverrideService {
|
|
34
|
+
private readonly overrides;
|
|
35
|
+
private readonly authorize;
|
|
36
|
+
constructor(overrides: FeatureOverrideCollection, authorize: FeatureOverrideAuthorizer);
|
|
37
|
+
setOverride(featureKey: string, scopeType: FeatureScopeType, scopeId: string, effect: FeatureOverrideEffect): Promise<FeatureOverride>;
|
|
38
|
+
removeOverride(featureKey: string, scopeType: FeatureScopeType, scopeId: string): Promise<boolean>;
|
|
39
|
+
private require;
|
|
40
|
+
}
|
|
41
|
+
//# sourceMappingURL=feature-override-service.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"feature-override-service.d.ts","sourceRoot":"","sources":["../src/feature-override-service.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AAC7D,OAAO,KAAK,EAAE,yBAAyB,EAAE,MAAM,wBAAwB,CAAC;AACxE,OAAO,KAAK,EAAE,qBAAqB,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAE1E,yEAAyE;AACzE,MAAM,WAAW,2BAA2B;IAC1C,SAAS,EAAE,KAAK,GAAG,QAAQ,CAAC;IAC5B,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,gBAAgB,CAAC;IAC5B,OAAO,EAAE,MAAM,CAAC;IAChB,yBAAyB;IACzB,MAAM,CAAC,EAAE,qBAAqB,CAAC;CAChC;AAED;;;;GAIG;AACH,MAAM,MAAM,yBAAyB,GAAG,CACtC,OAAO,EAAE,2BAA2B,KACjC,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;AAEhC,6EAA6E;AAC7E,qBAAa,iCAAkC,SAAQ,KAAK;IAE9C,QAAQ,CAAC,OAAO,EAAE,2BAA2B;IADzD,QAAQ,CAAC,MAAM,OAAO;gBACD,OAAO,EAAE,2BAA2B;CAM1D;AAED;;;;;;;GAOG;AACH,qBAAa,sBAAsB;IAE/B,OAAO,CAAC,QAAQ,CAAC,SAAS;IAC1B,OAAO,CAAC,QAAQ,CAAC,SAAS;gBADT,SAAS,EAAE,yBAAyB,EACpC,SAAS,EAAE,yBAAyB;IAGjD,WAAW,CACf,UAAU,EAAE,MAAM,EAClB,SAAS,EAAE,gBAAgB,EAC3B,OAAO,EAAE,MAAM,EACf,MAAM,EAAE,qBAAqB,GAC5B,OAAO,CAAC,eAAe,CAAC;IAWrB,cAAc,CAClB,UAAU,EAAE,MAAM,EAClB,SAAS,EAAE,gBAAgB,EAC3B,OAAO,EAAE,MAAM,GACd,OAAO,CAAC,OAAO,CAAC;YAKL,OAAO;CAStB"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { SmrtObject } from '@happyvertical/smrt-core';
|
|
2
|
+
import { FeatureOverrideEffect, FeatureOverrideOptions, FeatureScopeType } from './types.js';
|
|
3
|
+
export declare class FeatureOverride extends SmrtObject {
|
|
4
|
+
featureKey: string;
|
|
5
|
+
scopeType: FeatureScopeType;
|
|
6
|
+
scopeId: string;
|
|
7
|
+
effect: FeatureOverrideEffect;
|
|
8
|
+
constructor(options?: FeatureOverrideOptions);
|
|
9
|
+
isInherit(): boolean;
|
|
10
|
+
isEnabled(): boolean;
|
|
11
|
+
isDisabled(): boolean;
|
|
12
|
+
}
|
|
13
|
+
//# sourceMappingURL=feature-override.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"feature-override.d.ts","sourceRoot":"","sources":["../src/feature-override.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAQ,MAAM,0BAA0B,CAAC;AAC5D,OAAO,EACL,qBAAqB,EACrB,KAAK,sBAAsB,EAC3B,KAAK,gBAAgB,EAEtB,MAAM,YAAY,CAAC;AAUpB,qBAOa,eAAgB,SAAQ,UAAU;IAC7C,UAAU,EAAE,MAAM,CAAM;IACxB,SAAS,EAAE,gBAAgB,CAAY;IACvC,OAAO,EAAE,MAAM,CAA2B;IAC1C,MAAM,EAAE,qBAAqB,CAAiC;gBAElD,OAAO,GAAE,sBAA2B;IAShD,SAAS,IAAI,OAAO;IAIpB,SAAS,IAAI,OAAO;IAIpB,UAAU,IAAI,OAAO;CAGtB"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { SmrtCollection } from '@happyvertical/smrt-core';
|
|
2
|
+
import { FeatureOverride } from './feature-override.js';
|
|
3
|
+
import { FeatureOverrideEffect, FeatureScopeType } from './types.js';
|
|
4
|
+
export declare class FeatureOverrideCollection extends SmrtCollection<FeatureOverride> {
|
|
5
|
+
static readonly _itemClass: typeof FeatureOverride;
|
|
6
|
+
findByFeatureKey(featureKey: string): Promise<FeatureOverride[]>;
|
|
7
|
+
findByFeatureAndScope(featureKey: string, scopeType: FeatureScopeType, scopeId: string): Promise<FeatureOverride | null>;
|
|
8
|
+
getGlobalOverride(featureKey: string): Promise<FeatureOverride | null>;
|
|
9
|
+
getTenantOverride(featureKey: string, tenantId: string): Promise<FeatureOverride | null>;
|
|
10
|
+
getOverrideMap(featureKey: string, scopeType: FeatureScopeType, scopeIds: string[]): Promise<Map<string, FeatureOverride>>;
|
|
11
|
+
setOverride(featureKey: string, scopeType: FeatureScopeType, scopeId: string, effect: FeatureOverrideEffect): Promise<FeatureOverride>;
|
|
12
|
+
setGlobalOverride(featureKey: string, effect: FeatureOverrideEffect): Promise<FeatureOverride>;
|
|
13
|
+
setTenantOverride(featureKey: string, tenantId: string, effect: FeatureOverrideEffect): Promise<FeatureOverride>;
|
|
14
|
+
removeOverride(featureKey: string, scopeType: FeatureScopeType, scopeId: string): Promise<boolean>;
|
|
15
|
+
}
|
|
16
|
+
//# sourceMappingURL=feature-overrides.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"feature-overrides.d.ts","sourceRoot":"","sources":["../src/feature-overrides.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,0BAA0B,CAAC;AAC1D,OAAO,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AACxD,OAAO,EACL,KAAK,qBAAqB,EAC1B,KAAK,gBAAgB,EAEtB,MAAM,YAAY,CAAC;AAEpB,qBAAa,yBAA0B,SAAQ,cAAc,CAAC,eAAe,CAAC;IAC5E,MAAM,CAAC,QAAQ,CAAC,UAAU,yBAAmB;IAEvC,gBAAgB,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,eAAe,EAAE,CAAC;IAMhE,qBAAqB,CACzB,UAAU,EAAE,MAAM,EAClB,SAAS,EAAE,gBAAgB,EAC3B,OAAO,EAAE,MAAM,GACd,OAAO,CAAC,eAAe,GAAG,IAAI,CAAC;IAQ5B,iBAAiB,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,eAAe,GAAG,IAAI,CAAC;IAQtE,iBAAiB,CACrB,UAAU,EAAE,MAAM,EAClB,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC,eAAe,GAAG,IAAI,CAAC;IAI5B,cAAc,CAClB,UAAU,EAAE,MAAM,EAClB,SAAS,EAAE,gBAAgB,EAC3B,QAAQ,EAAE,MAAM,EAAE,GACjB,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE,eAAe,CAAC,CAAC;IAiBlC,WAAW,CACf,UAAU,EAAE,MAAM,EAClB,SAAS,EAAE,gBAAgB,EAC3B,OAAO,EAAE,MAAM,EACf,MAAM,EAAE,qBAAqB,GAC5B,OAAO,CAAC,eAAe,CAAC;IAuBrB,iBAAiB,CACrB,UAAU,EAAE,MAAM,EAClB,MAAM,EAAE,qBAAqB,GAC5B,OAAO,CAAC,eAAe,CAAC;IASrB,iBAAiB,CACrB,UAAU,EAAE,MAAM,EAClB,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,qBAAqB,GAC5B,OAAO,CAAC,eAAe,CAAC;IAIrB,cAAc,CAClB,UAAU,EAAE,MAAM,EAClB,SAAS,EAAE,gBAAgB,EAC3B,OAAO,EAAE,MAAM,GACd,OAAO,CAAC,OAAO,CAAC;CAcpB"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { SmrtClassOptions } from '@happyvertical/smrt-core';
|
|
2
|
+
import { FeatureResolutionContext, FeatureResolverOptions, FeatureTargetConstructor } from './types.js';
|
|
3
|
+
export declare class FeatureResolver {
|
|
4
|
+
private readonly options;
|
|
5
|
+
private readonly resolverOptions;
|
|
6
|
+
private featureDefinitions;
|
|
7
|
+
private featureOverrides;
|
|
8
|
+
private initializationPromise;
|
|
9
|
+
private tenantHierarchyPromise;
|
|
10
|
+
constructor(options?: SmrtClassOptions, resolverOptions?: FeatureResolverOptions);
|
|
11
|
+
isEnabled(featureKey: string, context?: FeatureResolutionContext): Promise<boolean>;
|
|
12
|
+
isEnabledFor(classOrInstance: object | FeatureTargetConstructor, localId: string, context?: FeatureResolutionContext): Promise<boolean>;
|
|
13
|
+
private ensureInitialized;
|
|
14
|
+
private resolveBaseState;
|
|
15
|
+
private applyOverride;
|
|
16
|
+
private getTenantHierarchy;
|
|
17
|
+
}
|
|
18
|
+
//# sourceMappingURL=feature-resolver.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"feature-resolver.d.ts","sourceRoot":"","sources":["../src/feature-resolver.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,0BAA0B,CAAC;AAIjE,OAAO,EAEL,KAAK,wBAAwB,EAC7B,KAAK,sBAAsB,EAC3B,KAAK,wBAAwB,EAI9B,MAAM,YAAY,CAAC;AAMpB,qBAAa,eAAe;IAC1B,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAmB;IAC3C,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAyB;IACzD,OAAO,CAAC,kBAAkB,CAA+B;IACzD,OAAO,CAAC,gBAAgB,CAA6B;IACrD,OAAO,CAAC,qBAAqB,CAA8B;IAC3D,OAAO,CAAC,sBAAsB,CACvB;gBAGL,OAAO,GAAE,gBAAqB,EAC9B,eAAe,GAAE,sBAA2B;IAMxC,SAAS,CACb,UAAU,EAAE,MAAM,EAClB,OAAO,GAAE,wBAA6B,GACrC,OAAO,CAAC,OAAO,CAAC;IAqDb,YAAY,CAChB,eAAe,EAAE,MAAM,GAAG,wBAAwB,EAClD,OAAO,EAAE,MAAM,EACf,OAAO,GAAE,wBAA6B,GACrC,OAAO,CAAC,OAAO,CAAC;YAKL,iBAAiB;YAejB,gBAAgB;IAiB9B,OAAO,CAAC,aAAa;YAcP,kBAAkB;CAUjC"}
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
import { SmrtClassOptions } from '@happyvertical/smrt-core';
|
|
2
|
+
import { FeatureDefinitionCollection } from './feature-definitions.js';
|
|
3
|
+
import { FeatureOverride } from './feature-override.js';
|
|
4
|
+
import { FeatureOverrideAuthorizer } from './feature-override-service.js';
|
|
5
|
+
import { FeatureOverrideCollection } from './feature-overrides.js';
|
|
6
|
+
import { FeatureResolver } from './feature-resolver.js';
|
|
7
|
+
import { FeatureMetadata, FeatureOverrideEffect, FeatureResolverOptions, FeatureScopeType } from './types.js';
|
|
8
|
+
/**
|
|
9
|
+
* One feature, rendered for a management screen: the code-owned definition,
|
|
10
|
+
* the value that is actually in effect for the requested tenant, and the raw
|
|
11
|
+
* override row at each scope level so the UI can show *why* it is in effect.
|
|
12
|
+
*
|
|
13
|
+
* Structurally assignable to the presentational `FeatureSettingsView` consumed
|
|
14
|
+
* by `@happyvertical/smrt-features/svelte`, so a server load can hand its rows
|
|
15
|
+
* straight to `FeatureSettingsPanel` without a mapping step.
|
|
16
|
+
*/
|
|
17
|
+
export interface FeatureSettingsRow {
|
|
18
|
+
/**
|
|
19
|
+
* The persisted identifier. Renaming it orphans every existing override —
|
|
20
|
+
* see the warning on {@link FeatureSettingsService}.
|
|
21
|
+
*/
|
|
22
|
+
featureKey: string;
|
|
23
|
+
/** Human label from the definition; may be empty. */
|
|
24
|
+
label: string;
|
|
25
|
+
/** Human description from the definition; may be empty. */
|
|
26
|
+
description: string;
|
|
27
|
+
/** Owning package, as synced from the manifest. */
|
|
28
|
+
packageName: string;
|
|
29
|
+
/** Qualified `package:Class` name of the feature's target. */
|
|
30
|
+
qualifiedClassName: string;
|
|
31
|
+
/** Unqualified class name of the feature's target. */
|
|
32
|
+
className: string;
|
|
33
|
+
/** Class-local feature id. */
|
|
34
|
+
localId: string;
|
|
35
|
+
/** Definition visibility marker (`public` unless the seed says otherwise). */
|
|
36
|
+
visibility: string;
|
|
37
|
+
/** Parsed definition metadata. */
|
|
38
|
+
metadata: FeatureMetadata;
|
|
39
|
+
/** The code-owned default, used when no override applies. */
|
|
40
|
+
defaultEnabled: boolean;
|
|
41
|
+
/**
|
|
42
|
+
* What {@link FeatureResolver} resolves for the requested context — the
|
|
43
|
+
* value the application actually behaves by.
|
|
44
|
+
*/
|
|
45
|
+
effectiveEnabled: boolean;
|
|
46
|
+
/** Effect of the global override row, or `null` when there is none. */
|
|
47
|
+
globalEffect: FeatureOverrideEffect | null;
|
|
48
|
+
/**
|
|
49
|
+
* Effect of the override row for the requested tenant, or `null` when there
|
|
50
|
+
* is none (or when no `tenantId` was requested).
|
|
51
|
+
*/
|
|
52
|
+
tenantEffect: FeatureOverrideEffect | null;
|
|
53
|
+
/**
|
|
54
|
+
* What this feature resolves to for the requested tenant with **no override
|
|
55
|
+
* of its own** — the state that removing the tenant override would produce,
|
|
56
|
+
* which is what a management UI must name on its "back to Default" control.
|
|
57
|
+
*
|
|
58
|
+
* `null` when no `tenantId` was requested — nothing tenant-inherited was
|
|
59
|
+
* computed, and the global state is not an answer to a question about a
|
|
60
|
+
* tenant — and `null` when the tenant holds an override of its own, because
|
|
61
|
+
* then the inherited state depends on the levels above this tenant, which a
|
|
62
|
+
* configured `FeatureTenantHierarchyProvider` extends to ancestor tenants'
|
|
63
|
+
* overrides. It is reported only for a requested tenant with no override of
|
|
64
|
+
* its own, where the inherited state *is* the effective state. Rows that
|
|
65
|
+
* leave it `null` get the neutral "Default (inherited)" label in the panel,
|
|
66
|
+
* never a claim about a state nothing computed.
|
|
67
|
+
*/
|
|
68
|
+
inheritedEnabled: boolean | null;
|
|
69
|
+
}
|
|
70
|
+
/** Selection for {@link FeatureSettingsService.listFeatureSettings}. */
|
|
71
|
+
export interface ListFeatureSettingsOptions {
|
|
72
|
+
/**
|
|
73
|
+
* Tenant whose overrides and effective values to report. Omit for a
|
|
74
|
+
* global-only view.
|
|
75
|
+
*/
|
|
76
|
+
tenantId?: string;
|
|
77
|
+
/** Restrict to one owning package (the usual case for an app's own screen). */
|
|
78
|
+
packageName?: string;
|
|
79
|
+
/**
|
|
80
|
+
* Restrict to an explicit allowlist of keys. Keys with no definition are
|
|
81
|
+
* simply absent from the result — this filters, it does not validate.
|
|
82
|
+
*/
|
|
83
|
+
featureKeys?: string[];
|
|
84
|
+
}
|
|
85
|
+
/** One override write requested through {@link FeatureSettingsService}. */
|
|
86
|
+
export interface SetFeatureOverrideRequest {
|
|
87
|
+
featureKey: string;
|
|
88
|
+
scopeType: FeatureScopeType;
|
|
89
|
+
scopeId: string;
|
|
90
|
+
/**
|
|
91
|
+
* `INHERIT` removes the override row at that scope, returning the feature to
|
|
92
|
+
* whatever the next level up resolves to.
|
|
93
|
+
*/
|
|
94
|
+
effect: FeatureOverrideEffect;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Thrown when a write names a `featureKey` that has no {@link FeatureDefinition}
|
|
98
|
+
* row. Nothing is written.
|
|
99
|
+
*
|
|
100
|
+
* This is the generalized form of the per-app "is this one of my keys?" guard
|
|
101
|
+
* every consumer would otherwise hand-roll to keep a form post from creating an
|
|
102
|
+
* override for an arbitrary string.
|
|
103
|
+
*/
|
|
104
|
+
export declare class UnknownFeatureKeyError extends Error {
|
|
105
|
+
readonly featureKey: string;
|
|
106
|
+
readonly status = 400;
|
|
107
|
+
constructor(featureKey: string);
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Thrown when a write names a scope the resolver would never read back: an
|
|
111
|
+
* unknown `scopeType`, a global write under anything but
|
|
112
|
+
* {@link GLOBAL_FEATURE_SCOPE_ID}, or a tenant write with a blank or untrimmed
|
|
113
|
+
* `scopeId`. Nothing is written.
|
|
114
|
+
*
|
|
115
|
+
* `FeatureOverride` rows are looked up by the exact `(featureKey, scopeType,
|
|
116
|
+
* scopeId)` triple, so a row stored under a scope nothing queries is invisible
|
|
117
|
+
* state: it never takes effect, and it silently reappears if the scope naming
|
|
118
|
+
* is ever corrected.
|
|
119
|
+
*/
|
|
120
|
+
export declare class InvalidFeatureScopeError extends Error {
|
|
121
|
+
readonly scopeType: string;
|
|
122
|
+
readonly scopeId: string;
|
|
123
|
+
readonly status = 400;
|
|
124
|
+
constructor(scopeType: string, scopeId: string, reason: string);
|
|
125
|
+
}
|
|
126
|
+
/** Options for {@link FeatureSettingsService.create}. */
|
|
127
|
+
export interface FeatureSettingsServiceOptions {
|
|
128
|
+
/**
|
|
129
|
+
* The host's authorization decision for each override write, bound to the
|
|
130
|
+
* current caller. **Omitting it makes the service read-only**: every write
|
|
131
|
+
* then fails closed with `FeatureOverrideAuthorizationError`. This
|
|
132
|
+
* package never decides who may write.
|
|
133
|
+
*/
|
|
134
|
+
authorize?: FeatureOverrideAuthorizer;
|
|
135
|
+
/** Forwarded to {@link FeatureResolver} (e.g. the tenant-hierarchy loader). */
|
|
136
|
+
resolver?: FeatureResolverOptions;
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Coerce an untrusted value (a form field, a query parameter, a JSON body) to a
|
|
140
|
+
* {@link FeatureOverrideEffect}. Anything unrecognized — including `null` and
|
|
141
|
+
* `undefined` — becomes `INHERIT`, i.e. "back to the default", which is the
|
|
142
|
+
* safe reading of a value the host could not parse.
|
|
143
|
+
*/
|
|
144
|
+
export declare function featureOverrideEffectFromValue(value: unknown): FeatureOverrideEffect;
|
|
145
|
+
/**
|
|
146
|
+
* Read and write side of a feature-flag management screen, so consumers do not
|
|
147
|
+
* re-derive the row shape or the unknown-key guard.
|
|
148
|
+
*
|
|
149
|
+
* **Authorization stays with the consumer.** This service makes no
|
|
150
|
+
* authorization decision of its own: reads are unfiltered, and every write is
|
|
151
|
+
* delegated to {@link FeatureOverrideService}, which asks the host-supplied
|
|
152
|
+
* {@link FeatureOverrideAuthorizer} first and fails closed. Call it only from
|
|
153
|
+
* server code that has already established who the caller is. See
|
|
154
|
+
* `packages/features/README.md` for the consumer recipe.
|
|
155
|
+
*
|
|
156
|
+
* **Feature keys are persisted identifiers.** Overrides are stored keyed by the
|
|
157
|
+
* exact `featureKey` string and resolved by that string at runtime. Renaming a
|
|
158
|
+
* key orphans every existing override, silently reverting all tenants to the
|
|
159
|
+
* definition default. Treat a key as immutable once shipped; a rename needs a
|
|
160
|
+
* data migration, not an edit.
|
|
161
|
+
*/
|
|
162
|
+
export declare class FeatureSettingsService {
|
|
163
|
+
private readonly definitions;
|
|
164
|
+
private readonly overrides;
|
|
165
|
+
private readonly resolver;
|
|
166
|
+
private readonly writes;
|
|
167
|
+
constructor(definitions: FeatureDefinitionCollection, overrides: FeatureOverrideCollection, resolver: FeatureResolver, authorize?: FeatureOverrideAuthorizer);
|
|
168
|
+
/**
|
|
169
|
+
* Build a service over the given database options.
|
|
170
|
+
*
|
|
171
|
+
* @param options - SMRT database options, as passed to any collection.
|
|
172
|
+
* @param serviceOptions - Host authorizer and resolver wiring. Omit
|
|
173
|
+
* `authorize` for a read-only (load-function) service.
|
|
174
|
+
*/
|
|
175
|
+
static create(options?: SmrtClassOptions, serviceOptions?: FeatureSettingsServiceOptions): Promise<FeatureSettingsService>;
|
|
176
|
+
/**
|
|
177
|
+
* One {@link FeatureSettingsRow} per matching definition, ordered by owning
|
|
178
|
+
* package, then label, then key, so a rendered list is stable across loads.
|
|
179
|
+
*
|
|
180
|
+
* Makes no authorization decision — the caller has already decided that this
|
|
181
|
+
* principal may see this tenant's flags.
|
|
182
|
+
*/
|
|
183
|
+
listFeatureSettings(options?: ListFeatureSettingsOptions): Promise<FeatureSettingsRow[]>;
|
|
184
|
+
/** The single row for one key, or `null` when no definition owns that key. */
|
|
185
|
+
getFeatureSetting(featureKey: string, tenantId?: string): Promise<FeatureSettingsRow | null>;
|
|
186
|
+
/**
|
|
187
|
+
* Apply one override change.
|
|
188
|
+
*
|
|
189
|
+
* Refuses any `featureKey` without a {@link FeatureDefinition} row and any
|
|
190
|
+
* scope the resolver would never read back, then delegates to
|
|
191
|
+
* {@link FeatureOverrideService} so the host authorizer decides whether this
|
|
192
|
+
* caller may write this scope. `INHERIT` removes the row.
|
|
193
|
+
*
|
|
194
|
+
* @returns the stored override, or `null` when the row was removed (or was
|
|
195
|
+
* already absent).
|
|
196
|
+
* @throws {UnknownFeatureKeyError} when the key has no definition.
|
|
197
|
+
* @throws {InvalidFeatureScopeError} when the scope is not one the resolver reads.
|
|
198
|
+
* @throws `FeatureOverrideAuthorizationError` when the authorizer declines.
|
|
199
|
+
*/
|
|
200
|
+
setFeatureOverride(request: SetFeatureOverrideRequest): Promise<FeatureOverride | null>;
|
|
201
|
+
/** {@link setFeatureOverride} for the global scope. */
|
|
202
|
+
setGlobalFeatureOverride(featureKey: string, effect: FeatureOverrideEffect): Promise<FeatureOverride | null>;
|
|
203
|
+
/** {@link setFeatureOverride} for one tenant. */
|
|
204
|
+
setTenantFeatureOverride(featureKey: string, tenantId: string, effect: FeatureOverrideEffect): Promise<FeatureOverride | null>;
|
|
205
|
+
private assertKnownFeatureKey;
|
|
206
|
+
private loadDefinitions;
|
|
207
|
+
private toRow;
|
|
208
|
+
}
|
|
209
|
+
//# sourceMappingURL=feature-settings.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"feature-settings.d.ts","sourceRoot":"","sources":["../src/feature-settings.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { SmrtClassOptions } from '@happyvertical/smrt-core';
|
|
2
|
+
import { SmartObjectManifest } from '@happyvertical/smrt-core/manifest';
|
|
3
|
+
import { FeatureSyncResult, SyncDefinitionsOptions, SyncManifestOptions } from './types.js';
|
|
4
|
+
export declare class FeatureSyncService {
|
|
5
|
+
private readonly options;
|
|
6
|
+
private featureDefinitions;
|
|
7
|
+
private initializationPromise;
|
|
8
|
+
constructor(options?: SmrtClassOptions);
|
|
9
|
+
syncDefinitions(options?: SyncDefinitionsOptions): Promise<FeatureSyncResult>;
|
|
10
|
+
syncManifest(manifest: SmartObjectManifest, options?: SyncManifestOptions): Promise<FeatureSyncResult>;
|
|
11
|
+
private ensureInitialized;
|
|
12
|
+
private collectDefinitionsFromRegistrations;
|
|
13
|
+
private collectTouchedPackagesFromRegistrations;
|
|
14
|
+
private collectRegistrations;
|
|
15
|
+
private applyDefinitions;
|
|
16
|
+
}
|
|
17
|
+
//# sourceMappingURL=feature-sync.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"feature-sync.d.ts","sourceRoot":"","sources":["../src/feature-sync.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,gBAAgB,EACtB,MAAM,0BAA0B,CAAC;AAClC,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,mCAAmC,CAAC;AAE7E,OAAO,KAAK,EAEV,iBAAiB,EACjB,sBAAsB,EACtB,mBAAmB,EACpB,MAAM,YAAY,CAAC;AASpB,qBAAa,kBAAkB;IAC7B,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAmB;IAC3C,OAAO,CAAC,kBAAkB,CAA+B;IACzD,OAAO,CAAC,qBAAqB,CAA8B;gBAE/C,OAAO,GAAE,gBAAqB;IAIpC,eAAe,CACnB,OAAO,GAAE,sBAA2B,GACnC,OAAO,CAAC,iBAAiB,CAAC;IAcvB,YAAY,CAChB,QAAQ,EAAE,mBAAmB,EAC7B,OAAO,GAAE,mBAAwB,GAChC,OAAO,CAAC,iBAAiB,CAAC;YAWf,iBAAiB;IAY/B,OAAO,CAAC,mCAAmC;IAY3C,OAAO,CAAC,uCAAuC;IAmB/C,OAAO,CAAC,oBAAoB;YA6Bd,gBAAgB;CA0D/B"}
|