@mesub/node 0.0.0-stage → 0.1.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/LICENSE +21 -0
- package/README.md +835 -2
- package/dist/chunk-43Q4PNF7.cjs +480 -0
- package/dist/chunk-43Q4PNF7.cjs.map +1 -0
- package/dist/chunk-ELK7IBLO.js +340 -0
- package/dist/chunk-ELK7IBLO.js.map +1 -0
- package/dist/chunk-JJWHMB2X.js +480 -0
- package/dist/chunk-JJWHMB2X.js.map +1 -0
- package/dist/chunk-KMPBFNJE.js +2038 -0
- package/dist/chunk-KMPBFNJE.js.map +1 -0
- package/dist/chunk-PCDZI7B4.cjs +340 -0
- package/dist/chunk-PCDZI7B4.cjs.map +1 -0
- package/dist/chunk-SGSYQJYT.cjs +2038 -0
- package/dist/chunk-SGSYQJYT.cjs.map +1 -0
- package/dist/chunk-VMKI4VVA.cjs +120 -0
- package/dist/chunk-VMKI4VVA.cjs.map +1 -0
- package/dist/chunk-XDLLXY7Y.js +120 -0
- package/dist/chunk-XDLLXY7Y.js.map +1 -0
- package/dist/client-BQuRYSdB.d.ts +759 -0
- package/dist/client-Dm1jamju.d.cts +759 -0
- package/dist/errors-CtPucyOh.d.cts +112 -0
- package/dist/errors-DqW3DMBS.d.ts +112 -0
- package/dist/express.cjs +129 -0
- package/dist/express.cjs.map +1 -0
- package/dist/express.d.cts +57 -0
- package/dist/express.d.ts +57 -0
- package/dist/express.js +129 -0
- package/dist/express.js.map +1 -0
- package/dist/guard-DKb6iU1z.d.ts +51 -0
- package/dist/guard-Dhy4fI6n.d.cts +51 -0
- package/dist/index.cjs +39 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +44 -0
- package/dist/index.d.ts +44 -0
- package/dist/index.js +39 -0
- package/dist/index.js.map +1 -0
- package/dist/nest.cjs +62 -0
- package/dist/nest.cjs.map +1 -0
- package/dist/nest.d.cts +57 -0
- package/dist/nest.d.ts +57 -0
- package/dist/nest.js +62 -0
- package/dist/nest.js.map +1 -0
- package/dist/next.cjs +113 -0
- package/dist/next.cjs.map +1 -0
- package/dist/next.d.cts +63 -0
- package/dist/next.d.ts +63 -0
- package/dist/next.js +113 -0
- package/dist/next.js.map +1 -0
- package/dist/routes-CLtZnUzl.d.ts +130 -0
- package/dist/routes-DWkRFOP0.d.cts +130 -0
- package/dist/situations.cjs +15 -0
- package/dist/situations.cjs.map +1 -0
- package/dist/situations.d.cts +607 -0
- package/dist/situations.d.ts +607 -0
- package/dist/situations.js +15 -0
- package/dist/situations.js.map +1 -0
- package/dist/subscriptions-D1hE7MUc.d.cts +768 -0
- package/dist/subscriptions-D1hE7MUc.d.ts +768 -0
- package/dist/testing.cjs +1486 -0
- package/dist/testing.cjs.map +1 -0
- package/dist/testing.d.cts +331 -0
- package/dist/testing.d.ts +331 -0
- package/dist/testing.js +1486 -0
- package/dist/testing.js.map +1 -0
- package/package.json +158 -4
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Mesub,
|
|
3
|
+
customerOf
|
|
4
|
+
} from "./chunk-KMPBFNJE.js";
|
|
5
|
+
|
|
6
|
+
// src/guard.ts
|
|
7
|
+
var DENIAL_STATUS = {
|
|
8
|
+
unauthenticated: 401,
|
|
9
|
+
no_access: 402,
|
|
10
|
+
unavailable: 503
|
|
11
|
+
};
|
|
12
|
+
var UNAVAILABLE_RETRY_AFTER_S = 30;
|
|
13
|
+
function retryAfterOf(outcome) {
|
|
14
|
+
const asked = outcome.decision?.retryAfter;
|
|
15
|
+
return String(asked === void 0 ? UNAVAILABLE_RETRY_AFTER_S : Math.ceil(asked / 1e3));
|
|
16
|
+
}
|
|
17
|
+
var shared;
|
|
18
|
+
function defaultClient() {
|
|
19
|
+
shared ??= new Mesub();
|
|
20
|
+
return shared;
|
|
21
|
+
}
|
|
22
|
+
function plansOf(plan, request) {
|
|
23
|
+
return checkedPlans(typeof plan === "function" ? plan(request) : plan);
|
|
24
|
+
}
|
|
25
|
+
function checkPlan(plan) {
|
|
26
|
+
if (typeof plan !== "function") checkedPlans(plan);
|
|
27
|
+
}
|
|
28
|
+
var MAX_PLANS = 3;
|
|
29
|
+
function checkedPlans(value) {
|
|
30
|
+
const plans = typeof value === "string" ? [value] : Array.isArray(value) ? value : [];
|
|
31
|
+
if (plans.length === 0 || !plans.every((plan) => typeof plan === "string" && plan !== "")) {
|
|
32
|
+
throw new TypeError("A guard needs a plan, or a list of plans, as non-empty strings.");
|
|
33
|
+
}
|
|
34
|
+
const unique = [...new Set(plans)];
|
|
35
|
+
if (unique.length > MAX_PLANS) {
|
|
36
|
+
throw new TypeError(
|
|
37
|
+
`A guard asks about ${MAX_PLANS} plans at most: each one is a call to Mesub on every request.`
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
return unique;
|
|
41
|
+
}
|
|
42
|
+
function checkCustomer(options) {
|
|
43
|
+
if (typeof options?.customer !== "function") {
|
|
44
|
+
throw new TypeError(
|
|
45
|
+
"A guard needs `customer`: a function of the request returning who is signed in, e.g. `(req) => ({ external_id: req.user.id })`."
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
async function askerOf(request, customer) {
|
|
50
|
+
const named = await customer(request);
|
|
51
|
+
if (named === null || named === void 0) return null;
|
|
52
|
+
const asked = customerOf(named);
|
|
53
|
+
if (asked.value === "") {
|
|
54
|
+
throw new TypeError(
|
|
55
|
+
"`customer` returned an empty value: return null when nobody is signed in."
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
return asked;
|
|
59
|
+
}
|
|
60
|
+
function asCustomer(asked) {
|
|
61
|
+
if (asked.kind === "wallet") return asked.value;
|
|
62
|
+
return asked.kind === "email" ? { email: asked.value } : { external_id: asked.value };
|
|
63
|
+
}
|
|
64
|
+
async function guard(client, asked, plansFor) {
|
|
65
|
+
if (!asked) return { allowed: false, reason: "unauthenticated" };
|
|
66
|
+
const plans = plansFor();
|
|
67
|
+
const customer = asCustomer(asked);
|
|
68
|
+
const pending = plans.map((plan) => client.decide(customer, plan));
|
|
69
|
+
for (const decision of pending) decision.catch(() => void 0);
|
|
70
|
+
let first;
|
|
71
|
+
let unavailable;
|
|
72
|
+
for (const [index, next] of pending.entries()) {
|
|
73
|
+
const decision = await next;
|
|
74
|
+
if (decision.access) {
|
|
75
|
+
return { allowed: true, customer: asked, plan: plans[index], decision };
|
|
76
|
+
}
|
|
77
|
+
first ??= decision;
|
|
78
|
+
if (decision.unavailable) unavailable ??= decision;
|
|
79
|
+
}
|
|
80
|
+
if (unavailable) return { allowed: false, reason: "unavailable", decision: unavailable };
|
|
81
|
+
return { allowed: false, reason: "no_access", ...first ? { decision: first } : {} };
|
|
82
|
+
}
|
|
83
|
+
function denialBody(outcome) {
|
|
84
|
+
return {
|
|
85
|
+
access: false,
|
|
86
|
+
reason: outcome.reason,
|
|
87
|
+
...outcome.decision?.answer ? { status: outcome.decision.answer.status } : {}
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
function accessOf(outcome) {
|
|
91
|
+
const { customer } = outcome;
|
|
92
|
+
return {
|
|
93
|
+
wallet: customer.kind === "wallet" ? customer.value : outcome.decision.answer?.wallet ?? null,
|
|
94
|
+
customer,
|
|
95
|
+
plan: outcome.plan,
|
|
96
|
+
answer: outcome.decision.answer,
|
|
97
|
+
stale: outcome.decision.stale
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
function denialOf(outcome) {
|
|
101
|
+
return {
|
|
102
|
+
reason: outcome.reason,
|
|
103
|
+
status: DENIAL_STATUS[outcome.reason],
|
|
104
|
+
answer: outcome.decision?.answer ?? null
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export {
|
|
109
|
+
retryAfterOf,
|
|
110
|
+
defaultClient,
|
|
111
|
+
plansOf,
|
|
112
|
+
checkPlan,
|
|
113
|
+
checkCustomer,
|
|
114
|
+
askerOf,
|
|
115
|
+
guard,
|
|
116
|
+
denialBody,
|
|
117
|
+
accessOf,
|
|
118
|
+
denialOf
|
|
119
|
+
};
|
|
120
|
+
//# sourceMappingURL=chunk-XDLLXY7Y.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/guard.ts"],"sourcesContent":["import type { AccessAnswer, Customer } from './answer.js';\nimport { type Decision, Mesub } from './client.js';\nimport { type Asked, customerOf } from './customer.js';\n\n/** Why a request was turned away, and what it answers by default. */\nexport type DenialReason = 'unauthenticated' | 'no_access' | 'unavailable';\n\nexport type GuardOutcome =\n | { allowed: true; customer: Asked; plan: string; decision: Decision }\n | { allowed: false; reason: DenialReason; decision?: Decision };\n\n/** Who is asking and what Mesub answered, as a guarded route receives it. */\nexport interface MesubAccess {\n /**\n * The wallet that pays. Asked by external id or email, the one Mesub\n * answered with; null when the answer is the outage fallback's and holds none.\n */\n wallet: string | null;\n /** Who Mesub was asked about: a wallet, your own id for them, or their email. */\n customer: Asked;\n /** The plan that let the request through: the first of the list that did. */\n plan: string;\n /** Mesub's answer for that plan. */\n answer: AccessAnswer | null;\n /** The answer came from the outage fallback. */\n stale: boolean;\n}\n\n/** A refusal, as `onDenied` receives it. */\nexport interface Denial {\n reason: DenialReason;\n /** 401, 402 or 503: what would be answered without `onDenied`. */\n status: number;\n /** Mesub's answer, for the first plan asked when there are several. */\n answer: AccessAnswer | null;\n}\n\n/** What a guard answers by default for each refusal. */\nexport const DENIAL_STATUS: Record<DenialReason, number> = {\n unauthenticated: 401,\n no_access: 402,\n unavailable: 503,\n};\n\n/**\n * Seconds a client should wait before asking again when Mesub is unreachable\n * and did not say how long itself.\n */\nexport const UNAVAILABLE_RETRY_AFTER_S = 30;\n\n/**\n * The `Retry-After` of a guard's 503, in seconds: the one Mesub sent with its\n * 429 or 503 when it sent one, `UNAVAILABLE_RETRY_AFTER_S` otherwise (#132).\n */\nexport function retryAfterOf(outcome: Extract<GuardOutcome, { allowed: false }>): string {\n const asked = outcome.decision?.retryAfter;\n\n return String(asked === undefined ? UNAVAILABLE_RETRY_AFTER_S : Math.ceil(asked / 1000));\n}\n\nlet shared: Mesub | undefined;\n\n/** The client a middleware uses when given none: built once, from MESUB_API_KEY. */\nexport function defaultClient(): Mesub {\n shared ??= new Mesub();\n\n return shared;\n}\n\n/**\n * The plan a guard asks about: one, any one of several (the first that grants\n * lets the request through), or either of those worked out per request.\n */\nexport type PlanOption<Req> =\n string | readonly string[] | ((request: Req) => string | readonly string[]);\n\n/**\n * The plans a guard asks about for that request, in order, each once. A\n * plan that is not a non-empty string, or no plan at all, is a broken\n * integration: thrown, never read as a refusal.\n */\nexport function plansOf<Req>(plan: PlanOption<Req>, request: Req): string[] {\n return checkedPlans(typeof plan === 'function' ? plan(request) : plan);\n}\n\n/**\n * Checks a fixed plan when the guard is built, so a typo fails at start-up\n * rather than on the first request. One worked out per request is checked\n * then, by `plansOf`.\n */\nexport function checkPlan<Req>(plan: PlanOption<Req>): void {\n if (typeof plan !== 'function') checkedPlans(plan);\n}\n\n/**\n * Each plan a guard asks about is one call to `/v1/access`, made for every\n * request, against the key's 1000 calls a minute. Three is what a Dev\n * project can hold; a list longer than that is more likely built from the\n * request than written by hand.\n */\nconst MAX_PLANS = 3;\n\nfunction checkedPlans(value: unknown): string[] {\n const plans: unknown[] =\n typeof value === 'string' ? [value] : Array.isArray(value) ? value : [];\n\n if (plans.length === 0 || !plans.every((plan) => typeof plan === 'string' && plan !== '')) {\n throw new TypeError('A guard needs a plan, or a list of plans, as non-empty strings.');\n }\n\n const unique = [...new Set(plans as string[])];\n\n if (unique.length > MAX_PLANS) {\n throw new TypeError(\n `A guard asks about ${MAX_PLANS} plans at most: each one is a call to Mesub on every request.`,\n );\n }\n\n return unique;\n}\n\n/**\n * Who is asking, from your own auth: `(req) => ({ external_id: req.user.id })`,\n * a wallet, or an email. Null or undefined when nobody is signed in, a 401.\n * May be async.\n *\n * It must come from a session you verified, never from the request itself (a\n * query, a body, a header the caller writes): whoever names a subscriber\n * would get their access.\n */\nexport type CustomerOption<Req> = (\n request: Req,\n) => Customer | string | null | undefined | Promise<Customer | string | null | undefined>;\n\n/** Checked when the guard is built: without `customer` it could not tell who is asking. */\nexport function checkCustomer<Req>(options: { customer: CustomerOption<Req> }): void {\n if (typeof options?.customer !== 'function') {\n throw new TypeError(\n 'A guard needs `customer`: a function of the request returning who is signed in, ' +\n 'e.g. `(req) => ({ external_id: req.user.id })`.',\n );\n }\n}\n\n/**\n * Who is asking, as `customer` names them, or null for nobody.\n *\n * A `customer` that returns something Mesub cannot be asked about (two\n * identifiers, an empty one, not a string) is a broken integration: thrown.\n */\nexport async function askerOf<Req>(\n request: Req,\n customer: CustomerOption<Req>,\n): Promise<Asked | null> {\n const named = await customer(request);\n\n if (named === null || named === undefined) return null;\n\n const asked = customerOf(named);\n\n if (asked.value === '') {\n throw new TypeError(\n '`customer` returned an empty value: return null when nobody is signed in.',\n );\n }\n\n return asked;\n}\n\n/** The customer as `decide` takes it. */\nfunction asCustomer(asked: Asked): Customer | string {\n if (asked.kind === 'wallet') return asked.value;\n\n return asked.kind === 'email' ? { email: asked.value } : { external_id: asked.value };\n}\n\n/**\n * The one decision the middlewares make: who is asking, from `askerOf`, then\n * whether they have access to one of the plans.\n *\n * - nobody is asking (`customer` returned null): `unauthenticated`\n * - the fallback of `hasAccess` otherwise, per plan, through `decide`,\n * within the client's `guardTimeout`. The plans are asked at once, so\n * several fit the same budget, and read in order: the first that grants\n * lets the request through, without waiting for the ones after it.\n * `unavailable` when none grants and Mesub failed (outage, rate limit) or\n * the budget ran out on one with nothing cached: 402 only ever means\n * Mesub said no, for every plan\n *\n * An integration error (a bad API key, an unknown plan) is thrown, unless a\n * plan before it in the list already granted.\n *\n * `plansFor` runs only once somebody is identified: a plan worked out from\n * the request never runs, nor throws, for an anonymous one.\n */\nexport async function guard(\n client: Mesub,\n asked: Asked | null,\n plansFor: () => readonly string[],\n): Promise<GuardOutcome> {\n if (!asked) return { allowed: false, reason: 'unauthenticated' };\n\n const plans = plansFor();\n const customer = asCustomer(asked);\n const pending = plans.map((plan) => client.decide(customer, plan));\n\n // The ones not read, once a plan before them grants, must not reject unhandled.\n for (const decision of pending) decision.catch(() => undefined);\n\n let first: Decision | undefined;\n let unavailable: Decision | undefined;\n\n for (const [index, next] of pending.entries()) {\n const decision = await next;\n\n if (decision.access) {\n return { allowed: true, customer: asked, plan: plans[index]!, decision };\n }\n\n first ??= decision;\n // Mesub failed or did not answer within the budget, and nothing was\n // cached for that customer and plan: not a no, an outage.\n if (decision.unavailable) unavailable ??= decision;\n }\n\n if (unavailable) return { allowed: false, reason: 'unavailable', decision: unavailable };\n\n return { allowed: false, reason: 'no_access', ...(first ? { decision: first } : {}) };\n}\n\n/** The body a refusal answers with. `status` only when there is an answer to read it from. */\nexport function denialBody(outcome: Extract<GuardOutcome, { allowed: false }>) {\n return {\n access: false,\n reason: outcome.reason,\n ...(outcome.decision?.answer ? { status: outcome.decision.answer.status } : {}),\n };\n}\n\nexport function accessOf(outcome: Extract<GuardOutcome, { allowed: true }>): MesubAccess {\n const { customer } = outcome;\n\n return {\n wallet:\n customer.kind === 'wallet' ? customer.value : (outcome.decision.answer?.wallet ?? null),\n customer,\n plan: outcome.plan,\n answer: outcome.decision.answer,\n stale: outcome.decision.stale,\n };\n}\n\nexport function denialOf(outcome: Extract<GuardOutcome, { allowed: false }>): Denial {\n return {\n reason: outcome.reason,\n status: DENIAL_STATUS[outcome.reason],\n answer: outcome.decision?.answer ?? null,\n };\n}\n"],"mappings":"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","names":[]}
|