mirror of
https://github.com/nestriness/nestri.git
synced 2026-09-19 17:25:19 +03:00
Every team now exists with the payment provider, free ones included. An upgrade then changes a subscription rather than inventing a customer, and there is one question to ask about anybody instead of two. A subscription at nothing a month needs no payment, so it is created outright rather than by sending somebody through a checkout to pay zero. It runs after the team rows are committed and cannot affect them. Signing up is not allowed to depend on a third party being reachable, so this cannot fail the call and does not retry — a team that misses it is free, which is what it would have been anyway, and the next call puts it right because the operation is idempotent. This broke the webhook mapping, which read the plan off the event type. A free subscription announces itself with the same `subscription.created` a paid one does, so every new signup would have landed on the paid allowance. The plan now comes from the product, and a product we do not sell is left alone rather than guessed at — somebody selling something else through the same account must not be able to change what a team may run by doing so. The external id stays the team. It is the billing subject, and keying on the user would collapse somebody with two teams into one customer with no way to say which subscription belonged to which.
77 lines
2.5 KiB
TypeScript
77 lines
2.5 KiB
TypeScript
import { z } from 'zod';
|
|
|
|
import { memo } from './utils/memo.js';
|
|
|
|
let _overrides: Record<string, unknown> = {};
|
|
|
|
export namespace Env {
|
|
export const Info = z.object({
|
|
NODE_ENV: z.enum(['development', 'production', 'test']).default('development'),
|
|
|
|
AUTH_ISSUER_URL: z.string().optional(),
|
|
|
|
/**
|
|
* Where to *reach* the issuer, when that is not where it *lives*.
|
|
*
|
|
* `AUTH_ISSUER_URL` is an identity: a token carries the address it was
|
|
* minted through and verification compares the two literally, so it is
|
|
* the public name and can be nothing else. But the public name is
|
|
* often not routable from inside a deployment — a container on a
|
|
* private network, a host behind its own proxy — and one setting
|
|
* cannot be both.
|
|
*
|
|
* So this one is the route and the other is the name, which is the
|
|
* same split a service binding makes on its own: the binding is the
|
|
* route, and the `iss` claim is still the name. Unset means they are
|
|
* the same address, which is the ordinary case.
|
|
*/
|
|
AUTH_INTERNAL_URL: z.string().optional(),
|
|
|
|
/**
|
|
* Burn allowances per plan, as JSON. Unset takes the placeholder set.
|
|
*
|
|
* Configuration rather than constants because these are retuned against
|
|
* real burn far more often than the code that reads them changes, and a
|
|
* rate that needs a deploy is a rate that stays wrong until the next one.
|
|
*/
|
|
BURN_LIMITS: z.string().optional(),
|
|
|
|
/**
|
|
* The payment provider.
|
|
*
|
|
* `POLAR_SERVER` picks the instance and the two are entirely separate
|
|
* servers with separate data, so a token from one is refused by the
|
|
* other and a product id from one means nothing to it. Getting this
|
|
* wrong fails loudly rather than quietly charging somebody.
|
|
*/
|
|
POLAR_ACCESS_TOKEN: z.string().optional(),
|
|
POLAR_WEBHOOK_SECRET: z.string().optional(),
|
|
POLAR_PRODUCT_ID: z.string().optional(),
|
|
/** The product a team is put on at signup, priced at nothing. */
|
|
POLAR_FREE_PRODUCT_ID: z.string().optional(),
|
|
POLAR_SERVER: z.enum(['sandbox', 'production']).optional(),
|
|
|
|
DATABASE_URL: z.string().optional()
|
|
});
|
|
|
|
export type Info = z.infer<typeof Info>;
|
|
|
|
const _get = memo(() => Info.parse({ ...process.env, ..._overrides }));
|
|
|
|
export function get(): Info {
|
|
return _get();
|
|
}
|
|
|
|
export function init(bindings: Record<string, unknown>) {
|
|
_overrides = {
|
|
...bindings,
|
|
...(bindings.HYPERDRIVE
|
|
? {
|
|
DATABASE_URL: (bindings.HYPERDRIVE as { connectionString: string }).connectionString
|
|
}
|
|
: {})
|
|
};
|
|
_get.reset();
|
|
}
|
|
}
|