@auth-gate/testing 0.9.2 → 0.10.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/README.md +182 -0
- package/dist/billing/index.cjs +282 -0
- package/dist/billing/index.d.cts +129 -0
- package/dist/billing/index.d.ts +129 -0
- package/dist/billing/index.mjs +252 -0
- package/dist/{chunk-OV3AFWVB.mjs → chunk-7QV2KZWA.mjs} +0 -8
- package/dist/chunk-MBTDPSN5.mjs +27 -0
- package/dist/cypress.mjs +2 -1
- package/dist/index.mjs +2 -1
- package/dist/playwright.mjs +4 -2
- package/dist/rbac/index.cjs +223 -0
- package/dist/rbac/index.d.cts +167 -0
- package/dist/rbac/index.d.ts +167 -0
- package/dist/rbac/index.mjs +180 -0
- package/icon.png +0 -0
- package/package.json +29 -6
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { BillingConfig, TypedBilling, PlanKey, FeatureKey } from '@auth-gate/billing';
|
|
2
|
+
export { computeDiff, priceConfigKey } from '@auth-gate/billing';
|
|
3
|
+
|
|
4
|
+
interface AdvanceOptions {
|
|
5
|
+
days?: number;
|
|
6
|
+
months?: number;
|
|
7
|
+
}
|
|
8
|
+
type BillingBoundary = "month_end" | "year_end";
|
|
9
|
+
declare class TestClock {
|
|
10
|
+
private _now;
|
|
11
|
+
constructor(startDate?: Date);
|
|
12
|
+
now(): Date;
|
|
13
|
+
advance(options: AdvanceOptions): Date;
|
|
14
|
+
/**
|
|
15
|
+
* Check if advancing from `before` to `after` crosses a billing boundary.
|
|
16
|
+
*/
|
|
17
|
+
crossesBoundary(before: Date, after: Date, boundary: BillingBoundary): boolean;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
interface Subscription {
|
|
21
|
+
id: string;
|
|
22
|
+
userId: string;
|
|
23
|
+
planKey: string;
|
|
24
|
+
priceIndex: number;
|
|
25
|
+
status: "active" | "canceled" | "past_due" | "trialing";
|
|
26
|
+
currentPeriodStart: Date;
|
|
27
|
+
currentPeriodEnd: Date;
|
|
28
|
+
cancelAtPeriodEnd: boolean;
|
|
29
|
+
createdAt: Date;
|
|
30
|
+
}
|
|
31
|
+
interface UsageRecord {
|
|
32
|
+
userId: string;
|
|
33
|
+
metric: string;
|
|
34
|
+
quantity: number;
|
|
35
|
+
timestamp: Date;
|
|
36
|
+
}
|
|
37
|
+
declare class InMemoryStore {
|
|
38
|
+
private subscriptions;
|
|
39
|
+
private usage;
|
|
40
|
+
private nextId;
|
|
41
|
+
createSubscription(params: {
|
|
42
|
+
userId: string;
|
|
43
|
+
planKey: string;
|
|
44
|
+
priceIndex?: number;
|
|
45
|
+
status?: Subscription["status"];
|
|
46
|
+
periodStart: Date;
|
|
47
|
+
periodEnd: Date;
|
|
48
|
+
}): Subscription;
|
|
49
|
+
getSubscription(id: string): Subscription | undefined;
|
|
50
|
+
getSubscriptionByUser(userId: string): Subscription | undefined;
|
|
51
|
+
updateSubscription(id: string, updates: Partial<Pick<Subscription, "planKey" | "priceIndex" | "status" | "cancelAtPeriodEnd" | "currentPeriodStart" | "currentPeriodEnd">>): Subscription | undefined;
|
|
52
|
+
listSubscriptions(filter?: {
|
|
53
|
+
status?: Subscription["status"];
|
|
54
|
+
planKey?: string;
|
|
55
|
+
}): Subscription[];
|
|
56
|
+
recordUsage(userId: string, metric: string, quantity: number, timestamp: Date): void;
|
|
57
|
+
getUsage(userId: string, metric: string, periodStart: Date, periodEnd: Date): number;
|
|
58
|
+
subscriberCount(planKey: string): number;
|
|
59
|
+
clear(): void;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
interface BooleanCheck {
|
|
63
|
+
type: "boolean";
|
|
64
|
+
feature: string;
|
|
65
|
+
allowed: boolean;
|
|
66
|
+
}
|
|
67
|
+
interface MeteredCheck {
|
|
68
|
+
type: "metered";
|
|
69
|
+
feature: string;
|
|
70
|
+
allowed: boolean;
|
|
71
|
+
used: number;
|
|
72
|
+
limit: number;
|
|
73
|
+
remaining: number;
|
|
74
|
+
}
|
|
75
|
+
type EntitlementCheck = BooleanCheck | MeteredCheck;
|
|
76
|
+
declare class EntitlementChecker {
|
|
77
|
+
private config;
|
|
78
|
+
private store;
|
|
79
|
+
constructor(config: BillingConfig, store: InMemoryStore);
|
|
80
|
+
check(userId: string, feature: string): EntitlementCheck;
|
|
81
|
+
checkAll(userId: string): Record<string, EntitlementCheck>;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
interface TestBillingOptions {
|
|
85
|
+
config: BillingConfig;
|
|
86
|
+
startDate?: Date;
|
|
87
|
+
}
|
|
88
|
+
interface TestBilling {
|
|
89
|
+
clock: TestClock;
|
|
90
|
+
store: InMemoryStore;
|
|
91
|
+
checker: EntitlementChecker;
|
|
92
|
+
subscribe(userId: string, planKey: string, priceIndex?: number): Subscription;
|
|
93
|
+
changePlan(userId: string, newPlanKey: string, priceIndex?: number): Subscription;
|
|
94
|
+
cancel(userId: string): Subscription;
|
|
95
|
+
reportUsage(userId: string, metric: string, quantity: number): void;
|
|
96
|
+
checkEntitlement(userId: string, feature: string): EntitlementCheck;
|
|
97
|
+
getSubscription(userId: string): Subscription | undefined;
|
|
98
|
+
advanceTime(options: {
|
|
99
|
+
days?: number;
|
|
100
|
+
months?: number;
|
|
101
|
+
}): Date;
|
|
102
|
+
subscriberCounts(): Record<string, number>;
|
|
103
|
+
}
|
|
104
|
+
interface TypedTestBillingOptions<B extends TypedBilling<any>> {
|
|
105
|
+
billing: B;
|
|
106
|
+
startDate?: Date;
|
|
107
|
+
}
|
|
108
|
+
interface TypedTestBilling<B extends TypedBilling<any>> {
|
|
109
|
+
clock: TestClock;
|
|
110
|
+
store: InMemoryStore;
|
|
111
|
+
checker: EntitlementChecker;
|
|
112
|
+
subscribe(userId: string, planKey: PlanKey<B>, priceIndex?: number): Subscription;
|
|
113
|
+
changePlan(userId: string, newPlanKey: PlanKey<B>, priceIndex?: number): Subscription;
|
|
114
|
+
cancel(userId: string): Subscription;
|
|
115
|
+
reportUsage(userId: string, metric: FeatureKey<B>, quantity: number): void;
|
|
116
|
+
checkEntitlement(userId: string, feature: FeatureKey<B>): EntitlementCheck;
|
|
117
|
+
getSubscription(userId: string): Subscription | undefined;
|
|
118
|
+
advanceTime(options: {
|
|
119
|
+
days?: number;
|
|
120
|
+
months?: number;
|
|
121
|
+
}): Date;
|
|
122
|
+
subscriberCounts(): Record<PlanKey<B>, number>;
|
|
123
|
+
}
|
|
124
|
+
/** Create typed test billing from a `defineBilling()` config. */
|
|
125
|
+
declare function createTestBilling<B extends TypedBilling<any>>(options: TypedTestBillingOptions<B>): TypedTestBilling<B>;
|
|
126
|
+
/** Create untyped test billing (legacy). */
|
|
127
|
+
declare function createTestBilling(options: TestBillingOptions): TestBilling;
|
|
128
|
+
|
|
129
|
+
export { type AdvanceOptions, type BillingBoundary, type BooleanCheck, type EntitlementCheck, EntitlementChecker, InMemoryStore, type MeteredCheck, type Subscription, type TestBilling, type TestBillingOptions, TestClock, type TypedTestBilling, type TypedTestBillingOptions, type UsageRecord, createTestBilling };
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
import "../chunk-MBTDPSN5.mjs";
|
|
2
|
+
|
|
3
|
+
// src/billing/clock.ts
|
|
4
|
+
var TestClock = class {
|
|
5
|
+
constructor(startDate) {
|
|
6
|
+
this._now = startDate ? new Date(startDate) : /* @__PURE__ */ new Date("2025-01-01T00:00:00Z");
|
|
7
|
+
}
|
|
8
|
+
now() {
|
|
9
|
+
return new Date(this._now);
|
|
10
|
+
}
|
|
11
|
+
advance(options) {
|
|
12
|
+
const next = new Date(this._now);
|
|
13
|
+
if (options.days) {
|
|
14
|
+
next.setUTCDate(next.getUTCDate() + options.days);
|
|
15
|
+
}
|
|
16
|
+
if (options.months) {
|
|
17
|
+
next.setUTCMonth(next.getUTCMonth() + options.months);
|
|
18
|
+
}
|
|
19
|
+
this._now = next;
|
|
20
|
+
return this.now();
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Check if advancing from `before` to `after` crosses a billing boundary.
|
|
24
|
+
*/
|
|
25
|
+
crossesBoundary(before, after, boundary) {
|
|
26
|
+
if (boundary === "month_end") {
|
|
27
|
+
return before.getUTCMonth() !== after.getUTCMonth() || before.getUTCFullYear() !== after.getUTCFullYear();
|
|
28
|
+
}
|
|
29
|
+
if (boundary === "year_end") {
|
|
30
|
+
return before.getUTCFullYear() !== after.getUTCFullYear();
|
|
31
|
+
}
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
// src/billing/store.ts
|
|
37
|
+
var InMemoryStore = class {
|
|
38
|
+
constructor() {
|
|
39
|
+
this.subscriptions = /* @__PURE__ */ new Map();
|
|
40
|
+
this.usage = [];
|
|
41
|
+
this.nextId = 1;
|
|
42
|
+
}
|
|
43
|
+
createSubscription(params) {
|
|
44
|
+
var _a, _b;
|
|
45
|
+
const id = `sub_${this.nextId++}`;
|
|
46
|
+
const sub = {
|
|
47
|
+
id,
|
|
48
|
+
userId: params.userId,
|
|
49
|
+
planKey: params.planKey,
|
|
50
|
+
priceIndex: (_a = params.priceIndex) != null ? _a : 0,
|
|
51
|
+
status: (_b = params.status) != null ? _b : "active",
|
|
52
|
+
currentPeriodStart: params.periodStart,
|
|
53
|
+
currentPeriodEnd: params.periodEnd,
|
|
54
|
+
cancelAtPeriodEnd: false,
|
|
55
|
+
createdAt: new Date(params.periodStart)
|
|
56
|
+
};
|
|
57
|
+
this.subscriptions.set(id, sub);
|
|
58
|
+
return sub;
|
|
59
|
+
}
|
|
60
|
+
getSubscription(id) {
|
|
61
|
+
return this.subscriptions.get(id);
|
|
62
|
+
}
|
|
63
|
+
getSubscriptionByUser(userId) {
|
|
64
|
+
for (const sub of this.subscriptions.values()) {
|
|
65
|
+
if (sub.userId === userId && sub.status !== "canceled") return sub;
|
|
66
|
+
}
|
|
67
|
+
return void 0;
|
|
68
|
+
}
|
|
69
|
+
updateSubscription(id, updates) {
|
|
70
|
+
const sub = this.subscriptions.get(id);
|
|
71
|
+
if (!sub) return void 0;
|
|
72
|
+
Object.assign(sub, updates);
|
|
73
|
+
return sub;
|
|
74
|
+
}
|
|
75
|
+
listSubscriptions(filter) {
|
|
76
|
+
let subs = [...this.subscriptions.values()];
|
|
77
|
+
if (filter == null ? void 0 : filter.status) subs = subs.filter((s) => s.status === filter.status);
|
|
78
|
+
if (filter == null ? void 0 : filter.planKey) subs = subs.filter((s) => s.planKey === filter.planKey);
|
|
79
|
+
return subs;
|
|
80
|
+
}
|
|
81
|
+
recordUsage(userId, metric, quantity, timestamp) {
|
|
82
|
+
this.usage.push({ userId, metric, quantity, timestamp });
|
|
83
|
+
}
|
|
84
|
+
getUsage(userId, metric, periodStart, periodEnd) {
|
|
85
|
+
return this.usage.filter(
|
|
86
|
+
(r) => r.userId === userId && r.metric === metric && r.timestamp >= periodStart && r.timestamp < periodEnd
|
|
87
|
+
).reduce((sum, r) => sum + r.quantity, 0);
|
|
88
|
+
}
|
|
89
|
+
subscriberCount(planKey) {
|
|
90
|
+
return this.listSubscriptions({ planKey, status: "active" }).length;
|
|
91
|
+
}
|
|
92
|
+
clear() {
|
|
93
|
+
this.subscriptions.clear();
|
|
94
|
+
this.usage = [];
|
|
95
|
+
this.nextId = 1;
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
// src/billing/entitlements.ts
|
|
100
|
+
var EntitlementChecker = class {
|
|
101
|
+
constructor(config, store) {
|
|
102
|
+
this.config = config;
|
|
103
|
+
this.store = store;
|
|
104
|
+
}
|
|
105
|
+
check(userId, feature) {
|
|
106
|
+
var _a;
|
|
107
|
+
const sub = this.store.getSubscriptionByUser(userId);
|
|
108
|
+
if (!sub) {
|
|
109
|
+
return { type: "boolean", feature, allowed: false };
|
|
110
|
+
}
|
|
111
|
+
const plan = this.config.plans[sub.planKey];
|
|
112
|
+
if (!plan) {
|
|
113
|
+
return { type: "boolean", feature, allowed: false };
|
|
114
|
+
}
|
|
115
|
+
if (plan.entitlements) {
|
|
116
|
+
const value = plan.entitlements[feature];
|
|
117
|
+
if (value === void 0) {
|
|
118
|
+
return { type: "boolean", feature, allowed: false };
|
|
119
|
+
}
|
|
120
|
+
if (value === true) {
|
|
121
|
+
return { type: "boolean", feature, allowed: true };
|
|
122
|
+
}
|
|
123
|
+
if (typeof value === "object" && "limit" in value) {
|
|
124
|
+
const used = this.store.getUsage(
|
|
125
|
+
userId,
|
|
126
|
+
feature,
|
|
127
|
+
sub.currentPeriodStart,
|
|
128
|
+
sub.currentPeriodEnd
|
|
129
|
+
);
|
|
130
|
+
const limit = value.limit;
|
|
131
|
+
return {
|
|
132
|
+
type: "metered",
|
|
133
|
+
feature,
|
|
134
|
+
allowed: used < limit,
|
|
135
|
+
used,
|
|
136
|
+
limit,
|
|
137
|
+
remaining: Math.max(0, limit - used)
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
if ((_a = plan.features) == null ? void 0 : _a.includes(feature)) {
|
|
142
|
+
return { type: "boolean", feature, allowed: true };
|
|
143
|
+
}
|
|
144
|
+
return { type: "boolean", feature, allowed: false };
|
|
145
|
+
}
|
|
146
|
+
checkAll(userId) {
|
|
147
|
+
const sub = this.store.getSubscriptionByUser(userId);
|
|
148
|
+
if (!sub) return {};
|
|
149
|
+
const plan = this.config.plans[sub.planKey];
|
|
150
|
+
if (!plan) return {};
|
|
151
|
+
const result = {};
|
|
152
|
+
if (plan.entitlements) {
|
|
153
|
+
for (const feature of Object.keys(plan.entitlements)) {
|
|
154
|
+
result[feature] = this.check(userId, feature);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
if (plan.features) {
|
|
158
|
+
for (const feature of plan.features) {
|
|
159
|
+
if (!result[feature]) {
|
|
160
|
+
result[feature] = { type: "boolean", feature, allowed: true };
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
return result;
|
|
165
|
+
}
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
// src/billing/test-billing.ts
|
|
169
|
+
function getPeriodEnd(start, interval) {
|
|
170
|
+
const end = new Date(start);
|
|
171
|
+
if (interval === "monthly") {
|
|
172
|
+
end.setUTCMonth(end.getUTCMonth() + 1);
|
|
173
|
+
} else {
|
|
174
|
+
end.setUTCFullYear(end.getUTCFullYear() + 1);
|
|
175
|
+
}
|
|
176
|
+
return end;
|
|
177
|
+
}
|
|
178
|
+
function buildTestBilling(config, startDate) {
|
|
179
|
+
const clock = new TestClock(startDate);
|
|
180
|
+
const store = new InMemoryStore();
|
|
181
|
+
const checker = new EntitlementChecker(config, store);
|
|
182
|
+
return {
|
|
183
|
+
clock,
|
|
184
|
+
store,
|
|
185
|
+
checker,
|
|
186
|
+
subscribe(userId, planKey, priceIndex = 0) {
|
|
187
|
+
const plan = config.plans[planKey];
|
|
188
|
+
if (!plan) throw new Error(`Plan "${planKey}" not found in config`);
|
|
189
|
+
if (!plan.prices[priceIndex]) throw new Error(`Price index ${priceIndex} not found on plan "${planKey}"`);
|
|
190
|
+
const existing = store.getSubscriptionByUser(userId);
|
|
191
|
+
if (existing) throw new Error(`User "${userId}" already has an active subscription (${existing.planKey})`);
|
|
192
|
+
const now = clock.now();
|
|
193
|
+
const interval = plan.prices[priceIndex].interval;
|
|
194
|
+
return store.createSubscription({
|
|
195
|
+
userId,
|
|
196
|
+
planKey,
|
|
197
|
+
priceIndex,
|
|
198
|
+
periodStart: now,
|
|
199
|
+
periodEnd: getPeriodEnd(now, interval)
|
|
200
|
+
});
|
|
201
|
+
},
|
|
202
|
+
changePlan(userId, newPlanKey, priceIndex = 0) {
|
|
203
|
+
const sub = store.getSubscriptionByUser(userId);
|
|
204
|
+
if (!sub) throw new Error(`User "${userId}" has no active subscription`);
|
|
205
|
+
const plan = config.plans[newPlanKey];
|
|
206
|
+
if (!plan) throw new Error(`Plan "${newPlanKey}" not found in config`);
|
|
207
|
+
if (!plan.prices[priceIndex]) throw new Error(`Price index ${priceIndex} not found on plan "${newPlanKey}"`);
|
|
208
|
+
store.updateSubscription(sub.id, { planKey: newPlanKey, priceIndex });
|
|
209
|
+
return store.getSubscription(sub.id);
|
|
210
|
+
},
|
|
211
|
+
cancel(userId) {
|
|
212
|
+
const sub = store.getSubscriptionByUser(userId);
|
|
213
|
+
if (!sub) throw new Error(`User "${userId}" has no active subscription`);
|
|
214
|
+
store.updateSubscription(sub.id, { cancelAtPeriodEnd: true });
|
|
215
|
+
return store.getSubscription(sub.id);
|
|
216
|
+
},
|
|
217
|
+
reportUsage(userId, metric, quantity) {
|
|
218
|
+
store.recordUsage(userId, metric, quantity, clock.now());
|
|
219
|
+
},
|
|
220
|
+
checkEntitlement(userId, feature) {
|
|
221
|
+
return checker.check(userId, feature);
|
|
222
|
+
},
|
|
223
|
+
getSubscription(userId) {
|
|
224
|
+
return store.getSubscriptionByUser(userId);
|
|
225
|
+
},
|
|
226
|
+
advanceTime(options) {
|
|
227
|
+
return clock.advance(options);
|
|
228
|
+
},
|
|
229
|
+
subscriberCounts() {
|
|
230
|
+
const counts = {};
|
|
231
|
+
for (const planKey of Object.keys(config.plans)) {
|
|
232
|
+
counts[planKey] = store.subscriberCount(planKey);
|
|
233
|
+
}
|
|
234
|
+
return counts;
|
|
235
|
+
}
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
function createTestBilling(options) {
|
|
239
|
+
const config = "billing" in options ? options.billing._config : options.config;
|
|
240
|
+
return buildTestBilling(config, options.startDate);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// src/billing/index.ts
|
|
244
|
+
import { computeDiff, priceConfigKey } from "@auth-gate/billing";
|
|
245
|
+
export {
|
|
246
|
+
EntitlementChecker,
|
|
247
|
+
InMemoryStore,
|
|
248
|
+
TestClock,
|
|
249
|
+
computeDiff,
|
|
250
|
+
createTestBilling,
|
|
251
|
+
priceConfigKey
|
|
252
|
+
};
|
|
@@ -1,10 +1,3 @@
|
|
|
1
|
-
var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
|
|
2
|
-
get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
|
|
3
|
-
}) : x)(function(x) {
|
|
4
|
-
if (typeof require !== "undefined") return require.apply(this, arguments);
|
|
5
|
-
throw Error('Dynamic require of "' + x + '" is not supported');
|
|
6
|
-
});
|
|
7
|
-
|
|
8
1
|
// src/constants.ts
|
|
9
2
|
var TEST_EMAIL_DOMAIN = "test.authgate.dev";
|
|
10
3
|
var TEST_OTP_CODE = "424242";
|
|
@@ -170,7 +163,6 @@ var AuthGateTest = class {
|
|
|
170
163
|
};
|
|
171
164
|
|
|
172
165
|
export {
|
|
173
|
-
__require,
|
|
174
166
|
TEST_EMAIL_DOMAIN,
|
|
175
167
|
TEST_OTP_CODE,
|
|
176
168
|
AuthGateTest
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
var __defProp = Object.defineProperty;
|
|
2
|
+
var __getOwnPropSymbols = Object.getOwnPropertySymbols;
|
|
3
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
4
|
+
var __propIsEnum = Object.prototype.propertyIsEnumerable;
|
|
5
|
+
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
|
|
6
|
+
var __spreadValues = (a, b) => {
|
|
7
|
+
for (var prop in b || (b = {}))
|
|
8
|
+
if (__hasOwnProp.call(b, prop))
|
|
9
|
+
__defNormalProp(a, prop, b[prop]);
|
|
10
|
+
if (__getOwnPropSymbols)
|
|
11
|
+
for (var prop of __getOwnPropSymbols(b)) {
|
|
12
|
+
if (__propIsEnum.call(b, prop))
|
|
13
|
+
__defNormalProp(a, prop, b[prop]);
|
|
14
|
+
}
|
|
15
|
+
return a;
|
|
16
|
+
};
|
|
17
|
+
var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
|
|
18
|
+
get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
|
|
19
|
+
}) : x)(function(x) {
|
|
20
|
+
if (typeof require !== "undefined") return require.apply(this, arguments);
|
|
21
|
+
throw Error('Dynamic require of "' + x + '" is not supported');
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
export {
|
|
25
|
+
__spreadValues,
|
|
26
|
+
__require
|
|
27
|
+
};
|
package/dist/cypress.mjs
CHANGED
package/dist/index.mjs
CHANGED
package/dist/playwright.mjs
CHANGED
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __getOwnPropSymbols = Object.getOwnPropertySymbols;
|
|
6
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
7
|
+
var __propIsEnum = Object.prototype.propertyIsEnumerable;
|
|
8
|
+
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
|
|
9
|
+
var __spreadValues = (a, b) => {
|
|
10
|
+
for (var prop in b || (b = {}))
|
|
11
|
+
if (__hasOwnProp.call(b, prop))
|
|
12
|
+
__defNormalProp(a, prop, b[prop]);
|
|
13
|
+
if (__getOwnPropSymbols)
|
|
14
|
+
for (var prop of __getOwnPropSymbols(b)) {
|
|
15
|
+
if (__propIsEnum.call(b, prop))
|
|
16
|
+
__defNormalProp(a, prop, b[prop]);
|
|
17
|
+
}
|
|
18
|
+
return a;
|
|
19
|
+
};
|
|
20
|
+
var __export = (target, all) => {
|
|
21
|
+
for (var name in all)
|
|
22
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
23
|
+
};
|
|
24
|
+
var __copyProps = (to, from, except, desc) => {
|
|
25
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
26
|
+
for (let key of __getOwnPropNames(from))
|
|
27
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
28
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
29
|
+
}
|
|
30
|
+
return to;
|
|
31
|
+
};
|
|
32
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
33
|
+
|
|
34
|
+
// src/rbac/index.ts
|
|
35
|
+
var rbac_exports = {};
|
|
36
|
+
__export(rbac_exports, {
|
|
37
|
+
RbacChecker: () => RbacChecker,
|
|
38
|
+
createTestRbacConfig: () => createTestRbacConfig,
|
|
39
|
+
createTestResource: () => createTestResource,
|
|
40
|
+
createTestRole: () => createTestRole,
|
|
41
|
+
expectNoPermission: () => expectNoPermission,
|
|
42
|
+
expectPermission: () => expectPermission,
|
|
43
|
+
expectRolePermissions: () => expectRolePermissions
|
|
44
|
+
});
|
|
45
|
+
module.exports = __toCommonJS(rbac_exports);
|
|
46
|
+
|
|
47
|
+
// src/rbac/builders.ts
|
|
48
|
+
function createTestRbacConfig(overrides) {
|
|
49
|
+
return __spreadValues({
|
|
50
|
+
resources: {
|
|
51
|
+
documents: { actions: ["read", "write", "delete"] }
|
|
52
|
+
},
|
|
53
|
+
roles: {
|
|
54
|
+
admin: {
|
|
55
|
+
name: "Admin",
|
|
56
|
+
grants: {
|
|
57
|
+
documents: { read: true, write: true, delete: true }
|
|
58
|
+
}
|
|
59
|
+
},
|
|
60
|
+
viewer: {
|
|
61
|
+
name: "Viewer",
|
|
62
|
+
isDefault: true,
|
|
63
|
+
grants: {
|
|
64
|
+
documents: { read: true }
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}, overrides);
|
|
69
|
+
}
|
|
70
|
+
function createTestResource(key, actions, overrides) {
|
|
71
|
+
return {
|
|
72
|
+
[key]: __spreadValues({
|
|
73
|
+
actions
|
|
74
|
+
}, overrides)
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
function createTestRole(key, name, grants, overrides) {
|
|
78
|
+
return {
|
|
79
|
+
[key]: __spreadValues({
|
|
80
|
+
name,
|
|
81
|
+
grants
|
|
82
|
+
}, overrides)
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// src/rbac/checker.ts
|
|
87
|
+
var RbacChecker = class {
|
|
88
|
+
/**
|
|
89
|
+
* @param input - Either a plain `RbacConfig` object, or a `TypedRbac`
|
|
90
|
+
* instance (from `defineRbac()`) which exposes `_config`.
|
|
91
|
+
*/
|
|
92
|
+
constructor(input) {
|
|
93
|
+
this.config = "_config" in input ? input._config : input;
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Check whether a role has a specific permission.
|
|
97
|
+
*
|
|
98
|
+
* @param roleKey - The role key (e.g. `"admin"`).
|
|
99
|
+
* @param permission - A `"resource:action"` string (e.g. `"documents:read"`).
|
|
100
|
+
* @returns `true` if the role (or any ancestor via inheritance) grants the permission.
|
|
101
|
+
*/
|
|
102
|
+
hasPermission(roleKey, permission) {
|
|
103
|
+
return this.getPermissions(roleKey).has(permission);
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Collect all permissions for a role, including inherited ones.
|
|
107
|
+
*
|
|
108
|
+
* @param roleKey - The role key.
|
|
109
|
+
* @returns A `Set<string>` of `"resource:action"` strings.
|
|
110
|
+
*/
|
|
111
|
+
getPermissions(roleKey) {
|
|
112
|
+
const permissions = /* @__PURE__ */ new Set();
|
|
113
|
+
this.collectPermissions(roleKey, permissions, /* @__PURE__ */ new Set());
|
|
114
|
+
return permissions;
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Find all roles that grant a specific permission.
|
|
118
|
+
*
|
|
119
|
+
* @param permission - A `"resource:action"` string.
|
|
120
|
+
* @returns Array of role keys that have the permission (directly or via inheritance).
|
|
121
|
+
*/
|
|
122
|
+
getRolesWithPermission(permission) {
|
|
123
|
+
const result = [];
|
|
124
|
+
for (const roleKey of Object.keys(this.config.roles)) {
|
|
125
|
+
if (this.hasPermission(roleKey, permission)) {
|
|
126
|
+
result.push(roleKey);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
return result;
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Recursively collect permissions for a role, following `inherits` chains.
|
|
133
|
+
* Uses a `visited` set to prevent infinite loops from circular inheritance.
|
|
134
|
+
*/
|
|
135
|
+
collectPermissions(roleKey, permissions, visited) {
|
|
136
|
+
if (visited.has(roleKey)) return;
|
|
137
|
+
visited.add(roleKey);
|
|
138
|
+
const role = this.config.roles[roleKey];
|
|
139
|
+
if (!role) return;
|
|
140
|
+
for (const [resource, actions] of Object.entries(role.grants)) {
|
|
141
|
+
for (const [action, value] of Object.entries(
|
|
142
|
+
actions
|
|
143
|
+
)) {
|
|
144
|
+
if (isGranted(value)) {
|
|
145
|
+
permissions.add(`${resource}:${action}`);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
if (role.inherits) {
|
|
150
|
+
for (const parentKey of role.inherits) {
|
|
151
|
+
this.collectPermissions(parentKey, permissions, visited);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
};
|
|
156
|
+
function isGranted(value) {
|
|
157
|
+
if (value === true) return true;
|
|
158
|
+
if (typeof value === "string") return true;
|
|
159
|
+
if (typeof value === "object" && value !== null && "when" in value) return true;
|
|
160
|
+
return false;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// src/rbac/assertions.ts
|
|
164
|
+
function toChecker(input) {
|
|
165
|
+
return new RbacChecker(input);
|
|
166
|
+
}
|
|
167
|
+
function expectPermission(rbac, roleKey, permission) {
|
|
168
|
+
const checker = toChecker(rbac);
|
|
169
|
+
if (!checker.hasPermission(roleKey, permission)) {
|
|
170
|
+
const actual = checker.getPermissions(roleKey);
|
|
171
|
+
const permList = actual.size > 0 ? Array.from(actual).sort().join(", ") : "(none)";
|
|
172
|
+
throw new Error(
|
|
173
|
+
`Expected role "${roleKey}" to have permission "${permission}", but it does not. Actual permissions: [${permList}]`
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
function expectNoPermission(rbac, roleKey, permission) {
|
|
178
|
+
const checker = toChecker(rbac);
|
|
179
|
+
if (checker.hasPermission(roleKey, permission)) {
|
|
180
|
+
throw new Error(
|
|
181
|
+
`Expected role "${roleKey}" NOT to have permission "${permission}", but it does.`
|
|
182
|
+
);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
function expectRolePermissions(rbac, roleKey, expected) {
|
|
186
|
+
const checker = toChecker(rbac);
|
|
187
|
+
const actual = checker.getPermissions(roleKey);
|
|
188
|
+
const expectedSet = new Set(expected);
|
|
189
|
+
const missing = [];
|
|
190
|
+
const extra = [];
|
|
191
|
+
for (const perm of expectedSet) {
|
|
192
|
+
if (!actual.has(perm)) {
|
|
193
|
+
missing.push(perm);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
for (const perm of actual) {
|
|
197
|
+
if (!expectedSet.has(perm)) {
|
|
198
|
+
extra.push(perm);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
if (missing.length > 0 || extra.length > 0) {
|
|
202
|
+
const parts = [];
|
|
203
|
+
if (missing.length > 0) {
|
|
204
|
+
parts.push(`Missing: [${missing.sort().join(", ")}]`);
|
|
205
|
+
}
|
|
206
|
+
if (extra.length > 0) {
|
|
207
|
+
parts.push(`Extra: [${extra.sort().join(", ")}]`);
|
|
208
|
+
}
|
|
209
|
+
throw new Error(
|
|
210
|
+
`Role "${roleKey}" permissions do not match expected set. ${parts.join(". ")}`
|
|
211
|
+
);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
215
|
+
0 && (module.exports = {
|
|
216
|
+
RbacChecker,
|
|
217
|
+
createTestRbacConfig,
|
|
218
|
+
createTestResource,
|
|
219
|
+
createTestRole,
|
|
220
|
+
expectNoPermission,
|
|
221
|
+
expectPermission,
|
|
222
|
+
expectRolePermissions
|
|
223
|
+
});
|