@flaghoist/core 0.1.2 → 0.2.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.
- package/dist/index.cjs +76 -1
- package/dist/index.d.cts +131 -1
- package/dist/index.d.ts +131 -1
- package/dist/index.js +69 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -20,16 +20,23 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
20
20
|
// src/index.ts
|
|
21
21
|
var index_exports = {};
|
|
22
22
|
__export(index_exports, {
|
|
23
|
+
FLAG_AUDIT_ACTIONS: () => FLAG_AUDIT_ACTIONS,
|
|
23
24
|
FLAG_KEY_RULE: () => FLAG_KEY_RULE,
|
|
24
25
|
FORBIDDEN_ATTRIBUTES: () => FORBIDDEN_ATTRIBUTES,
|
|
25
26
|
LIMITS: () => LIMITS,
|
|
27
|
+
MEMBER_WEBHOOK_EVENTS: () => MEMBER_WEBHOOK_EVENTS,
|
|
28
|
+
WEBHOOK_EVENTS: () => WEBHOOK_EVENTS,
|
|
29
|
+
assertRecordAddress: () => assertRecordAddress,
|
|
30
|
+
auditCategory: () => auditCategory,
|
|
26
31
|
clampPercentage: () => clampPercentage,
|
|
27
32
|
compareSemver: () => compareSemver,
|
|
28
33
|
createFlag: () => createFlag,
|
|
29
34
|
evaluate: () => evaluate,
|
|
30
35
|
evaluateAll: () => evaluateAll,
|
|
31
36
|
isInRollout: () => isInRollout,
|
|
37
|
+
isValidCollectionName: () => isValidCollectionName,
|
|
32
38
|
isValidFlagKey: () => isValidFlagKey,
|
|
39
|
+
isValidRecordId: () => isValidRecordId,
|
|
33
40
|
matchCondition: () => matchCondition,
|
|
34
41
|
matchesAllConditions: () => matchesAllConditions,
|
|
35
42
|
parseFlag: () => parseFlag,
|
|
@@ -37,6 +44,33 @@ __export(index_exports, {
|
|
|
37
44
|
});
|
|
38
45
|
module.exports = __toCommonJS(index_exports);
|
|
39
46
|
|
|
47
|
+
// src/types.ts
|
|
48
|
+
var FLAG_AUDIT_ACTIONS = [
|
|
49
|
+
"create",
|
|
50
|
+
"update",
|
|
51
|
+
"delete",
|
|
52
|
+
"archive",
|
|
53
|
+
"restore"
|
|
54
|
+
];
|
|
55
|
+
function auditCategory(action) {
|
|
56
|
+
return FLAG_AUDIT_ACTIONS.includes(action) ? "flags" : "security";
|
|
57
|
+
}
|
|
58
|
+
var WEBHOOK_EVENTS = [
|
|
59
|
+
"flag.created",
|
|
60
|
+
"flag.updated",
|
|
61
|
+
"flag.deleted",
|
|
62
|
+
"flag.archived",
|
|
63
|
+
"flag.restored"
|
|
64
|
+
];
|
|
65
|
+
var MEMBER_WEBHOOK_EVENTS = [
|
|
66
|
+
"member.invited",
|
|
67
|
+
"member.joined",
|
|
68
|
+
"member.role_changed",
|
|
69
|
+
"member.disabled",
|
|
70
|
+
"member.enabled",
|
|
71
|
+
"member.removed"
|
|
72
|
+
];
|
|
73
|
+
|
|
40
74
|
// src/hash.ts
|
|
41
75
|
function clampPercentage(percentage) {
|
|
42
76
|
if (Number.isNaN(percentage)) return 0;
|
|
@@ -74,6 +108,31 @@ function isValidFlagKey(key) {
|
|
|
74
108
|
return key.length >= 1 && key.length <= LIMITS.maxKeyLength && FLAG_KEY_PATTERN.test(key);
|
|
75
109
|
}
|
|
76
110
|
var FLAG_KEY_RULE = "must be 1-256 characters of [A-Za-z0-9._-] and start with an alphanumeric";
|
|
111
|
+
var COLLECTION_NAME_PATTERN = /^[a-z][a-z0-9-]{0,63}$/;
|
|
112
|
+
function isValidCollectionName(name) {
|
|
113
|
+
return COLLECTION_NAME_PATTERN.test(name);
|
|
114
|
+
}
|
|
115
|
+
var MAX_RECORD_ID_BYTES = 256;
|
|
116
|
+
function isValidRecordId(id) {
|
|
117
|
+
if (id.length < 1 || new TextEncoder().encode(id).length > MAX_RECORD_ID_BYTES) return false;
|
|
118
|
+
for (let i = 0; i < id.length; i++) {
|
|
119
|
+
const code = id.charCodeAt(i);
|
|
120
|
+
if (code < 32 || code === 127) return false;
|
|
121
|
+
}
|
|
122
|
+
return true;
|
|
123
|
+
}
|
|
124
|
+
function assertRecordAddress(collection, id) {
|
|
125
|
+
if (!isValidCollectionName(collection)) {
|
|
126
|
+
throw new Error(
|
|
127
|
+
`Invalid record collection ${JSON.stringify(collection)}: must match ${COLLECTION_NAME_PATTERN.source}.`
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
if (id !== void 0 && !isValidRecordId(id)) {
|
|
131
|
+
throw new Error(
|
|
132
|
+
`Invalid record id: must be 1-${MAX_RECORD_ID_BYTES} bytes with no control characters.`
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
77
136
|
|
|
78
137
|
// src/operators.ts
|
|
79
138
|
function isScalar(v) {
|
|
@@ -151,6 +210,7 @@ async function resolveRule(result, targetingKey, flagKey, ruleIndex) {
|
|
|
151
210
|
return { value, reason: isSplit(percentage) ? "SPLIT" : "TARGETING_MATCH", ruleIndex };
|
|
152
211
|
}
|
|
153
212
|
async function evaluate(flag, context = {}) {
|
|
213
|
+
if (flag.archived) return { value: false, reason: "DISABLED" };
|
|
154
214
|
if (!flag.enabled) return { value: false, reason: "DISABLED" };
|
|
155
215
|
const targetingKey = context.targetingKey ?? "";
|
|
156
216
|
let ruleIndex = 0;
|
|
@@ -256,7 +316,7 @@ function parseFlag(input) {
|
|
|
256
316
|
const rawRules = Array.isArray(o.rules) ? o.rules : [];
|
|
257
317
|
if (rawRules.length > LIMITS.maxRules) return null;
|
|
258
318
|
const rules = rawRules.map(parseRule).filter((r) => r !== null);
|
|
259
|
-
|
|
319
|
+
const flag = {
|
|
260
320
|
key: o.key,
|
|
261
321
|
enabled: o.enabled,
|
|
262
322
|
rollout: { percentage },
|
|
@@ -264,6 +324,14 @@ function parseFlag(input) {
|
|
|
264
324
|
description: typeof o.description === "string" ? o.description : "",
|
|
265
325
|
metadata: parseMetadata(o.metadata)
|
|
266
326
|
};
|
|
327
|
+
if (o.archived === true) {
|
|
328
|
+
flag.archived = true;
|
|
329
|
+
if (typeof o.archivedAt === "string") flag.archivedAt = o.archivedAt;
|
|
330
|
+
}
|
|
331
|
+
if (typeof o.environment === "string" && o.environment.length > 0) {
|
|
332
|
+
flag.environment = o.environment;
|
|
333
|
+
}
|
|
334
|
+
return flag;
|
|
267
335
|
}
|
|
268
336
|
function createFlag(input) {
|
|
269
337
|
if (!isValidFlagKey(input.key)) {
|
|
@@ -282,16 +350,23 @@ function createFlag(input) {
|
|
|
282
350
|
}
|
|
283
351
|
// Annotate the CommonJS export names for ESM import in node:
|
|
284
352
|
0 && (module.exports = {
|
|
353
|
+
FLAG_AUDIT_ACTIONS,
|
|
285
354
|
FLAG_KEY_RULE,
|
|
286
355
|
FORBIDDEN_ATTRIBUTES,
|
|
287
356
|
LIMITS,
|
|
357
|
+
MEMBER_WEBHOOK_EVENTS,
|
|
358
|
+
WEBHOOK_EVENTS,
|
|
359
|
+
assertRecordAddress,
|
|
360
|
+
auditCategory,
|
|
288
361
|
clampPercentage,
|
|
289
362
|
compareSemver,
|
|
290
363
|
createFlag,
|
|
291
364
|
evaluate,
|
|
292
365
|
evaluateAll,
|
|
293
366
|
isInRollout,
|
|
367
|
+
isValidCollectionName,
|
|
294
368
|
isValidFlagKey,
|
|
369
|
+
isValidRecordId,
|
|
295
370
|
matchCondition,
|
|
296
371
|
matchesAllConditions,
|
|
297
372
|
parseFlag,
|
package/dist/index.d.cts
CHANGED
|
@@ -50,6 +50,14 @@ interface FeatureFlag {
|
|
|
50
50
|
rules?: TargetingRule[];
|
|
51
51
|
description: string;
|
|
52
52
|
metadata: FlagMetadata;
|
|
53
|
+
archived?: boolean;
|
|
54
|
+
archivedAt?: string;
|
|
55
|
+
/**
|
|
56
|
+
* The environment this flag belongs to, when the server has environments configured. Absent
|
|
57
|
+
* means the default environment ("production"), so existing bare-keyed flags need no migration
|
|
58
|
+
* when environments are turned on for the first time.
|
|
59
|
+
*/
|
|
60
|
+
environment?: string;
|
|
53
61
|
}
|
|
54
62
|
/**
|
|
55
63
|
* The OpenFeature-style evaluation context: an optional stable `targetingKey` used for
|
|
@@ -76,6 +84,114 @@ interface StorageAdapter {
|
|
|
76
84
|
put(key: string, flag: FeatureFlag): Promise<void>;
|
|
77
85
|
delete(key: string): Promise<void>;
|
|
78
86
|
list(): Promise<FeatureFlag[]>;
|
|
87
|
+
/** Append an audit entry to persistent storage. Optional; the server falls back to in-memory. */
|
|
88
|
+
appendAudit?(entry: AuditEntry): Promise<void>;
|
|
89
|
+
/** List audit entries with optional filtering and pagination. */
|
|
90
|
+
listAudit?(options?: AuditListOptions): Promise<AuditPage>;
|
|
91
|
+
/** Store a webhook endpoint. Optional; the server falls back to in-memory. */
|
|
92
|
+
putWebhook?(id: string, webhook: WebhookEndpoint): Promise<void>;
|
|
93
|
+
/** Retrieve a webhook by id. */
|
|
94
|
+
getWebhook?(id: string): Promise<WebhookEndpoint | null>;
|
|
95
|
+
/** Remove a webhook. */
|
|
96
|
+
deleteWebhook?(id: string): Promise<void>;
|
|
97
|
+
/** List all webhook endpoints. */
|
|
98
|
+
listWebhooks?(): Promise<WebhookEndpoint[]>;
|
|
99
|
+
/**
|
|
100
|
+
* Generic record store: JSON values grouped into named collections, addressed by id. Optional.
|
|
101
|
+
* Features that need persistent server-side state beyond flags build on this rather than adding
|
|
102
|
+
* a new method group to every adapter. Collection names must pass `isValidCollectionName` and
|
|
103
|
+
* ids `isValidRecordId`; values must be JSON-serializable.
|
|
104
|
+
*/
|
|
105
|
+
getRecord?(collection: string, id: string): Promise<unknown | null>;
|
|
106
|
+
/** Create or replace a record. */
|
|
107
|
+
putRecord?(collection: string, id: string, value: unknown): Promise<void>;
|
|
108
|
+
/** Remove a record. Removing a missing record is a no-op. */
|
|
109
|
+
deleteRecord?(collection: string, id: string): Promise<void>;
|
|
110
|
+
/** List every record in one collection, in no particular order. */
|
|
111
|
+
listRecords?(collection: string): Promise<RecordEntry[]>;
|
|
112
|
+
}
|
|
113
|
+
/** One record returned by `StorageAdapter.listRecords`. */
|
|
114
|
+
interface RecordEntry {
|
|
115
|
+
id: string;
|
|
116
|
+
value: unknown;
|
|
117
|
+
}
|
|
118
|
+
/** A point-in-time snapshot of a flag's evaluable state, recorded in audit entries. */
|
|
119
|
+
interface FlagSnapshot {
|
|
120
|
+
enabled: boolean;
|
|
121
|
+
rollout: {
|
|
122
|
+
percentage: number;
|
|
123
|
+
};
|
|
124
|
+
description: string;
|
|
125
|
+
}
|
|
126
|
+
/** Actions recorded against a flag. */
|
|
127
|
+
type FlagAuditAction = 'create' | 'update' | 'delete' | 'archive' | 'restore';
|
|
128
|
+
/**
|
|
129
|
+
* Actions recorded against accounts, sessions and webhooks. These never carry a `flagKey`; they
|
|
130
|
+
* name what they touched in `target` instead.
|
|
131
|
+
*/
|
|
132
|
+
type SecurityAuditAction = 'login' | 'login.failed' | 'logout' | 'password.changed' | 'session.revoked' | 'password.reset' | 'user.created' | 'user.updated' | 'user.removed' | 'invite.created' | 'invite.accepted' | 'invite.revoked' | 'token.created' | 'token.revoked' | 'token.expired' | 'two_factor.enabled' | 'two_factor.disabled' | 'two_factor.reset' | 'two_factor.recovery_used' | 'webhook.created' | 'webhook.updated' | 'webhook.deleted';
|
|
133
|
+
type AuditAction = FlagAuditAction | SecurityAuditAction;
|
|
134
|
+
declare const FLAG_AUDIT_ACTIONS: readonly FlagAuditAction[];
|
|
135
|
+
/** Which log an entry belongs to: flag changes, or sign-ins and configuration changes. */
|
|
136
|
+
type AuditCategory = 'flags' | 'security';
|
|
137
|
+
declare function auditCategory(action: AuditAction): AuditCategory;
|
|
138
|
+
/** What a security event touched. */
|
|
139
|
+
interface AuditTarget {
|
|
140
|
+
type: 'user' | 'session' | 'invite' | 'token' | 'webhook';
|
|
141
|
+
id: string;
|
|
142
|
+
}
|
|
143
|
+
interface AuditEntry {
|
|
144
|
+
id: string;
|
|
145
|
+
timestamp: string;
|
|
146
|
+
action: AuditAction;
|
|
147
|
+
/** The flag that changed. Always set on flag events; absent on security events. */
|
|
148
|
+
flagKey?: string;
|
|
149
|
+
/** What a security event touched. Absent on flag events. */
|
|
150
|
+
target?: AuditTarget;
|
|
151
|
+
actor: string;
|
|
152
|
+
previous?: FlagSnapshot;
|
|
153
|
+
current?: FlagSnapshot;
|
|
154
|
+
/** A note on the change: the author's description on flag events, context on security events. */
|
|
155
|
+
changeDescription?: string;
|
|
156
|
+
/** The environment the change happened in. Absent means the default environment. */
|
|
157
|
+
environment?: string;
|
|
158
|
+
}
|
|
159
|
+
interface AuditListOptions {
|
|
160
|
+
limit?: number;
|
|
161
|
+
offset?: number;
|
|
162
|
+
/**
|
|
163
|
+
* Which log to read. Adapters that persist audit entries should honour this; `auditCategory()`
|
|
164
|
+
* maps an action onto its category. Absent means both.
|
|
165
|
+
*/
|
|
166
|
+
category?: AuditCategory;
|
|
167
|
+
flagKey?: string;
|
|
168
|
+
action?: AuditAction;
|
|
169
|
+
/** Restrict to entries recorded in this environment. Absent means the default environment. */
|
|
170
|
+
environment?: string;
|
|
171
|
+
}
|
|
172
|
+
interface AuditPage {
|
|
173
|
+
entries: AuditEntry[];
|
|
174
|
+
total: number;
|
|
175
|
+
}
|
|
176
|
+
type FlagWebhookEvent = 'flag.created' | 'flag.updated' | 'flag.deleted' | 'flag.archived' | 'flag.restored';
|
|
177
|
+
/**
|
|
178
|
+
* Team changes, for servers with user accounts. Opt-in: a webhook gets them only when it lists
|
|
179
|
+
* them, never through the default event list, so a receiver built for flag events never sees a
|
|
180
|
+
* payload of another shape.
|
|
181
|
+
*/
|
|
182
|
+
type MemberWebhookEvent = 'member.invited' | 'member.joined' | 'member.role_changed' | 'member.disabled' | 'member.enabled' | 'member.removed';
|
|
183
|
+
type WebhookEvent = FlagWebhookEvent | MemberWebhookEvent;
|
|
184
|
+
/** The flag events: what a webhook subscribes to when it names no events. */
|
|
185
|
+
declare const WEBHOOK_EVENTS: FlagWebhookEvent[];
|
|
186
|
+
declare const MEMBER_WEBHOOK_EVENTS: MemberWebhookEvent[];
|
|
187
|
+
interface WebhookEndpoint {
|
|
188
|
+
id: string;
|
|
189
|
+
url: string;
|
|
190
|
+
secret: string;
|
|
191
|
+
events: WebhookEvent[];
|
|
192
|
+
enabled: boolean;
|
|
193
|
+
createdAt: string;
|
|
194
|
+
updatedAt: string;
|
|
79
195
|
}
|
|
80
196
|
/** The authenticated caller, extracted from a validated admin token. */
|
|
81
197
|
interface AuthContext {
|
|
@@ -134,6 +250,20 @@ declare const FORBIDDEN_ATTRIBUTES: ReadonlySet<string>;
|
|
|
134
250
|
declare function isValidFlagKey(key: string): boolean;
|
|
135
251
|
/** Human-readable description of the flag key rule, for error messages. */
|
|
136
252
|
declare const FLAG_KEY_RULE = "must be 1-256 characters of [A-Za-z0-9._-] and start with an alphanumeric";
|
|
253
|
+
/**
|
|
254
|
+
* A record-store collection name. Collections are chosen by server code, never by callers, but
|
|
255
|
+
* adapters embed them in storage keys and table rows, so the charset stays narrow: lowercase
|
|
256
|
+
* alphanumerics and hyphens, no separators that could collide with an adapter's key layout.
|
|
257
|
+
*/
|
|
258
|
+
declare function isValidCollectionName(name: string): boolean;
|
|
259
|
+
/**
|
|
260
|
+
* A record id. Ids can carry user-supplied text (an email address used as a lookup key, say), so
|
|
261
|
+
* they allow any printable characters but must be non-empty, bounded, and free of control
|
|
262
|
+
* characters.
|
|
263
|
+
*/
|
|
264
|
+
declare function isValidRecordId(id: string): boolean;
|
|
265
|
+
/** Throw if a collection name or record id is not safe to hand to a storage adapter. */
|
|
266
|
+
declare function assertRecordAddress(collection: string, id?: string): void;
|
|
137
267
|
|
|
138
268
|
/**
|
|
139
269
|
* Compare two semver strings. Returns -1, 0, or 1. A version with no prerelease outranks
|
|
@@ -186,4 +316,4 @@ interface CreateFlagInput {
|
|
|
186
316
|
*/
|
|
187
317
|
declare function createFlag(input: CreateFlagInput): FeatureFlag;
|
|
188
318
|
|
|
189
|
-
export { type AttributeValue, type AuthContext, type AuthVerifier, type Condition, type ConditionValue, type CreateFlagInput, type EvaluationContext, type EvaluationReason, type EvaluationResult, FLAG_KEY_RULE, FORBIDDEN_ATTRIBUTES, type FeatureFlag, type FlagMetadata, LIMITS, type Operator, type RuleResult, type StorageAdapter, type TargetingRule, clampPercentage, compareSemver, createFlag, evaluate, evaluateAll, isInRollout, isValidFlagKey, matchCondition, matchesAllConditions, parseFlag, stickyBucket };
|
|
319
|
+
export { type AttributeValue, type AuditAction, type AuditCategory, type AuditEntry, type AuditListOptions, type AuditPage, type AuditTarget, type AuthContext, type AuthVerifier, type Condition, type ConditionValue, type CreateFlagInput, type EvaluationContext, type EvaluationReason, type EvaluationResult, FLAG_AUDIT_ACTIONS, FLAG_KEY_RULE, FORBIDDEN_ATTRIBUTES, type FeatureFlag, type FlagAuditAction, type FlagMetadata, type FlagSnapshot, type FlagWebhookEvent, LIMITS, MEMBER_WEBHOOK_EVENTS, type MemberWebhookEvent, type Operator, type RecordEntry, type RuleResult, type SecurityAuditAction, type StorageAdapter, type TargetingRule, WEBHOOK_EVENTS, type WebhookEndpoint, type WebhookEvent, assertRecordAddress, auditCategory, clampPercentage, compareSemver, createFlag, evaluate, evaluateAll, isInRollout, isValidCollectionName, isValidFlagKey, isValidRecordId, matchCondition, matchesAllConditions, parseFlag, stickyBucket };
|
package/dist/index.d.ts
CHANGED
|
@@ -50,6 +50,14 @@ interface FeatureFlag {
|
|
|
50
50
|
rules?: TargetingRule[];
|
|
51
51
|
description: string;
|
|
52
52
|
metadata: FlagMetadata;
|
|
53
|
+
archived?: boolean;
|
|
54
|
+
archivedAt?: string;
|
|
55
|
+
/**
|
|
56
|
+
* The environment this flag belongs to, when the server has environments configured. Absent
|
|
57
|
+
* means the default environment ("production"), so existing bare-keyed flags need no migration
|
|
58
|
+
* when environments are turned on for the first time.
|
|
59
|
+
*/
|
|
60
|
+
environment?: string;
|
|
53
61
|
}
|
|
54
62
|
/**
|
|
55
63
|
* The OpenFeature-style evaluation context: an optional stable `targetingKey` used for
|
|
@@ -76,6 +84,114 @@ interface StorageAdapter {
|
|
|
76
84
|
put(key: string, flag: FeatureFlag): Promise<void>;
|
|
77
85
|
delete(key: string): Promise<void>;
|
|
78
86
|
list(): Promise<FeatureFlag[]>;
|
|
87
|
+
/** Append an audit entry to persistent storage. Optional; the server falls back to in-memory. */
|
|
88
|
+
appendAudit?(entry: AuditEntry): Promise<void>;
|
|
89
|
+
/** List audit entries with optional filtering and pagination. */
|
|
90
|
+
listAudit?(options?: AuditListOptions): Promise<AuditPage>;
|
|
91
|
+
/** Store a webhook endpoint. Optional; the server falls back to in-memory. */
|
|
92
|
+
putWebhook?(id: string, webhook: WebhookEndpoint): Promise<void>;
|
|
93
|
+
/** Retrieve a webhook by id. */
|
|
94
|
+
getWebhook?(id: string): Promise<WebhookEndpoint | null>;
|
|
95
|
+
/** Remove a webhook. */
|
|
96
|
+
deleteWebhook?(id: string): Promise<void>;
|
|
97
|
+
/** List all webhook endpoints. */
|
|
98
|
+
listWebhooks?(): Promise<WebhookEndpoint[]>;
|
|
99
|
+
/**
|
|
100
|
+
* Generic record store: JSON values grouped into named collections, addressed by id. Optional.
|
|
101
|
+
* Features that need persistent server-side state beyond flags build on this rather than adding
|
|
102
|
+
* a new method group to every adapter. Collection names must pass `isValidCollectionName` and
|
|
103
|
+
* ids `isValidRecordId`; values must be JSON-serializable.
|
|
104
|
+
*/
|
|
105
|
+
getRecord?(collection: string, id: string): Promise<unknown | null>;
|
|
106
|
+
/** Create or replace a record. */
|
|
107
|
+
putRecord?(collection: string, id: string, value: unknown): Promise<void>;
|
|
108
|
+
/** Remove a record. Removing a missing record is a no-op. */
|
|
109
|
+
deleteRecord?(collection: string, id: string): Promise<void>;
|
|
110
|
+
/** List every record in one collection, in no particular order. */
|
|
111
|
+
listRecords?(collection: string): Promise<RecordEntry[]>;
|
|
112
|
+
}
|
|
113
|
+
/** One record returned by `StorageAdapter.listRecords`. */
|
|
114
|
+
interface RecordEntry {
|
|
115
|
+
id: string;
|
|
116
|
+
value: unknown;
|
|
117
|
+
}
|
|
118
|
+
/** A point-in-time snapshot of a flag's evaluable state, recorded in audit entries. */
|
|
119
|
+
interface FlagSnapshot {
|
|
120
|
+
enabled: boolean;
|
|
121
|
+
rollout: {
|
|
122
|
+
percentage: number;
|
|
123
|
+
};
|
|
124
|
+
description: string;
|
|
125
|
+
}
|
|
126
|
+
/** Actions recorded against a flag. */
|
|
127
|
+
type FlagAuditAction = 'create' | 'update' | 'delete' | 'archive' | 'restore';
|
|
128
|
+
/**
|
|
129
|
+
* Actions recorded against accounts, sessions and webhooks. These never carry a `flagKey`; they
|
|
130
|
+
* name what they touched in `target` instead.
|
|
131
|
+
*/
|
|
132
|
+
type SecurityAuditAction = 'login' | 'login.failed' | 'logout' | 'password.changed' | 'session.revoked' | 'password.reset' | 'user.created' | 'user.updated' | 'user.removed' | 'invite.created' | 'invite.accepted' | 'invite.revoked' | 'token.created' | 'token.revoked' | 'token.expired' | 'two_factor.enabled' | 'two_factor.disabled' | 'two_factor.reset' | 'two_factor.recovery_used' | 'webhook.created' | 'webhook.updated' | 'webhook.deleted';
|
|
133
|
+
type AuditAction = FlagAuditAction | SecurityAuditAction;
|
|
134
|
+
declare const FLAG_AUDIT_ACTIONS: readonly FlagAuditAction[];
|
|
135
|
+
/** Which log an entry belongs to: flag changes, or sign-ins and configuration changes. */
|
|
136
|
+
type AuditCategory = 'flags' | 'security';
|
|
137
|
+
declare function auditCategory(action: AuditAction): AuditCategory;
|
|
138
|
+
/** What a security event touched. */
|
|
139
|
+
interface AuditTarget {
|
|
140
|
+
type: 'user' | 'session' | 'invite' | 'token' | 'webhook';
|
|
141
|
+
id: string;
|
|
142
|
+
}
|
|
143
|
+
interface AuditEntry {
|
|
144
|
+
id: string;
|
|
145
|
+
timestamp: string;
|
|
146
|
+
action: AuditAction;
|
|
147
|
+
/** The flag that changed. Always set on flag events; absent on security events. */
|
|
148
|
+
flagKey?: string;
|
|
149
|
+
/** What a security event touched. Absent on flag events. */
|
|
150
|
+
target?: AuditTarget;
|
|
151
|
+
actor: string;
|
|
152
|
+
previous?: FlagSnapshot;
|
|
153
|
+
current?: FlagSnapshot;
|
|
154
|
+
/** A note on the change: the author's description on flag events, context on security events. */
|
|
155
|
+
changeDescription?: string;
|
|
156
|
+
/** The environment the change happened in. Absent means the default environment. */
|
|
157
|
+
environment?: string;
|
|
158
|
+
}
|
|
159
|
+
interface AuditListOptions {
|
|
160
|
+
limit?: number;
|
|
161
|
+
offset?: number;
|
|
162
|
+
/**
|
|
163
|
+
* Which log to read. Adapters that persist audit entries should honour this; `auditCategory()`
|
|
164
|
+
* maps an action onto its category. Absent means both.
|
|
165
|
+
*/
|
|
166
|
+
category?: AuditCategory;
|
|
167
|
+
flagKey?: string;
|
|
168
|
+
action?: AuditAction;
|
|
169
|
+
/** Restrict to entries recorded in this environment. Absent means the default environment. */
|
|
170
|
+
environment?: string;
|
|
171
|
+
}
|
|
172
|
+
interface AuditPage {
|
|
173
|
+
entries: AuditEntry[];
|
|
174
|
+
total: number;
|
|
175
|
+
}
|
|
176
|
+
type FlagWebhookEvent = 'flag.created' | 'flag.updated' | 'flag.deleted' | 'flag.archived' | 'flag.restored';
|
|
177
|
+
/**
|
|
178
|
+
* Team changes, for servers with user accounts. Opt-in: a webhook gets them only when it lists
|
|
179
|
+
* them, never through the default event list, so a receiver built for flag events never sees a
|
|
180
|
+
* payload of another shape.
|
|
181
|
+
*/
|
|
182
|
+
type MemberWebhookEvent = 'member.invited' | 'member.joined' | 'member.role_changed' | 'member.disabled' | 'member.enabled' | 'member.removed';
|
|
183
|
+
type WebhookEvent = FlagWebhookEvent | MemberWebhookEvent;
|
|
184
|
+
/** The flag events: what a webhook subscribes to when it names no events. */
|
|
185
|
+
declare const WEBHOOK_EVENTS: FlagWebhookEvent[];
|
|
186
|
+
declare const MEMBER_WEBHOOK_EVENTS: MemberWebhookEvent[];
|
|
187
|
+
interface WebhookEndpoint {
|
|
188
|
+
id: string;
|
|
189
|
+
url: string;
|
|
190
|
+
secret: string;
|
|
191
|
+
events: WebhookEvent[];
|
|
192
|
+
enabled: boolean;
|
|
193
|
+
createdAt: string;
|
|
194
|
+
updatedAt: string;
|
|
79
195
|
}
|
|
80
196
|
/** The authenticated caller, extracted from a validated admin token. */
|
|
81
197
|
interface AuthContext {
|
|
@@ -134,6 +250,20 @@ declare const FORBIDDEN_ATTRIBUTES: ReadonlySet<string>;
|
|
|
134
250
|
declare function isValidFlagKey(key: string): boolean;
|
|
135
251
|
/** Human-readable description of the flag key rule, for error messages. */
|
|
136
252
|
declare const FLAG_KEY_RULE = "must be 1-256 characters of [A-Za-z0-9._-] and start with an alphanumeric";
|
|
253
|
+
/**
|
|
254
|
+
* A record-store collection name. Collections are chosen by server code, never by callers, but
|
|
255
|
+
* adapters embed them in storage keys and table rows, so the charset stays narrow: lowercase
|
|
256
|
+
* alphanumerics and hyphens, no separators that could collide with an adapter's key layout.
|
|
257
|
+
*/
|
|
258
|
+
declare function isValidCollectionName(name: string): boolean;
|
|
259
|
+
/**
|
|
260
|
+
* A record id. Ids can carry user-supplied text (an email address used as a lookup key, say), so
|
|
261
|
+
* they allow any printable characters but must be non-empty, bounded, and free of control
|
|
262
|
+
* characters.
|
|
263
|
+
*/
|
|
264
|
+
declare function isValidRecordId(id: string): boolean;
|
|
265
|
+
/** Throw if a collection name or record id is not safe to hand to a storage adapter. */
|
|
266
|
+
declare function assertRecordAddress(collection: string, id?: string): void;
|
|
137
267
|
|
|
138
268
|
/**
|
|
139
269
|
* Compare two semver strings. Returns -1, 0, or 1. A version with no prerelease outranks
|
|
@@ -186,4 +316,4 @@ interface CreateFlagInput {
|
|
|
186
316
|
*/
|
|
187
317
|
declare function createFlag(input: CreateFlagInput): FeatureFlag;
|
|
188
318
|
|
|
189
|
-
export { type AttributeValue, type AuthContext, type AuthVerifier, type Condition, type ConditionValue, type CreateFlagInput, type EvaluationContext, type EvaluationReason, type EvaluationResult, FLAG_KEY_RULE, FORBIDDEN_ATTRIBUTES, type FeatureFlag, type FlagMetadata, LIMITS, type Operator, type RuleResult, type StorageAdapter, type TargetingRule, clampPercentage, compareSemver, createFlag, evaluate, evaluateAll, isInRollout, isValidFlagKey, matchCondition, matchesAllConditions, parseFlag, stickyBucket };
|
|
319
|
+
export { type AttributeValue, type AuditAction, type AuditCategory, type AuditEntry, type AuditListOptions, type AuditPage, type AuditTarget, type AuthContext, type AuthVerifier, type Condition, type ConditionValue, type CreateFlagInput, type EvaluationContext, type EvaluationReason, type EvaluationResult, FLAG_AUDIT_ACTIONS, FLAG_KEY_RULE, FORBIDDEN_ATTRIBUTES, type FeatureFlag, type FlagAuditAction, type FlagMetadata, type FlagSnapshot, type FlagWebhookEvent, LIMITS, MEMBER_WEBHOOK_EVENTS, type MemberWebhookEvent, type Operator, type RecordEntry, type RuleResult, type SecurityAuditAction, type StorageAdapter, type TargetingRule, WEBHOOK_EVENTS, type WebhookEndpoint, type WebhookEvent, assertRecordAddress, auditCategory, clampPercentage, compareSemver, createFlag, evaluate, evaluateAll, isInRollout, isValidCollectionName, isValidFlagKey, isValidRecordId, matchCondition, matchesAllConditions, parseFlag, stickyBucket };
|
package/dist/index.js
CHANGED
|
@@ -1,3 +1,30 @@
|
|
|
1
|
+
// src/types.ts
|
|
2
|
+
var FLAG_AUDIT_ACTIONS = [
|
|
3
|
+
"create",
|
|
4
|
+
"update",
|
|
5
|
+
"delete",
|
|
6
|
+
"archive",
|
|
7
|
+
"restore"
|
|
8
|
+
];
|
|
9
|
+
function auditCategory(action) {
|
|
10
|
+
return FLAG_AUDIT_ACTIONS.includes(action) ? "flags" : "security";
|
|
11
|
+
}
|
|
12
|
+
var WEBHOOK_EVENTS = [
|
|
13
|
+
"flag.created",
|
|
14
|
+
"flag.updated",
|
|
15
|
+
"flag.deleted",
|
|
16
|
+
"flag.archived",
|
|
17
|
+
"flag.restored"
|
|
18
|
+
];
|
|
19
|
+
var MEMBER_WEBHOOK_EVENTS = [
|
|
20
|
+
"member.invited",
|
|
21
|
+
"member.joined",
|
|
22
|
+
"member.role_changed",
|
|
23
|
+
"member.disabled",
|
|
24
|
+
"member.enabled",
|
|
25
|
+
"member.removed"
|
|
26
|
+
];
|
|
27
|
+
|
|
1
28
|
// src/hash.ts
|
|
2
29
|
function clampPercentage(percentage) {
|
|
3
30
|
if (Number.isNaN(percentage)) return 0;
|
|
@@ -35,6 +62,31 @@ function isValidFlagKey(key) {
|
|
|
35
62
|
return key.length >= 1 && key.length <= LIMITS.maxKeyLength && FLAG_KEY_PATTERN.test(key);
|
|
36
63
|
}
|
|
37
64
|
var FLAG_KEY_RULE = "must be 1-256 characters of [A-Za-z0-9._-] and start with an alphanumeric";
|
|
65
|
+
var COLLECTION_NAME_PATTERN = /^[a-z][a-z0-9-]{0,63}$/;
|
|
66
|
+
function isValidCollectionName(name) {
|
|
67
|
+
return COLLECTION_NAME_PATTERN.test(name);
|
|
68
|
+
}
|
|
69
|
+
var MAX_RECORD_ID_BYTES = 256;
|
|
70
|
+
function isValidRecordId(id) {
|
|
71
|
+
if (id.length < 1 || new TextEncoder().encode(id).length > MAX_RECORD_ID_BYTES) return false;
|
|
72
|
+
for (let i = 0; i < id.length; i++) {
|
|
73
|
+
const code = id.charCodeAt(i);
|
|
74
|
+
if (code < 32 || code === 127) return false;
|
|
75
|
+
}
|
|
76
|
+
return true;
|
|
77
|
+
}
|
|
78
|
+
function assertRecordAddress(collection, id) {
|
|
79
|
+
if (!isValidCollectionName(collection)) {
|
|
80
|
+
throw new Error(
|
|
81
|
+
`Invalid record collection ${JSON.stringify(collection)}: must match ${COLLECTION_NAME_PATTERN.source}.`
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
if (id !== void 0 && !isValidRecordId(id)) {
|
|
85
|
+
throw new Error(
|
|
86
|
+
`Invalid record id: must be 1-${MAX_RECORD_ID_BYTES} bytes with no control characters.`
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
38
90
|
|
|
39
91
|
// src/operators.ts
|
|
40
92
|
function isScalar(v) {
|
|
@@ -112,6 +164,7 @@ async function resolveRule(result, targetingKey, flagKey, ruleIndex) {
|
|
|
112
164
|
return { value, reason: isSplit(percentage) ? "SPLIT" : "TARGETING_MATCH", ruleIndex };
|
|
113
165
|
}
|
|
114
166
|
async function evaluate(flag, context = {}) {
|
|
167
|
+
if (flag.archived) return { value: false, reason: "DISABLED" };
|
|
115
168
|
if (!flag.enabled) return { value: false, reason: "DISABLED" };
|
|
116
169
|
const targetingKey = context.targetingKey ?? "";
|
|
117
170
|
let ruleIndex = 0;
|
|
@@ -217,7 +270,7 @@ function parseFlag(input) {
|
|
|
217
270
|
const rawRules = Array.isArray(o.rules) ? o.rules : [];
|
|
218
271
|
if (rawRules.length > LIMITS.maxRules) return null;
|
|
219
272
|
const rules = rawRules.map(parseRule).filter((r) => r !== null);
|
|
220
|
-
|
|
273
|
+
const flag = {
|
|
221
274
|
key: o.key,
|
|
222
275
|
enabled: o.enabled,
|
|
223
276
|
rollout: { percentage },
|
|
@@ -225,6 +278,14 @@ function parseFlag(input) {
|
|
|
225
278
|
description: typeof o.description === "string" ? o.description : "",
|
|
226
279
|
metadata: parseMetadata(o.metadata)
|
|
227
280
|
};
|
|
281
|
+
if (o.archived === true) {
|
|
282
|
+
flag.archived = true;
|
|
283
|
+
if (typeof o.archivedAt === "string") flag.archivedAt = o.archivedAt;
|
|
284
|
+
}
|
|
285
|
+
if (typeof o.environment === "string" && o.environment.length > 0) {
|
|
286
|
+
flag.environment = o.environment;
|
|
287
|
+
}
|
|
288
|
+
return flag;
|
|
228
289
|
}
|
|
229
290
|
function createFlag(input) {
|
|
230
291
|
if (!isValidFlagKey(input.key)) {
|
|
@@ -242,16 +303,23 @@ function createFlag(input) {
|
|
|
242
303
|
};
|
|
243
304
|
}
|
|
244
305
|
export {
|
|
306
|
+
FLAG_AUDIT_ACTIONS,
|
|
245
307
|
FLAG_KEY_RULE,
|
|
246
308
|
FORBIDDEN_ATTRIBUTES,
|
|
247
309
|
LIMITS,
|
|
310
|
+
MEMBER_WEBHOOK_EVENTS,
|
|
311
|
+
WEBHOOK_EVENTS,
|
|
312
|
+
assertRecordAddress,
|
|
313
|
+
auditCategory,
|
|
248
314
|
clampPercentage,
|
|
249
315
|
compareSemver,
|
|
250
316
|
createFlag,
|
|
251
317
|
evaluate,
|
|
252
318
|
evaluateAll,
|
|
253
319
|
isInRollout,
|
|
320
|
+
isValidCollectionName,
|
|
254
321
|
isValidFlagKey,
|
|
322
|
+
isValidRecordId,
|
|
255
323
|
matchCondition,
|
|
256
324
|
matchesAllConditions,
|
|
257
325
|
parseFlag,
|