@mrepol742/launch-kit 0.0.0-stage → 0.1.3
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/LICENSE +21 -0
- package/README.md +287 -2
- package/dist/index.d.ts +170 -0
- package/dist/index.js +466 -0
- package/dist/index.js.map +1 -0
- package/package.json +66 -4
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Melvin Jones Repol
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,288 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Launch-Kit
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Control who gets access to what, and when.
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
npm install @mrepol742/launch-kit
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
```ts
|
|
10
|
+
import { createLaunch } from "@mrepol742/launch-kit";
|
|
11
|
+
|
|
12
|
+
const launch = createLaunch({
|
|
13
|
+
phases: {
|
|
14
|
+
beta: {
|
|
15
|
+
from: "2026-10-01T00:00:00Z",
|
|
16
|
+
until: "2026-11-01T00:00:00Z",
|
|
17
|
+
roles: ["tester"],
|
|
18
|
+
},
|
|
19
|
+
public: { from: "2026-11-01T00:00:00Z", allow: ["everyone"] },
|
|
20
|
+
},
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
const decision = await launch.canAccess({ id: "usr_123", roles: ["tester"] });
|
|
24
|
+
// { allowed: true, reason: "ALLOWED", message: "Access granted.", phase: "beta", ... }
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## What is Launch-Kit?
|
|
28
|
+
|
|
29
|
+
Launch-Kit is a small, framework-independent TypeScript evaluation engine for product launches,
|
|
30
|
+
private betas, early access, staged rollouts, and separately launched features. Your application
|
|
31
|
+
owns authentication, users, persistence, and invite storage; Launch-Kit evaluates your rules.
|
|
32
|
+
|
|
33
|
+
The core has no production dependencies, telemetry, network calls, database, or framework imports.
|
|
34
|
+
It runs anywhere modern server-side JavaScript runs, including Node.js, Next.js, Nuxt, Express,
|
|
35
|
+
Fastify, NestJS, and serverless functions.
|
|
36
|
+
|
|
37
|
+
## Installation
|
|
38
|
+
|
|
39
|
+
Launch-Kit is ESM-first and requires Node.js 20 or newer.
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
npm install @mrepol742/launch-kit
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Quick Start
|
|
46
|
+
|
|
47
|
+
```ts
|
|
48
|
+
import { createLaunch } from "@mrepol742/launch-kit";
|
|
49
|
+
|
|
50
|
+
const launch = createLaunch({
|
|
51
|
+
id: "acme-launch",
|
|
52
|
+
phases: {
|
|
53
|
+
internal: {
|
|
54
|
+
from: "2026-10-01",
|
|
55
|
+
until: "2026-10-10",
|
|
56
|
+
roles: ["admin", "employee"],
|
|
57
|
+
},
|
|
58
|
+
public: { from: "2026-10-10", allow: ["everyone"] },
|
|
59
|
+
},
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
console.log(launch.phase());
|
|
63
|
+
console.log(await launch.canAccess({ id: "usr_123", roles: ["employee"] }));
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Dates are normalized to UTC. Phase intervals are half-open: `from` is inclusive and `until` is
|
|
67
|
+
exclusive. Adjacent phases can therefore hand off at exactly the same instant.
|
|
68
|
+
|
|
69
|
+
## Launch Phases
|
|
70
|
+
|
|
71
|
+
Phases are ordered chronologically, regardless of object property order. Gaps are allowed;
|
|
72
|
+
overlaps, invalid dates, and an end before a start are rejected when the launch is created.
|
|
73
|
+
|
|
74
|
+
```ts
|
|
75
|
+
launch.phase(); // "beta" | "public" | null
|
|
76
|
+
launch.is("beta");
|
|
77
|
+
launch.hasStarted();
|
|
78
|
+
launch.hasEnded();
|
|
79
|
+
launch.previousPhase();
|
|
80
|
+
launch.nextPhase();
|
|
81
|
+
launch.timeline();
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Use an injected clock for deterministic tests:
|
|
85
|
+
|
|
86
|
+
```ts
|
|
87
|
+
const launch = createLaunch(config, {
|
|
88
|
+
now: () => new Date("2026-10-15T12:00:00Z"),
|
|
89
|
+
});
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
## User Access
|
|
93
|
+
|
|
94
|
+
Launch-Kit accepts your user object; it does not authenticate anyone. Direct phase criteria are
|
|
95
|
+
combined with AND semantics, while values inside an array use OR semantics.
|
|
96
|
+
|
|
97
|
+
```ts
|
|
98
|
+
const decision = await launch.canAccess(
|
|
99
|
+
{
|
|
100
|
+
id: "usr_123",
|
|
101
|
+
email: "person@example.com",
|
|
102
|
+
roles: ["tester"],
|
|
103
|
+
plan: "pro",
|
|
104
|
+
tags: ["beta-tester"],
|
|
105
|
+
metadata: { verified: true, country: "PH" },
|
|
106
|
+
},
|
|
107
|
+
{ context: { device: "mobile" } },
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
if (!decision.allowed) {
|
|
111
|
+
console.log(decision.reason, decision.message, decision.nextPhaseAt);
|
|
112
|
+
}
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
Supported direct criteria are `roles`, `plans`, `tags`, `emails`, `emailDomains`, and `metadata`.
|
|
116
|
+
Email matching is case-insensitive. `*@example.com` is accepted and normalized as a domain rule.
|
|
117
|
+
Metadata matching is intentionally simple: every configured top-level property must equal the
|
|
118
|
+
corresponding user metadata property.
|
|
119
|
+
|
|
120
|
+
Decisions contain stable machine-readable reason codes and separate human-readable messages.
|
|
121
|
+
Branch on `reason`, never on `message`.
|
|
122
|
+
|
|
123
|
+
## Cohorts
|
|
124
|
+
|
|
125
|
+
Cohorts are reusable named criteria. A user may match any cohort listed in `allow`.
|
|
126
|
+
|
|
127
|
+
```ts
|
|
128
|
+
const launch = createLaunch({
|
|
129
|
+
cohorts: {
|
|
130
|
+
staff: { roles: ["admin", "employee"] },
|
|
131
|
+
testers: { tags: ["beta-tester"] },
|
|
132
|
+
paid: { plans: ["pro", "business"] },
|
|
133
|
+
partners: { emailDomains: ["partner.example"] },
|
|
134
|
+
},
|
|
135
|
+
phases: {
|
|
136
|
+
beta: { allow: ["staff", "testers", "paid"] },
|
|
137
|
+
public: { from: "2026-11-01", allow: ["everyone"] },
|
|
138
|
+
},
|
|
139
|
+
});
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
Unknown cohort references fail immediately with a `LaunchConfigurationError`.
|
|
143
|
+
|
|
144
|
+
## Percentage Rollouts
|
|
145
|
+
|
|
146
|
+
Rollouts assign each eligible user to a stable bucket from 0 through 99 using the launch id, phase
|
|
147
|
+
name, and user id. Launch-Kit never uses `Math.random()`.
|
|
148
|
+
|
|
149
|
+
```ts
|
|
150
|
+
earlyAccess: { allow: ["paid"], rollout: 25 }
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
Buckets 0–24 are included. The public `bucket(value)` helper uses the same stable hash. Rollout
|
|
154
|
+
values must be between 0 and 100. A missing/empty user id is excluded unless a stable identifier is
|
|
155
|
+
provided with `{ identifier: "stable-session-id" }`.
|
|
156
|
+
|
|
157
|
+
Scheduled rollouts are also supported:
|
|
158
|
+
|
|
159
|
+
```ts
|
|
160
|
+
rollout: [
|
|
161
|
+
{ percentage: 10, at: "2026-11-01T09:00:00Z" },
|
|
162
|
+
{ percentage: 25, at: "2026-11-01T12:00:00Z" },
|
|
163
|
+
{ percentage: 100, at: "2026-11-01T18:00:00Z" },
|
|
164
|
+
];
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
Steps must be chronological and percentages cannot decrease.
|
|
168
|
+
|
|
169
|
+
## Invite Codes
|
|
170
|
+
|
|
171
|
+
Launch-Kit coordinates validation but does not store invite codes. Supply trusted application logic:
|
|
172
|
+
|
|
173
|
+
```ts
|
|
174
|
+
const launch = createLaunch({
|
|
175
|
+
invite: {
|
|
176
|
+
validate: async ({ code, user, phase }) =>
|
|
177
|
+
myDatabase.invites.exists({ code, userId: user.id, phase }),
|
|
178
|
+
},
|
|
179
|
+
phases: { privateBeta: { inviteRequired: true } },
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
await launch.canAccess(user, { inviteCode: "BETA-X8F2" });
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
Missing codes return `INVITE_REQUIRED`; rejected codes return `INVALID_INVITE`. Validator failures
|
|
186
|
+
throw `LaunchInviteValidationError` so infrastructure failures are not mistaken for denials.
|
|
187
|
+
|
|
188
|
+
## Custom Rules
|
|
189
|
+
|
|
190
|
+
Rules are developer-supplied functions and may be synchronous or asynchronous:
|
|
191
|
+
|
|
192
|
+
```ts
|
|
193
|
+
const launch = createLaunch({
|
|
194
|
+
rules: {
|
|
195
|
+
establishedAccount: ({ user, phase, now, context }) =>
|
|
196
|
+
Number(user.metadata?.accountAge ?? 0) >= 30,
|
|
197
|
+
},
|
|
198
|
+
phases: { beta: { rules: ["establishedAccount"] } },
|
|
199
|
+
});
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
Every listed rule must pass. A false result returns `CUSTOM_RULE_DENIED`; thrown errors are wrapped
|
|
203
|
+
in `LaunchRuleEvaluationError`. Launch-Kit never executes rules from JSON or remote configuration.
|
|
204
|
+
|
|
205
|
+
## Feature Launches
|
|
206
|
+
|
|
207
|
+
Features have independent timelines and share top-level cohorts, rules, and invite validation:
|
|
208
|
+
|
|
209
|
+
```ts
|
|
210
|
+
const launch = createLaunch({
|
|
211
|
+
features: {
|
|
212
|
+
dashboard: { phases: { beta: { rollout: 25 }, public: { from: "2026-12-01" } } },
|
|
213
|
+
assistant: { phases: { internal: { roles: ["employee"] } } },
|
|
214
|
+
},
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
launch.feature("dashboard").phase();
|
|
218
|
+
await launch.feature("assistant").canAccess(user);
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
Literal feature and phase names are preserved in TypeScript, so editor autocomplete works and
|
|
222
|
+
unknown feature names are type errors. A dynamic unknown name throws `LaunchFeatureNotFoundError`.
|
|
223
|
+
|
|
224
|
+
## Server-side Security
|
|
225
|
+
|
|
226
|
+
**Access checks protecting sensitive functionality must execute server-side.** A client-side check
|
|
227
|
+
may hide a beta button, but it is not a security boundary. Re-check access in the API route, server
|
|
228
|
+
action, function, or service that serves the protected operation or data.
|
|
229
|
+
|
|
230
|
+
Treat invite codes, user fields, request context, and all other client-provided values as untrusted.
|
|
231
|
+
Keep secrets out of Launch-Kit configuration. Validate invites through trusted server-side logic.
|
|
232
|
+
|
|
233
|
+
## TypeScript
|
|
234
|
+
|
|
235
|
+
The package includes declarations and is written in strict TypeScript. Extend the user and context
|
|
236
|
+
types when custom rules need application-specific data:
|
|
237
|
+
|
|
238
|
+
```ts
|
|
239
|
+
interface AppUser extends LaunchUser<{ accountAge: number }> {
|
|
240
|
+
organizationId: string;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const launch = createLaunch<AppUser, { country: string }>({
|
|
244
|
+
rules: {
|
|
245
|
+
eligible: ({ user, context }) =>
|
|
246
|
+
user.metadata.accountAge >= 30 && context?.country === "PH",
|
|
247
|
+
},
|
|
248
|
+
phases: { beta: { rules: ["eligible"] } },
|
|
249
|
+
});
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
## API Reference
|
|
253
|
+
|
|
254
|
+
- `createLaunch(config, options?)` validates configuration and creates a launch controller.
|
|
255
|
+
- `phase()`, `previousPhase()`, `nextPhase()` return names or `null`.
|
|
256
|
+
- `is(name)`, `hasStarted()`, `hasEnded()` inspect the current timeline.
|
|
257
|
+
- `timeline()` returns normalized phases with `completed`, `active`, or `upcoming` status.
|
|
258
|
+
- `canAccess(user, options?)` returns a `Promise<AccessDecision>`.
|
|
259
|
+
- `feature(name)` returns a controller for a configured feature.
|
|
260
|
+
- `bucket(value)` and `stableHash(value)` expose deterministic bucketing primitives.
|
|
261
|
+
- `LaunchConfigurationError`, `LaunchFeatureNotFoundError`, `LaunchRuleEvaluationError`, and
|
|
262
|
+
`LaunchInviteValidationError` provide actionable failure categories.
|
|
263
|
+
|
|
264
|
+
## Testing
|
|
265
|
+
|
|
266
|
+
```bash
|
|
267
|
+
npm test
|
|
268
|
+
npm run typecheck
|
|
269
|
+
npm run lint
|
|
270
|
+
npm run build
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
Use `npm run check` to run the complete verification pipeline.
|
|
274
|
+
|
|
275
|
+
## Privacy
|
|
276
|
+
|
|
277
|
+
Launch-Kit contains no telemetry or analytics, collects no user information, sends no configuration
|
|
278
|
+
anywhere, and makes no external requests. Access evaluation is local and deterministic except for
|
|
279
|
+
the custom functions your application explicitly supplies.
|
|
280
|
+
|
|
281
|
+
## Contributing
|
|
282
|
+
|
|
283
|
+
Issues and pull requests are welcome. See [CONTRIBUTING.md](./CONTRIBUTING.md) for the local workflow
|
|
284
|
+
and project expectations.
|
|
285
|
+
|
|
286
|
+
## License
|
|
287
|
+
|
|
288
|
+
[MIT](./LICENSE)
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
/** The minimal, authentication-provider-independent user shape LaunchKit understands. */
|
|
2
|
+
interface LaunchUser<Metadata extends Record<string, unknown> = Record<string, unknown>> {
|
|
3
|
+
id: string;
|
|
4
|
+
email?: string;
|
|
5
|
+
roles?: readonly string[];
|
|
6
|
+
plan?: string;
|
|
7
|
+
tags?: readonly string[];
|
|
8
|
+
metadata?: Metadata;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
type DateInput = string | Date;
|
|
12
|
+
interface CohortDefinition {
|
|
13
|
+
roles?: readonly string[];
|
|
14
|
+
plans?: readonly string[];
|
|
15
|
+
tags?: readonly string[];
|
|
16
|
+
emails?: readonly string[];
|
|
17
|
+
emailDomains?: readonly string[];
|
|
18
|
+
metadata?: Readonly<Record<string, unknown>>;
|
|
19
|
+
}
|
|
20
|
+
interface ScheduledRollout {
|
|
21
|
+
percentage: number;
|
|
22
|
+
at: DateInput;
|
|
23
|
+
}
|
|
24
|
+
type RolloutConfig = number | readonly ScheduledRollout[];
|
|
25
|
+
interface RuleContext<User extends LaunchUser = LaunchUser, Context = unknown, PhaseName extends string = string> {
|
|
26
|
+
user: User;
|
|
27
|
+
phase: PhaseName;
|
|
28
|
+
now: Date;
|
|
29
|
+
context: Context | undefined;
|
|
30
|
+
}
|
|
31
|
+
type CustomRule<User extends LaunchUser = LaunchUser, Context = unknown, PhaseName extends string = string> = (input: RuleContext<User, Context, PhaseName>) => boolean | Promise<boolean>;
|
|
32
|
+
interface InviteContext<User extends LaunchUser = LaunchUser, PhaseName extends string = string> {
|
|
33
|
+
code: string;
|
|
34
|
+
user: User;
|
|
35
|
+
phase: PhaseName;
|
|
36
|
+
}
|
|
37
|
+
interface InviteConfig<User extends LaunchUser = LaunchUser> {
|
|
38
|
+
validate: (input: InviteContext<User>) => boolean | Promise<boolean>;
|
|
39
|
+
}
|
|
40
|
+
/** A phase can reference cohorts and can also contain simple access criteria directly. */
|
|
41
|
+
interface PhaseConfig extends CohortDefinition {
|
|
42
|
+
from?: DateInput;
|
|
43
|
+
until?: DateInput;
|
|
44
|
+
allow?: readonly string[];
|
|
45
|
+
rules?: readonly string[];
|
|
46
|
+
inviteRequired?: boolean;
|
|
47
|
+
rollout?: RolloutConfig;
|
|
48
|
+
}
|
|
49
|
+
interface FeatureConfig {
|
|
50
|
+
id?: string;
|
|
51
|
+
phases: Readonly<Record<string, PhaseConfig>>;
|
|
52
|
+
}
|
|
53
|
+
interface LaunchConfig<User extends LaunchUser = LaunchUser, Context = unknown> {
|
|
54
|
+
id?: string;
|
|
55
|
+
cohorts?: Readonly<Record<string, CohortDefinition>>;
|
|
56
|
+
rules?: Readonly<Record<string, CustomRule<User, Context>>>;
|
|
57
|
+
invite?: InviteConfig<User>;
|
|
58
|
+
phases?: Readonly<Record<string, PhaseConfig>>;
|
|
59
|
+
features?: Readonly<Record<string, FeatureConfig>>;
|
|
60
|
+
}
|
|
61
|
+
interface LaunchOptions {
|
|
62
|
+
now?: () => Date;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
type AccessReason = "ALLOWED" | "PUBLIC" | "NO_ACTIVE_PHASE" | "NOT_IN_COHORT" | "ROLE_REQUIRED" | "PLAN_REQUIRED" | "EMAIL_NOT_ALLOWED" | "INVITE_REQUIRED" | "INVALID_INVITE" | "ROLLOUT_EXCLUDED" | "METADATA_MISMATCH" | "CUSTOM_RULE_DENIED";
|
|
66
|
+
interface AccessDecision<PhaseName extends string = string> {
|
|
67
|
+
allowed: boolean;
|
|
68
|
+
reason: AccessReason;
|
|
69
|
+
/** A stable, human-readable explanation. Do not branch application logic on this text. */
|
|
70
|
+
message: string;
|
|
71
|
+
phase: PhaseName | null;
|
|
72
|
+
nextPhase?: PhaseName;
|
|
73
|
+
nextPhaseAt?: string;
|
|
74
|
+
rollout?: {
|
|
75
|
+
bucket: number;
|
|
76
|
+
percentage: number;
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
interface AccessOptions<Context = unknown> {
|
|
80
|
+
inviteCode?: string;
|
|
81
|
+
context?: Context;
|
|
82
|
+
/** Used for deterministic rollouts when the user does not have a stable id. */
|
|
83
|
+
identifier?: string;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
type PhaseStatus = "completed" | "active" | "upcoming";
|
|
87
|
+
interface TimelineEntry<PhaseName extends string = string> {
|
|
88
|
+
name: PhaseName;
|
|
89
|
+
status: PhaseStatus;
|
|
90
|
+
from?: string;
|
|
91
|
+
until?: string;
|
|
92
|
+
rollout?: number;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
interface Clock {
|
|
96
|
+
now(): Date;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
interface CompiledPhase<Name extends string = string> {
|
|
100
|
+
name: Name;
|
|
101
|
+
config: PhaseConfig;
|
|
102
|
+
from?: Date;
|
|
103
|
+
until?: Date;
|
|
104
|
+
}
|
|
105
|
+
interface CompiledLaunch<Name extends string = string> {
|
|
106
|
+
id: string;
|
|
107
|
+
phases: readonly CompiledPhase<Name>[];
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
declare class LaunchController<PhaseName extends string, User extends LaunchUser = LaunchUser, Context = unknown> {
|
|
111
|
+
private readonly compiled;
|
|
112
|
+
private readonly config;
|
|
113
|
+
private readonly clock;
|
|
114
|
+
constructor(compiled: CompiledLaunch<PhaseName>, config: LaunchConfig<User, Context>, clock: Clock);
|
|
115
|
+
private current;
|
|
116
|
+
phase(): PhaseName | null;
|
|
117
|
+
is(phase: PhaseName): boolean;
|
|
118
|
+
hasStarted(): boolean;
|
|
119
|
+
hasEnded(): boolean;
|
|
120
|
+
previousPhase(): PhaseName | null;
|
|
121
|
+
nextPhase(): PhaseName | null;
|
|
122
|
+
private next;
|
|
123
|
+
timeline(): readonly TimelineEntry<PhaseName>[];
|
|
124
|
+
canAccess(user: User, options?: AccessOptions<Context>): Promise<AccessDecision<PhaseName>>;
|
|
125
|
+
private decision;
|
|
126
|
+
}
|
|
127
|
+
type StringKey<T> = Extract<keyof T, string>;
|
|
128
|
+
type PhaseNames<T> = T extends {
|
|
129
|
+
phases: infer P;
|
|
130
|
+
} ? StringKey<P> : never;
|
|
131
|
+
type FeatureMap<T> = T extends {
|
|
132
|
+
features: infer F;
|
|
133
|
+
} ? F : Record<never, never>;
|
|
134
|
+
declare class LaunchKit<Config extends LaunchConfig<User, Context>, User extends LaunchUser = LaunchUser, Context = unknown> extends LaunchController<PhaseNames<Config>, User, Context> {
|
|
135
|
+
private readonly compiledFeatures;
|
|
136
|
+
constructor(root: CompiledLaunch<PhaseNames<Config>>, config: Config, clock: Clock, compiledFeatures: Readonly<Record<string, CompiledLaunch>>);
|
|
137
|
+
private readonly sourceConfig;
|
|
138
|
+
private readonly sourceClock;
|
|
139
|
+
feature<Name extends StringKey<FeatureMap<Config>>>(name: Name): LaunchController<PhaseNames<FeatureMap<Config>[Name]>, User, Context>;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** A small, stable FNV-1a hash. It intentionally does not depend on platform crypto APIs. */
|
|
143
|
+
declare function stableHash(value: string): number;
|
|
144
|
+
/** Assign a stable identifier to one of 100 buckets (0 through 99). */
|
|
145
|
+
declare function bucket(value: string): number;
|
|
146
|
+
|
|
147
|
+
declare class LaunchKitError extends Error {
|
|
148
|
+
readonly name: string;
|
|
149
|
+
}
|
|
150
|
+
declare class LaunchConfigurationError extends LaunchKitError {
|
|
151
|
+
readonly name = "LaunchConfigurationError";
|
|
152
|
+
}
|
|
153
|
+
declare class LaunchFeatureNotFoundError extends LaunchKitError {
|
|
154
|
+
readonly name = "LaunchFeatureNotFoundError";
|
|
155
|
+
constructor(feature: string);
|
|
156
|
+
}
|
|
157
|
+
declare class LaunchRuleEvaluationError extends LaunchKitError {
|
|
158
|
+
readonly name = "LaunchRuleEvaluationError";
|
|
159
|
+
readonly cause: unknown;
|
|
160
|
+
constructor(rule: string, cause: unknown);
|
|
161
|
+
}
|
|
162
|
+
declare class LaunchInviteValidationError extends LaunchKitError {
|
|
163
|
+
readonly name = "LaunchInviteValidationError";
|
|
164
|
+
readonly cause: unknown;
|
|
165
|
+
constructor(cause: unknown);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
declare function createLaunch<User extends LaunchUser = LaunchUser, Context = unknown, const Config extends LaunchConfig<User, Context> = LaunchConfig<User, Context>>(config: Config, options?: LaunchOptions): LaunchKit<Config, User, Context>;
|
|
169
|
+
|
|
170
|
+
export { type AccessDecision, type AccessOptions, type AccessReason, type CohortDefinition, type CustomRule, type DateInput, type FeatureConfig, type InviteConfig, type InviteContext, type LaunchConfig, LaunchConfigurationError, LaunchFeatureNotFoundError, LaunchInviteValidationError, LaunchKitError, type LaunchOptions, LaunchRuleEvaluationError, type LaunchUser, type PhaseConfig, type PhaseStatus, type RolloutConfig, type RuleContext, type ScheduledRollout, type TimelineEntry, bucket, createLaunch, stableHash };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,466 @@
|
|
|
1
|
+
// src/time/clock.ts
|
|
2
|
+
function createClock(now) {
|
|
3
|
+
return {
|
|
4
|
+
now: () => new Date((now ? now() : /* @__PURE__ */ new Date()).getTime())
|
|
5
|
+
};
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
// src/access/email.ts
|
|
9
|
+
function normalizeEmail(value) {
|
|
10
|
+
return value.trim().toLowerCase();
|
|
11
|
+
}
|
|
12
|
+
function emailDomain(value) {
|
|
13
|
+
const normalized = normalizeEmail(value);
|
|
14
|
+
const at = normalized.lastIndexOf("@");
|
|
15
|
+
return at > 0 && at < normalized.length - 1 ? normalized.slice(at + 1) : void 0;
|
|
16
|
+
}
|
|
17
|
+
function isValidEmail(value) {
|
|
18
|
+
const normalized = normalizeEmail(value);
|
|
19
|
+
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(normalized);
|
|
20
|
+
}
|
|
21
|
+
function normalizeDomain(value) {
|
|
22
|
+
const normalized = value.trim().toLowerCase();
|
|
23
|
+
return normalized.startsWith("*@") ? normalized.slice(2) : normalized;
|
|
24
|
+
}
|
|
25
|
+
function matchesEmail(email, emails, domains) {
|
|
26
|
+
if (!email) return false;
|
|
27
|
+
const normalized = normalizeEmail(email);
|
|
28
|
+
const domain = emailDomain(normalized);
|
|
29
|
+
if (emails?.some((candidate) => {
|
|
30
|
+
const normalizedCandidate = normalizeEmail(candidate);
|
|
31
|
+
return normalizedCandidate === normalized || normalizedCandidate.startsWith("*@") && normalizeDomain(normalizedCandidate) === domain;
|
|
32
|
+
}))
|
|
33
|
+
return true;
|
|
34
|
+
return Boolean(
|
|
35
|
+
domain && domains?.some((candidate) => normalizeDomain(candidate) === domain)
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// src/access/metadata.ts
|
|
40
|
+
function matchesMetadata(actual, expected) {
|
|
41
|
+
if (!expected) return true;
|
|
42
|
+
if (!actual) return false;
|
|
43
|
+
return Object.entries(expected).every(
|
|
44
|
+
([key, value]) => Object.hasOwn(actual, key) && Object.is(actual[key], value)
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// src/access/cohorts.ts
|
|
49
|
+
function overlaps(actual, expected) {
|
|
50
|
+
return Boolean(actual?.some((value) => expected.includes(value)));
|
|
51
|
+
}
|
|
52
|
+
function matchCriteria(user, criteria) {
|
|
53
|
+
if (criteria.roles && !overlaps(user.roles, criteria.roles))
|
|
54
|
+
return { matches: false, reason: "ROLE_REQUIRED" };
|
|
55
|
+
if (criteria.plans && (!user.plan || !criteria.plans.includes(user.plan)))
|
|
56
|
+
return { matches: false, reason: "PLAN_REQUIRED" };
|
|
57
|
+
if (criteria.tags && !overlaps(user.tags, criteria.tags))
|
|
58
|
+
return { matches: false, reason: "NOT_IN_COHORT" };
|
|
59
|
+
if ((criteria.emails || criteria.emailDomains) && !matchesEmail(user.email, criteria.emails, criteria.emailDomains))
|
|
60
|
+
return { matches: false, reason: "EMAIL_NOT_ALLOWED" };
|
|
61
|
+
if (!matchesMetadata(user.metadata, criteria.metadata))
|
|
62
|
+
return { matches: false, reason: "METADATA_MISMATCH" };
|
|
63
|
+
return { matches: true };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// src/utils/hash.ts
|
|
67
|
+
function stableHash(value) {
|
|
68
|
+
let hash = 2166136261;
|
|
69
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
70
|
+
hash ^= value.charCodeAt(index);
|
|
71
|
+
hash = Math.imul(hash, 16777619);
|
|
72
|
+
}
|
|
73
|
+
return hash >>> 0;
|
|
74
|
+
}
|
|
75
|
+
function bucket(value) {
|
|
76
|
+
return stableHash(value) % 100;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// src/access/rollout.ts
|
|
80
|
+
function rolloutPercentage(rollout, now) {
|
|
81
|
+
if (typeof rollout === "number") return rollout;
|
|
82
|
+
let percentage = 0;
|
|
83
|
+
for (const step of rollout) {
|
|
84
|
+
if (new Date(step.at).getTime() <= now.getTime()) percentage = step.percentage;
|
|
85
|
+
else break;
|
|
86
|
+
}
|
|
87
|
+
return percentage;
|
|
88
|
+
}
|
|
89
|
+
function evaluateRollout(rollout, key, now) {
|
|
90
|
+
const percentage = rolloutPercentage(rollout, now);
|
|
91
|
+
const assignedBucket = bucket(key);
|
|
92
|
+
return {
|
|
93
|
+
included: assignedBucket < percentage,
|
|
94
|
+
percentage,
|
|
95
|
+
bucket: assignedBucket
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
function sortRollout(rollout) {
|
|
99
|
+
if (typeof rollout === "number") return rollout;
|
|
100
|
+
return [...rollout].sort(
|
|
101
|
+
(left, right) => new Date(left.at).getTime() - new Date(right.at).getTime()
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// src/errors/index.ts
|
|
106
|
+
var LaunchKitError = class extends Error {
|
|
107
|
+
name = "LaunchKitError";
|
|
108
|
+
};
|
|
109
|
+
var LaunchConfigurationError = class extends LaunchKitError {
|
|
110
|
+
name = "LaunchConfigurationError";
|
|
111
|
+
};
|
|
112
|
+
var LaunchFeatureNotFoundError = class extends LaunchKitError {
|
|
113
|
+
name = "LaunchFeatureNotFoundError";
|
|
114
|
+
constructor(feature) {
|
|
115
|
+
super(`Feature "${feature}" was not found in the LaunchKit configuration.`);
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
var LaunchRuleEvaluationError = class extends LaunchKitError {
|
|
119
|
+
name = "LaunchRuleEvaluationError";
|
|
120
|
+
cause;
|
|
121
|
+
constructor(rule, cause) {
|
|
122
|
+
super(`Custom rule "${rule}" threw while evaluating access.`);
|
|
123
|
+
this.cause = cause;
|
|
124
|
+
}
|
|
125
|
+
};
|
|
126
|
+
var LaunchInviteValidationError = class extends LaunchKitError {
|
|
127
|
+
name = "LaunchInviteValidationError";
|
|
128
|
+
cause;
|
|
129
|
+
constructor(cause) {
|
|
130
|
+
super("The invite validator threw while evaluating access.");
|
|
131
|
+
this.cause = cause;
|
|
132
|
+
}
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
// src/core/launch.ts
|
|
136
|
+
var messages = {
|
|
137
|
+
ALLOWED: "Access granted.",
|
|
138
|
+
PUBLIC: "Access granted because the active phase is public.",
|
|
139
|
+
NO_ACTIVE_PHASE: "Access denied because no launch phase is currently active.",
|
|
140
|
+
NOT_IN_COHORT: "Access denied because the user does not belong to an allowed cohort.",
|
|
141
|
+
ROLE_REQUIRED: "Access denied because the user does not have a required role.",
|
|
142
|
+
PLAN_REQUIRED: "Access denied because the user is not on a required plan.",
|
|
143
|
+
EMAIL_NOT_ALLOWED: "Access denied because the user's email is not allowed.",
|
|
144
|
+
INVITE_REQUIRED: "Access denied because an invite code is required.",
|
|
145
|
+
INVALID_INVITE: "Access denied because the invite code is invalid.",
|
|
146
|
+
ROLLOUT_EXCLUDED: "Access denied because the user is not included in the rollout.",
|
|
147
|
+
METADATA_MISMATCH: "Access denied because the user's metadata does not match.",
|
|
148
|
+
CUSTOM_RULE_DENIED: "Access denied by a custom rule."
|
|
149
|
+
};
|
|
150
|
+
function isActive(phase, time) {
|
|
151
|
+
return (phase.from?.getTime() ?? Number.NEGATIVE_INFINITY) <= time && time < (phase.until?.getTime() ?? Number.POSITIVE_INFINITY);
|
|
152
|
+
}
|
|
153
|
+
function hasCriteria(config) {
|
|
154
|
+
return Boolean(
|
|
155
|
+
config.roles || config.plans || config.tags || config.emails || config.emailDomains || config.metadata
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
var LaunchController = class {
|
|
159
|
+
constructor(compiled, config, clock) {
|
|
160
|
+
this.compiled = compiled;
|
|
161
|
+
this.config = config;
|
|
162
|
+
this.clock = clock;
|
|
163
|
+
}
|
|
164
|
+
compiled;
|
|
165
|
+
config;
|
|
166
|
+
clock;
|
|
167
|
+
current(now = this.clock.now()) {
|
|
168
|
+
return this.compiled.phases.find((phase) => isActive(phase, now.getTime()));
|
|
169
|
+
}
|
|
170
|
+
phase() {
|
|
171
|
+
return this.current()?.name ?? null;
|
|
172
|
+
}
|
|
173
|
+
is(phase) {
|
|
174
|
+
return this.phase() === phase;
|
|
175
|
+
}
|
|
176
|
+
hasStarted() {
|
|
177
|
+
const first = this.compiled.phases[0];
|
|
178
|
+
return Boolean(
|
|
179
|
+
first && (!first.from || this.clock.now().getTime() >= first.from.getTime())
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
hasEnded() {
|
|
183
|
+
const last = this.compiled.phases.at(-1);
|
|
184
|
+
return Boolean(last?.until && this.clock.now().getTime() >= last.until.getTime());
|
|
185
|
+
}
|
|
186
|
+
previousPhase() {
|
|
187
|
+
const now = this.clock.now();
|
|
188
|
+
const active = this.current(now);
|
|
189
|
+
if (active) {
|
|
190
|
+
const index = this.compiled.phases.indexOf(active);
|
|
191
|
+
return this.compiled.phases[index - 1]?.name ?? null;
|
|
192
|
+
}
|
|
193
|
+
return [...this.compiled.phases].reverse().find((phase) => phase.until && phase.until.getTime() <= now.getTime())?.name ?? null;
|
|
194
|
+
}
|
|
195
|
+
nextPhase() {
|
|
196
|
+
return this.next(this.clock.now())?.name ?? null;
|
|
197
|
+
}
|
|
198
|
+
next(now) {
|
|
199
|
+
const active = this.current(now);
|
|
200
|
+
if (active) {
|
|
201
|
+
return this.compiled.phases[this.compiled.phases.indexOf(active) + 1];
|
|
202
|
+
}
|
|
203
|
+
return this.compiled.phases.find(
|
|
204
|
+
(phase) => phase.from && phase.from.getTime() > now.getTime()
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
timeline() {
|
|
208
|
+
const now = this.clock.now();
|
|
209
|
+
return this.compiled.phases.map((phase) => {
|
|
210
|
+
const active = isActive(phase, now.getTime());
|
|
211
|
+
const completed = Boolean(phase.until && phase.until.getTime() <= now.getTime());
|
|
212
|
+
return {
|
|
213
|
+
name: phase.name,
|
|
214
|
+
status: active ? "active" : completed ? "completed" : "upcoming",
|
|
215
|
+
...phase.from ? { from: phase.from.toISOString() } : {},
|
|
216
|
+
...phase.until ? { until: phase.until.toISOString() } : {},
|
|
217
|
+
...phase.config.rollout === void 0 ? {} : { rollout: rolloutPercentage(phase.config.rollout, now) }
|
|
218
|
+
};
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
async canAccess(user, options = {}) {
|
|
222
|
+
const now = this.clock.now();
|
|
223
|
+
const phase = this.current(now);
|
|
224
|
+
if (!phase) return this.decision(false, "NO_ACTIVE_PHASE", null, now);
|
|
225
|
+
const direct = matchCriteria(user, phase.config);
|
|
226
|
+
if (hasCriteria(phase.config) && !direct.matches) {
|
|
227
|
+
return this.decision(false, direct.reason ?? "NOT_IN_COHORT", phase.name, now);
|
|
228
|
+
}
|
|
229
|
+
const allowedCohorts = phase.config.allow ?? [];
|
|
230
|
+
const isPublic = allowedCohorts.includes("everyone");
|
|
231
|
+
if (allowedCohorts.length > 0 && !isPublic) {
|
|
232
|
+
const matched = allowedCohorts.some((name) => {
|
|
233
|
+
const cohort = this.config.cohorts?.[name];
|
|
234
|
+
return cohort ? matchCriteria(user, cohort).matches : false;
|
|
235
|
+
});
|
|
236
|
+
if (!matched) return this.decision(false, "NOT_IN_COHORT", phase.name, now);
|
|
237
|
+
}
|
|
238
|
+
if (phase.config.inviteRequired) {
|
|
239
|
+
if (!options.inviteCode) {
|
|
240
|
+
return this.decision(false, "INVITE_REQUIRED", phase.name, now);
|
|
241
|
+
}
|
|
242
|
+
let valid;
|
|
243
|
+
try {
|
|
244
|
+
valid = Boolean(
|
|
245
|
+
await this.config.invite?.validate({
|
|
246
|
+
code: options.inviteCode,
|
|
247
|
+
user,
|
|
248
|
+
phase: phase.name
|
|
249
|
+
})
|
|
250
|
+
);
|
|
251
|
+
} catch (error) {
|
|
252
|
+
throw new LaunchInviteValidationError(error);
|
|
253
|
+
}
|
|
254
|
+
if (!valid) return this.decision(false, "INVALID_INVITE", phase.name, now);
|
|
255
|
+
}
|
|
256
|
+
for (const ruleName of phase.config.rules ?? []) {
|
|
257
|
+
const rule = this.config.rules?.[ruleName];
|
|
258
|
+
try {
|
|
259
|
+
if (!await rule?.({
|
|
260
|
+
user,
|
|
261
|
+
phase: phase.name,
|
|
262
|
+
now: new Date(now),
|
|
263
|
+
context: options.context
|
|
264
|
+
})) {
|
|
265
|
+
return this.decision(false, "CUSTOM_RULE_DENIED", phase.name, now);
|
|
266
|
+
}
|
|
267
|
+
} catch (error) {
|
|
268
|
+
throw new LaunchRuleEvaluationError(ruleName, error);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
if (phase.config.rollout !== void 0) {
|
|
272
|
+
const identifier = options.identifier?.trim() || user.id?.trim();
|
|
273
|
+
if (!identifier) return this.decision(false, "ROLLOUT_EXCLUDED", phase.name, now);
|
|
274
|
+
const rollout = evaluateRollout(
|
|
275
|
+
phase.config.rollout,
|
|
276
|
+
`${this.compiled.id}:${phase.name}:${identifier}`,
|
|
277
|
+
now
|
|
278
|
+
);
|
|
279
|
+
if (!rollout.included) {
|
|
280
|
+
return {
|
|
281
|
+
...this.decision(false, "ROLLOUT_EXCLUDED", phase.name, now),
|
|
282
|
+
rollout: { bucket: rollout.bucket, percentage: rollout.percentage }
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
return {
|
|
286
|
+
...this.decision(true, isPublic ? "PUBLIC" : "ALLOWED", phase.name, now),
|
|
287
|
+
rollout: { bucket: rollout.bucket, percentage: rollout.percentage }
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
return this.decision(true, isPublic ? "PUBLIC" : "ALLOWED", phase.name, now);
|
|
291
|
+
}
|
|
292
|
+
decision(allowed, reason, phase, now) {
|
|
293
|
+
const next = this.next(now);
|
|
294
|
+
return {
|
|
295
|
+
allowed,
|
|
296
|
+
reason,
|
|
297
|
+
message: messages[reason],
|
|
298
|
+
phase,
|
|
299
|
+
...next ? { nextPhase: next.name } : {},
|
|
300
|
+
...next?.from ? { nextPhaseAt: next.from.toISOString() } : {}
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
};
|
|
304
|
+
var LaunchKit = class extends LaunchController {
|
|
305
|
+
constructor(root, config, clock, compiledFeatures) {
|
|
306
|
+
super(root, config, clock);
|
|
307
|
+
this.compiledFeatures = compiledFeatures;
|
|
308
|
+
this.sourceConfig = config;
|
|
309
|
+
this.sourceClock = clock;
|
|
310
|
+
}
|
|
311
|
+
compiledFeatures;
|
|
312
|
+
sourceConfig;
|
|
313
|
+
sourceClock;
|
|
314
|
+
feature(name) {
|
|
315
|
+
const feature = this.compiledFeatures[name];
|
|
316
|
+
if (!feature) throw new LaunchFeatureNotFoundError(name);
|
|
317
|
+
return new LaunchController(
|
|
318
|
+
feature,
|
|
319
|
+
this.sourceConfig,
|
|
320
|
+
this.sourceClock
|
|
321
|
+
);
|
|
322
|
+
}
|
|
323
|
+
};
|
|
324
|
+
|
|
325
|
+
// src/validation/config.ts
|
|
326
|
+
function fail(path, detail) {
|
|
327
|
+
throw new LaunchConfigurationError(`${path}: ${detail}`);
|
|
328
|
+
}
|
|
329
|
+
function parseDate(value, path) {
|
|
330
|
+
const date = value instanceof Date ? new Date(value.getTime()) : new Date(value);
|
|
331
|
+
if (Number.isNaN(date.getTime())) fail(path, `invalid date ${JSON.stringify(value)}.`);
|
|
332
|
+
return date;
|
|
333
|
+
}
|
|
334
|
+
function validatePercentage(value, path) {
|
|
335
|
+
if (!Number.isFinite(value) || value < 0 || value > 100) {
|
|
336
|
+
fail(
|
|
337
|
+
path,
|
|
338
|
+
`rollout percentage must be between 0 and 100; received ${String(value)}.`
|
|
339
|
+
);
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
function validateRollout(rollout, path) {
|
|
343
|
+
if (typeof rollout === "number") {
|
|
344
|
+
validatePercentage(rollout, path);
|
|
345
|
+
return rollout;
|
|
346
|
+
}
|
|
347
|
+
if (rollout.length === 0)
|
|
348
|
+
fail(path, "scheduled rollout must contain at least one step.");
|
|
349
|
+
let previousAt = Number.NEGATIVE_INFINITY;
|
|
350
|
+
let previousPercentage = 0;
|
|
351
|
+
for (const [index, step] of rollout.entries()) {
|
|
352
|
+
validatePercentage(step.percentage, `${path}[${index}].percentage`);
|
|
353
|
+
const at = parseDate(step.at, `${path}[${index}].at`).getTime();
|
|
354
|
+
if (at <= previousAt)
|
|
355
|
+
fail(path, "scheduled rollout steps must be in ascending time order.");
|
|
356
|
+
if (step.percentage < previousPercentage) {
|
|
357
|
+
fail(path, "scheduled rollout percentages must not decrease.");
|
|
358
|
+
}
|
|
359
|
+
previousAt = at;
|
|
360
|
+
previousPercentage = step.percentage;
|
|
361
|
+
}
|
|
362
|
+
return sortRollout(rollout);
|
|
363
|
+
}
|
|
364
|
+
function validateCriteria(criteria, path) {
|
|
365
|
+
for (const [index, email] of (criteria.emails ?? []).entries()) {
|
|
366
|
+
if (!isValidEmail(email.replace(/^\*@/, "wildcard@"))) {
|
|
367
|
+
fail(`${path}.emails[${index}]`, `invalid email ${JSON.stringify(email)}.`);
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
for (const [index, domain] of (criteria.emailDomains ?? []).entries()) {
|
|
371
|
+
const normalized = normalizeDomain(domain);
|
|
372
|
+
if (!/^[a-z0-9](?:[a-z0-9.-]*[a-z0-9])?\.[a-z]{2,}$/i.test(normalized)) {
|
|
373
|
+
fail(
|
|
374
|
+
`${path}.emailDomains[${index}]`,
|
|
375
|
+
`invalid email domain ${JSON.stringify(domain)}.`
|
|
376
|
+
);
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
function compilePhases(phases, config, path) {
|
|
381
|
+
const names = Object.keys(phases);
|
|
382
|
+
const normalizedNames = /* @__PURE__ */ new Set();
|
|
383
|
+
const compiled = names.map((name) => {
|
|
384
|
+
if (!name.trim()) fail(path, "phase names cannot be empty.");
|
|
385
|
+
const normalized = name.toLowerCase();
|
|
386
|
+
if (normalizedNames.has(normalized)) fail(path, `duplicate phase name "${name}".`);
|
|
387
|
+
normalizedNames.add(normalized);
|
|
388
|
+
const phase = phases[name];
|
|
389
|
+
if (!phase) fail(`${path}.${name}`, "phase configuration is missing.");
|
|
390
|
+
validateCriteria(phase, `${path}.${name}`);
|
|
391
|
+
for (const cohort of phase.allow ?? []) {
|
|
392
|
+
if (cohort !== "everyone" && !config.cohorts?.[cohort]) {
|
|
393
|
+
fail(`${path}.${name}.allow`, `unknown cohort "${cohort}".`);
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
for (const rule of phase.rules ?? []) {
|
|
397
|
+
if (!config.rules?.[rule])
|
|
398
|
+
fail(`${path}.${name}.rules`, `unknown custom rule "${rule}".`);
|
|
399
|
+
}
|
|
400
|
+
if (phase.inviteRequired && !config.invite) {
|
|
401
|
+
fail(`${path}.${name}.inviteRequired`, "an invite validator must be configured.");
|
|
402
|
+
}
|
|
403
|
+
const from = phase.from === void 0 ? void 0 : parseDate(phase.from, `${path}.${name}.from`);
|
|
404
|
+
const until = phase.until === void 0 ? void 0 : parseDate(phase.until, `${path}.${name}.until`);
|
|
405
|
+
if (from && until && until.getTime() <= from.getTime()) {
|
|
406
|
+
fail(`${path}.${name}`, "phase end must be after phase start.");
|
|
407
|
+
}
|
|
408
|
+
const normalizedPhase = {
|
|
409
|
+
...phase,
|
|
410
|
+
...phase.rollout === void 0 ? {} : { rollout: validateRollout(phase.rollout, `${path}.${name}.rollout`) }
|
|
411
|
+
};
|
|
412
|
+
return {
|
|
413
|
+
name,
|
|
414
|
+
config: normalizedPhase,
|
|
415
|
+
...from ? { from } : {},
|
|
416
|
+
...until ? { until } : {}
|
|
417
|
+
};
|
|
418
|
+
});
|
|
419
|
+
compiled.sort((left, right) => {
|
|
420
|
+
const difference = (left.from?.getTime() ?? Number.NEGATIVE_INFINITY) - (right.from?.getTime() ?? Number.NEGATIVE_INFINITY);
|
|
421
|
+
return difference || names.indexOf(left.name) - names.indexOf(right.name);
|
|
422
|
+
});
|
|
423
|
+
for (let index = 1; index < compiled.length; index += 1) {
|
|
424
|
+
const previous = compiled[index - 1];
|
|
425
|
+
const current = compiled[index];
|
|
426
|
+
if (!previous || !current) continue;
|
|
427
|
+
if (!previous.until || !current.from || previous.until.getTime() > current.from.getTime()) {
|
|
428
|
+
fail(path, `phases "${previous.name}" and "${current.name}" overlap.`);
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
return compiled;
|
|
432
|
+
}
|
|
433
|
+
function validateConfig(config) {
|
|
434
|
+
if (!config || typeof config !== "object") fail("config", "expected an object.");
|
|
435
|
+
for (const [name, cohort] of Object.entries(config.cohorts ?? {})) {
|
|
436
|
+
validateCriteria(cohort, `cohorts.${name}`);
|
|
437
|
+
}
|
|
438
|
+
if (!config.phases && !config.features) {
|
|
439
|
+
fail("config", "at least one product phase or feature must be configured.");
|
|
440
|
+
}
|
|
441
|
+
const id = config.id?.trim() || "launchkit";
|
|
442
|
+
const root = {
|
|
443
|
+
id,
|
|
444
|
+
phases: compilePhases(config.phases ?? {}, config, "phases")
|
|
445
|
+
};
|
|
446
|
+
const features = {};
|
|
447
|
+
for (const [name, feature] of Object.entries(config.features ?? {})) {
|
|
448
|
+
if (!name.trim()) fail("features", "feature names cannot be empty.");
|
|
449
|
+
features[name] = {
|
|
450
|
+
id: feature.id?.trim() || `${id}:${name}`,
|
|
451
|
+
phases: compilePhases(feature.phases, config, `features.${name}.phases`)
|
|
452
|
+
};
|
|
453
|
+
}
|
|
454
|
+
return { config, root, features };
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
// src/index.ts
|
|
458
|
+
function createLaunch(config, options = {}) {
|
|
459
|
+
const validated = validateConfig(config);
|
|
460
|
+
const clock = createClock(options.now);
|
|
461
|
+
return new LaunchKit(validated.root, config, clock, validated.features);
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
export { LaunchConfigurationError, LaunchFeatureNotFoundError, LaunchInviteValidationError, LaunchKitError, LaunchRuleEvaluationError, bucket, createLaunch, stableHash };
|
|
465
|
+
//# sourceMappingURL=index.js.map
|
|
466
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/time/clock.ts","../src/access/email.ts","../src/access/metadata.ts","../src/access/cohorts.ts","../src/utils/hash.ts","../src/access/rollout.ts","../src/errors/index.ts","../src/core/launch.ts","../src/validation/config.ts","../src/index.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["export interface Clock {\n now(): Date;\n}\n\nexport function createClock(now: (() => Date) | undefined): Clock {\n return {\n now: () => new Date((now ? now() : new Date()).getTime()),\n };\n}\n","export function normalizeEmail(value: string): string {\n return value.trim().toLowerCase();\n}\n\nexport function emailDomain(value: string): string | undefined {\n const normalized = normalizeEmail(value);\n const at = normalized.lastIndexOf(\"@\");\n return at > 0 && at < normalized.length - 1 ? normalized.slice(at + 1) : undefined;\n}\n\nexport function isValidEmail(value: string): boolean {\n const normalized = normalizeEmail(value);\n return /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/.test(normalized);\n}\n\nexport function normalizeDomain(value: string): string {\n const normalized = value.trim().toLowerCase();\n return normalized.startsWith(\"*@\") ? normalized.slice(2) : normalized;\n}\n\nexport function matchesEmail(\n email: string | undefined,\n emails: readonly string[] | undefined,\n domains: readonly string[] | undefined,\n): boolean {\n if (!email) return false;\n const normalized = normalizeEmail(email);\n const domain = emailDomain(normalized);\n if (\n emails?.some((candidate) => {\n const normalizedCandidate = normalizeEmail(candidate);\n return (\n normalizedCandidate === normalized ||\n (normalizedCandidate.startsWith(\"*@\") &&\n normalizeDomain(normalizedCandidate) === domain)\n );\n })\n )\n return true;\n return Boolean(\n domain && domains?.some((candidate) => normalizeDomain(candidate) === domain),\n );\n}\n","export function matchesMetadata(\n actual: Readonly<Record<string, unknown>> | undefined,\n expected: Readonly<Record<string, unknown>> | undefined,\n): boolean {\n if (!expected) return true;\n if (!actual) return false;\n return Object.entries(expected).every(\n ([key, value]) => Object.hasOwn(actual, key) && Object.is(actual[key], value),\n );\n}\n","import type { AccessReason } from \"../types/access.js\";\nimport type { CohortDefinition } from \"../types/config.js\";\nimport type { LaunchUser } from \"../types/user.js\";\nimport { matchesEmail } from \"./email.js\";\nimport { matchesMetadata } from \"./metadata.js\";\n\nexport interface MatchResult {\n matches: boolean;\n reason?: AccessReason;\n}\n\nfunction overlaps(\n actual: readonly string[] | undefined,\n expected: readonly string[],\n): boolean {\n return Boolean(actual?.some((value) => expected.includes(value)));\n}\n\nexport function matchCriteria(user: LaunchUser, criteria: CohortDefinition): MatchResult {\n if (criteria.roles && !overlaps(user.roles, criteria.roles))\n return { matches: false, reason: \"ROLE_REQUIRED\" };\n\n if (criteria.plans && (!user.plan || !criteria.plans.includes(user.plan)))\n return { matches: false, reason: \"PLAN_REQUIRED\" };\n\n if (criteria.tags && !overlaps(user.tags, criteria.tags))\n return { matches: false, reason: \"NOT_IN_COHORT\" };\n\n if (\n (criteria.emails || criteria.emailDomains) &&\n !matchesEmail(user.email, criteria.emails, criteria.emailDomains)\n )\n return { matches: false, reason: \"EMAIL_NOT_ALLOWED\" };\n\n if (!matchesMetadata(user.metadata, criteria.metadata))\n return { matches: false, reason: \"METADATA_MISMATCH\" };\n\n return { matches: true };\n}\n","/** A small, stable FNV-1a hash. It intentionally does not depend on platform crypto APIs. */\nexport function stableHash(value: string): number {\n let hash = 0x811c9dc5;\n for (let index = 0; index < value.length; index += 1) {\n hash ^= value.charCodeAt(index);\n hash = Math.imul(hash, 0x01000193);\n }\n return hash >>> 0;\n}\n\n/** Assign a stable identifier to one of 100 buckets (0 through 99). */\nexport function bucket(value: string): number {\n return stableHash(value) % 100;\n}\n","import type { RolloutConfig, ScheduledRollout } from \"../types/config.js\";\nimport { bucket } from \"../utils/hash.js\";\n\nexport interface RolloutResult {\n included: boolean;\n percentage: number;\n bucket: number;\n}\n\nexport function rolloutPercentage(rollout: RolloutConfig, now: Date): number {\n if (typeof rollout === \"number\") return rollout;\n let percentage = 0;\n for (const step of rollout) {\n if (new Date(step.at).getTime() <= now.getTime()) percentage = step.percentage;\n else break;\n }\n return percentage;\n}\n\nexport function evaluateRollout(\n rollout: RolloutConfig,\n key: string,\n now: Date,\n): RolloutResult {\n const percentage = rolloutPercentage(rollout, now);\n const assignedBucket = bucket(key);\n return {\n included: assignedBucket < percentage,\n percentage,\n bucket: assignedBucket,\n };\n}\n\nexport function sortRollout(rollout: RolloutConfig): RolloutConfig {\n if (typeof rollout === \"number\") return rollout;\n return [...rollout].sort(\n (left: ScheduledRollout, right: ScheduledRollout) =>\n new Date(left.at).getTime() - new Date(right.at).getTime(),\n );\n}\n","export class LaunchKitError extends Error {\n override readonly name: string = \"LaunchKitError\";\n}\n\nexport class LaunchConfigurationError extends LaunchKitError {\n override readonly name = \"LaunchConfigurationError\";\n}\n\nexport class LaunchFeatureNotFoundError extends LaunchKitError {\n override readonly name = \"LaunchFeatureNotFoundError\";\n\n constructor(feature: string) {\n super(`Feature \"${feature}\" was not found in the LaunchKit configuration.`);\n }\n}\n\nexport class LaunchRuleEvaluationError extends LaunchKitError {\n override readonly name = \"LaunchRuleEvaluationError\";\n readonly cause: unknown;\n\n constructor(rule: string, cause: unknown) {\n super(`Custom rule \"${rule}\" threw while evaluating access.`);\n this.cause = cause;\n }\n}\n\nexport class LaunchInviteValidationError extends LaunchKitError {\n override readonly name = \"LaunchInviteValidationError\";\n readonly cause: unknown;\n\n constructor(cause: unknown) {\n super(\"The invite validator threw while evaluating access.\");\n this.cause = cause;\n }\n}\n","import { matchCriteria } from \"../access/cohorts.js\";\nimport { evaluateRollout, rolloutPercentage } from \"../access/rollout.js\";\nimport {\n LaunchFeatureNotFoundError,\n LaunchInviteValidationError,\n LaunchRuleEvaluationError,\n} from \"../errors/index.js\";\nimport type { AccessDecision, AccessOptions, AccessReason } from \"../types/access.js\";\nimport type { LaunchConfig, PhaseConfig } from \"../types/config.js\";\nimport type { TimelineEntry } from \"../types/phase.js\";\nimport type { LaunchUser } from \"../types/user.js\";\nimport type { Clock } from \"../time/clock.js\";\nimport type { CompiledLaunch, CompiledPhase } from \"./compiled.js\";\n\nconst messages: Record<AccessReason, string> = {\n ALLOWED: \"Access granted.\",\n PUBLIC: \"Access granted because the active phase is public.\",\n NO_ACTIVE_PHASE: \"Access denied because no launch phase is currently active.\",\n NOT_IN_COHORT: \"Access denied because the user does not belong to an allowed cohort.\",\n ROLE_REQUIRED: \"Access denied because the user does not have a required role.\",\n PLAN_REQUIRED: \"Access denied because the user is not on a required plan.\",\n EMAIL_NOT_ALLOWED: \"Access denied because the user's email is not allowed.\",\n INVITE_REQUIRED: \"Access denied because an invite code is required.\",\n INVALID_INVITE: \"Access denied because the invite code is invalid.\",\n ROLLOUT_EXCLUDED: \"Access denied because the user is not included in the rollout.\",\n METADATA_MISMATCH: \"Access denied because the user's metadata does not match.\",\n CUSTOM_RULE_DENIED: \"Access denied by a custom rule.\",\n};\n\nfunction isActive(phase: CompiledPhase, time: number): boolean {\n return (\n (phase.from?.getTime() ?? Number.NEGATIVE_INFINITY) <= time &&\n time < (phase.until?.getTime() ?? Number.POSITIVE_INFINITY)\n );\n}\n\nfunction hasCriteria(config: PhaseConfig): boolean {\n return Boolean(\n config.roles ||\n config.plans ||\n config.tags ||\n config.emails ||\n config.emailDomains ||\n config.metadata,\n );\n}\n\nexport class LaunchController<\n PhaseName extends string,\n User extends LaunchUser = LaunchUser,\n Context = unknown,\n> {\n constructor(\n private readonly compiled: CompiledLaunch<PhaseName>,\n private readonly config: LaunchConfig<User, Context>,\n private readonly clock: Clock,\n ) {}\n\n private current(now = this.clock.now()): CompiledPhase<PhaseName> | undefined {\n return this.compiled.phases.find((phase) => isActive(phase, now.getTime()));\n }\n\n phase(): PhaseName | null {\n return this.current()?.name ?? null;\n }\n\n is(phase: PhaseName): boolean {\n return this.phase() === phase;\n }\n\n hasStarted(): boolean {\n const first = this.compiled.phases[0];\n return Boolean(\n first && (!first.from || this.clock.now().getTime() >= first.from.getTime()),\n );\n }\n\n hasEnded(): boolean {\n const last = this.compiled.phases.at(-1);\n return Boolean(last?.until && this.clock.now().getTime() >= last.until.getTime());\n }\n\n previousPhase(): PhaseName | null {\n const now = this.clock.now();\n const active = this.current(now);\n if (active) {\n const index = this.compiled.phases.indexOf(active);\n return this.compiled.phases[index - 1]?.name ?? null;\n }\n return (\n [...this.compiled.phases]\n .reverse()\n .find((phase) => phase.until && phase.until.getTime() <= now.getTime())?.name ??\n null\n );\n }\n\n nextPhase(): PhaseName | null {\n return this.next(this.clock.now())?.name ?? null;\n }\n\n private next(now: Date): CompiledPhase<PhaseName> | undefined {\n const active = this.current(now);\n if (active) {\n return this.compiled.phases[this.compiled.phases.indexOf(active) + 1];\n }\n return this.compiled.phases.find(\n (phase) => phase.from && phase.from.getTime() > now.getTime(),\n );\n }\n\n timeline(): readonly TimelineEntry<PhaseName>[] {\n const now = this.clock.now();\n return this.compiled.phases.map((phase) => {\n const active = isActive(phase, now.getTime());\n const completed = Boolean(phase.until && phase.until.getTime() <= now.getTime());\n return {\n name: phase.name,\n status: active ? \"active\" : completed ? \"completed\" : \"upcoming\",\n ...(phase.from ? { from: phase.from.toISOString() } : {}),\n ...(phase.until ? { until: phase.until.toISOString() } : {}),\n ...(phase.config.rollout === undefined\n ? {}\n : { rollout: rolloutPercentage(phase.config.rollout, now) }),\n };\n });\n }\n\n async canAccess(\n user: User,\n options: AccessOptions<Context> = {},\n ): Promise<AccessDecision<PhaseName>> {\n const now = this.clock.now();\n const phase = this.current(now);\n if (!phase) return this.decision(false, \"NO_ACTIVE_PHASE\", null, now);\n\n const direct = matchCriteria(user, phase.config);\n if (hasCriteria(phase.config) && !direct.matches) {\n return this.decision(false, direct.reason ?? \"NOT_IN_COHORT\", phase.name, now);\n }\n\n const allowedCohorts = phase.config.allow ?? [];\n const isPublic = allowedCohorts.includes(\"everyone\");\n if (allowedCohorts.length > 0 && !isPublic) {\n const matched = allowedCohorts.some((name) => {\n const cohort = this.config.cohorts?.[name];\n return cohort ? matchCriteria(user, cohort).matches : false;\n });\n if (!matched) return this.decision(false, \"NOT_IN_COHORT\", phase.name, now);\n }\n\n if (phase.config.inviteRequired) {\n if (!options.inviteCode) {\n return this.decision(false, \"INVITE_REQUIRED\", phase.name, now);\n }\n let valid: boolean;\n try {\n valid = Boolean(\n await this.config.invite?.validate({\n code: options.inviteCode,\n user,\n phase: phase.name,\n }),\n );\n } catch (error) {\n throw new LaunchInviteValidationError(error);\n }\n if (!valid) return this.decision(false, \"INVALID_INVITE\", phase.name, now);\n }\n\n for (const ruleName of phase.config.rules ?? []) {\n const rule = this.config.rules?.[ruleName];\n try {\n if (\n !(await rule?.({\n user,\n phase: phase.name,\n now: new Date(now),\n context: options.context,\n }))\n ) {\n return this.decision(false, \"CUSTOM_RULE_DENIED\", phase.name, now);\n }\n } catch (error) {\n throw new LaunchRuleEvaluationError(ruleName, error);\n }\n }\n\n if (phase.config.rollout !== undefined) {\n const identifier = options.identifier?.trim() || user.id?.trim();\n if (!identifier) return this.decision(false, \"ROLLOUT_EXCLUDED\", phase.name, now);\n const rollout = evaluateRollout(\n phase.config.rollout,\n `${this.compiled.id}:${phase.name}:${identifier}`,\n now,\n );\n if (!rollout.included) {\n return {\n ...this.decision(false, \"ROLLOUT_EXCLUDED\", phase.name, now),\n rollout: { bucket: rollout.bucket, percentage: rollout.percentage },\n };\n }\n return {\n ...this.decision(true, isPublic ? \"PUBLIC\" : \"ALLOWED\", phase.name, now),\n rollout: { bucket: rollout.bucket, percentage: rollout.percentage },\n };\n }\n\n return this.decision(true, isPublic ? \"PUBLIC\" : \"ALLOWED\", phase.name, now);\n }\n\n private decision(\n allowed: boolean,\n reason: AccessReason,\n phase: PhaseName | null,\n now: Date,\n ): AccessDecision<PhaseName> {\n const next = this.next(now);\n return {\n allowed,\n reason,\n message: messages[reason],\n phase,\n ...(next ? { nextPhase: next.name } : {}),\n ...(next?.from ? { nextPhaseAt: next.from.toISOString() } : {}),\n };\n }\n}\n\ntype StringKey<T> = Extract<keyof T, string>;\ntype PhaseNames<T> = T extends { phases: infer P } ? StringKey<P> : never;\ntype FeatureMap<T> = T extends { features: infer F } ? F : Record<never, never>;\n\nexport class LaunchKit<\n Config extends LaunchConfig<User, Context>,\n User extends LaunchUser = LaunchUser,\n Context = unknown,\n> extends LaunchController<PhaseNames<Config>, User, Context> {\n constructor(\n root: CompiledLaunch<PhaseNames<Config>>,\n config: Config,\n clock: Clock,\n private readonly compiledFeatures: Readonly<Record<string, CompiledLaunch>>,\n ) {\n super(root, config, clock);\n this.sourceConfig = config;\n this.sourceClock = clock;\n }\n\n private readonly sourceConfig: Config;\n private readonly sourceClock: Clock;\n\n feature<Name extends StringKey<FeatureMap<Config>>>(\n name: Name,\n ): LaunchController<PhaseNames<FeatureMap<Config>[Name]>, User, Context> {\n const feature = this.compiledFeatures[name];\n if (!feature) throw new LaunchFeatureNotFoundError(name);\n return new LaunchController(\n feature as CompiledLaunch<PhaseNames<FeatureMap<Config>[Name]>>,\n this.sourceConfig,\n this.sourceClock,\n );\n }\n}\n","import { LaunchConfigurationError } from \"../errors/index.js\";\nimport type {\n CohortDefinition,\n DateInput,\n LaunchConfig,\n PhaseConfig,\n RolloutConfig,\n} from \"../types/config.js\";\nimport type { LaunchUser } from \"../types/user.js\";\nimport { isValidEmail, normalizeDomain } from \"../access/email.js\";\nimport { sortRollout } from \"../access/rollout.js\";\nimport type { CompiledLaunch, CompiledPhase } from \"../core/compiled.js\";\n\nfunction fail(path: string, detail: string): never {\n throw new LaunchConfigurationError(`${path}: ${detail}`);\n}\n\nfunction parseDate(value: DateInput, path: string): Date {\n const date = value instanceof Date ? new Date(value.getTime()) : new Date(value);\n if (Number.isNaN(date.getTime())) fail(path, `invalid date ${JSON.stringify(value)}.`);\n return date;\n}\n\nfunction validatePercentage(value: number, path: string): void {\n if (!Number.isFinite(value) || value < 0 || value > 100) {\n fail(\n path,\n `rollout percentage must be between 0 and 100; received ${String(value)}.`,\n );\n }\n}\n\nfunction validateRollout(rollout: RolloutConfig, path: string): RolloutConfig {\n if (typeof rollout === \"number\") {\n validatePercentage(rollout, path);\n return rollout;\n }\n if (rollout.length === 0)\n fail(path, \"scheduled rollout must contain at least one step.\");\n let previousAt = Number.NEGATIVE_INFINITY;\n let previousPercentage = 0;\n for (const [index, step] of rollout.entries()) {\n validatePercentage(step.percentage, `${path}[${index}].percentage`);\n const at = parseDate(step.at, `${path}[${index}].at`).getTime();\n if (at <= previousAt)\n fail(path, \"scheduled rollout steps must be in ascending time order.\");\n if (step.percentage < previousPercentage) {\n fail(path, \"scheduled rollout percentages must not decrease.\");\n }\n previousAt = at;\n previousPercentage = step.percentage;\n }\n return sortRollout(rollout);\n}\n\nfunction validateCriteria(criteria: CohortDefinition, path: string): void {\n for (const [index, email] of (criteria.emails ?? []).entries()) {\n if (!isValidEmail(email.replace(/^\\*@/, \"wildcard@\"))) {\n fail(`${path}.emails[${index}]`, `invalid email ${JSON.stringify(email)}.`);\n }\n }\n for (const [index, domain] of (criteria.emailDomains ?? []).entries()) {\n const normalized = normalizeDomain(domain);\n if (!/^[a-z0-9](?:[a-z0-9.-]*[a-z0-9])?\\.[a-z]{2,}$/i.test(normalized)) {\n fail(\n `${path}.emailDomains[${index}]`,\n `invalid email domain ${JSON.stringify(domain)}.`,\n );\n }\n }\n}\n\nfunction compilePhases(\n phases: Readonly<Record<string, PhaseConfig>>,\n config: {\n cohorts?: Readonly<Record<string, CohortDefinition>>;\n rules?: Readonly<Record<string, unknown>>;\n invite?: unknown;\n },\n path: string,\n): readonly CompiledPhase[] {\n const names = Object.keys(phases);\n const normalizedNames = new Set<string>();\n const compiled = names.map((name) => {\n if (!name.trim()) fail(path, \"phase names cannot be empty.\");\n const normalized = name.toLowerCase();\n if (normalizedNames.has(normalized)) fail(path, `duplicate phase name \"${name}\".`);\n normalizedNames.add(normalized);\n const phase = phases[name];\n if (!phase) fail(`${path}.${name}`, \"phase configuration is missing.\");\n validateCriteria(phase, `${path}.${name}`);\n for (const cohort of phase.allow ?? []) {\n if (cohort !== \"everyone\" && !config.cohorts?.[cohort]) {\n fail(`${path}.${name}.allow`, `unknown cohort \"${cohort}\".`);\n }\n }\n for (const rule of phase.rules ?? []) {\n if (!config.rules?.[rule])\n fail(`${path}.${name}.rules`, `unknown custom rule \"${rule}\".`);\n }\n if (phase.inviteRequired && !config.invite) {\n fail(`${path}.${name}.inviteRequired`, \"an invite validator must be configured.\");\n }\n const from =\n phase.from === undefined\n ? undefined\n : parseDate(phase.from, `${path}.${name}.from`);\n const until =\n phase.until === undefined\n ? undefined\n : parseDate(phase.until, `${path}.${name}.until`);\n if (from && until && until.getTime() <= from.getTime()) {\n fail(`${path}.${name}`, \"phase end must be after phase start.\");\n }\n const normalizedPhase: PhaseConfig = {\n ...phase,\n ...(phase.rollout === undefined\n ? {}\n : { rollout: validateRollout(phase.rollout, `${path}.${name}.rollout`) }),\n };\n return {\n name,\n config: normalizedPhase,\n ...(from ? { from } : {}),\n ...(until ? { until } : {}),\n };\n });\n\n compiled.sort((left, right) => {\n const difference =\n (left.from?.getTime() ?? Number.NEGATIVE_INFINITY) -\n (right.from?.getTime() ?? Number.NEGATIVE_INFINITY);\n return difference || names.indexOf(left.name) - names.indexOf(right.name);\n });\n for (let index = 1; index < compiled.length; index += 1) {\n const previous = compiled[index - 1];\n const current = compiled[index];\n if (!previous || !current) continue;\n if (\n !previous.until ||\n !current.from ||\n previous.until.getTime() > current.from.getTime()\n ) {\n fail(path, `phases \"${previous.name}\" and \"${current.name}\" overlap.`);\n }\n }\n return compiled;\n}\n\nexport interface ValidatedConfig<\n User extends LaunchUser = LaunchUser,\n Context = unknown,\n> {\n config: LaunchConfig<User, Context>;\n root: CompiledLaunch;\n features: Readonly<Record<string, CompiledLaunch>>;\n}\n\nexport function validateConfig<User extends LaunchUser, Context>(\n config: LaunchConfig<User, Context>,\n): ValidatedConfig<User, Context> {\n if (!config || typeof config !== \"object\") fail(\"config\", \"expected an object.\");\n for (const [name, cohort] of Object.entries(config.cohorts ?? {})) {\n validateCriteria(cohort, `cohorts.${name}`);\n }\n if (!config.phases && !config.features) {\n fail(\"config\", \"at least one product phase or feature must be configured.\");\n }\n const id = config.id?.trim() || \"launchkit\";\n const root: CompiledLaunch = {\n id,\n phases: compilePhases(config.phases ?? {}, config, \"phases\"),\n };\n const features: Record<string, CompiledLaunch> = {};\n for (const [name, feature] of Object.entries(config.features ?? {})) {\n if (!name.trim()) fail(\"features\", \"feature names cannot be empty.\");\n features[name] = {\n id: feature.id?.trim() || `${id}:${name}`,\n phases: compilePhases(feature.phases, config, `features.${name}.phases`),\n };\n }\n return { config, root, features };\n}\n","import { createClock } from \"./time/clock.js\";\nimport type { LaunchConfig, LaunchOptions } from \"./types/config.js\";\nimport type { LaunchUser } from \"./types/user.js\";\nimport { LaunchKit } from \"./core/launch.js\";\nimport { validateConfig } from \"./validation/config.js\";\n\nexport function createLaunch<\n User extends LaunchUser = LaunchUser,\n Context = unknown,\n const Config extends LaunchConfig<User, Context> = LaunchConfig<User, Context>,\n>(config: Config, options: LaunchOptions = {}): LaunchKit<Config, User, Context> {\n const validated = validateConfig(config);\n const clock = createClock(options.now);\n return new LaunchKit(validated.root as never, config, clock, validated.features);\n}\n\nexport { bucket, stableHash } from \"./utils/hash.js\";\nexport {\n LaunchConfigurationError,\n LaunchFeatureNotFoundError,\n LaunchInviteValidationError,\n LaunchKitError,\n LaunchRuleEvaluationError,\n} from \"./errors/index.js\";\nexport type { AccessDecision, AccessOptions, AccessReason } from \"./types/access.js\";\nexport type {\n CohortDefinition,\n CustomRule,\n DateInput,\n FeatureConfig,\n InviteConfig,\n InviteContext,\n LaunchConfig,\n LaunchOptions,\n PhaseConfig,\n RolloutConfig,\n RuleContext,\n ScheduledRollout,\n} from \"./types/config.js\";\nexport type { PhaseStatus, TimelineEntry } from \"./types/phase.js\";\nexport type { LaunchUser } from \"./types/user.js\";\n"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,68 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mrepol742/launch-kit",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.1.3",
|
|
4
|
+
"description": "A small, framework-independent TypeScript evaluation engine for product launches, private betas, early access, staged rollouts, and separately launched features.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"author": {
|
|
8
|
+
"name": "Melvin Jones Repol",
|
|
9
|
+
"email": "mrepol742@gmail.com",
|
|
10
|
+
"url": "https://mrepol742.github.io"
|
|
11
|
+
},
|
|
12
|
+
"repository": {
|
|
13
|
+
"type": "git",
|
|
14
|
+
"url": "git+https://github.com/mrepol742/launch-kit.git"
|
|
15
|
+
},
|
|
16
|
+
"keywords": [
|
|
17
|
+
"feature-flags",
|
|
18
|
+
"launch",
|
|
19
|
+
"early-access",
|
|
20
|
+
"rollout",
|
|
21
|
+
"typescript"
|
|
22
|
+
],
|
|
23
|
+
"engines": {
|
|
24
|
+
"node": ">=20"
|
|
25
|
+
},
|
|
26
|
+
"sideEffects": false,
|
|
27
|
+
"files": [
|
|
28
|
+
"dist",
|
|
29
|
+
"README.md",
|
|
30
|
+
"LICENSE"
|
|
31
|
+
],
|
|
32
|
+
"main": "./dist/index.js",
|
|
33
|
+
"module": "./dist/index.js",
|
|
34
|
+
"types": "./dist/index.d.ts",
|
|
35
|
+
"exports": {
|
|
36
|
+
".": {
|
|
37
|
+
"types": "./dist/index.d.ts",
|
|
38
|
+
"import": "./dist/index.js"
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
"scripts": {
|
|
42
|
+
"build": "tsup",
|
|
43
|
+
"typecheck": "tsc --noEmit",
|
|
44
|
+
"test": "vitest run",
|
|
45
|
+
"test:watch": "vitest",
|
|
46
|
+
"lint": "eslint .",
|
|
47
|
+
"format": "prettier --write .",
|
|
48
|
+
"format:check": "prettier --check .",
|
|
49
|
+
"check": "npm run typecheck && npm run lint && npm run test && npm run build"
|
|
50
|
+
},
|
|
51
|
+
"devDependencies": {
|
|
52
|
+
"@eslint/js": "^9.37.0",
|
|
53
|
+
"@types/node": "^24.7.0",
|
|
54
|
+
"esbuild": "^0.28.2",
|
|
55
|
+
"eslint": "^9.37.0",
|
|
56
|
+
"eslint-config-prettier": "^10.1.8",
|
|
57
|
+
"prettier": "^3.6.2",
|
|
58
|
+
"tsup": "^8.5.0",
|
|
59
|
+
"typescript": "^5.9.3",
|
|
60
|
+
"typescript-eslint": "^8.46.0",
|
|
61
|
+
"vitest": "^5.0.3"
|
|
62
|
+
},
|
|
63
|
+
"overrides": {
|
|
64
|
+
"tsup": {
|
|
65
|
+
"esbuild": "^0.28.2"
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|