First public release

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-09-28 14:49:02 -04:00
co-authored by Claude Opus 5.5
commit ff7147296b
14 changed files with 2190 additions and 0 deletions
+12
View File
@@ -0,0 +1,12 @@
root = true
[*]
charset = utf-8
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
indent_style = space
indent_size = 2
[*.md]
trim_trailing_whitespace = false
+9
View File
@@ -0,0 +1,9 @@
node_modules/
dist/
*.log
.DS_Store
.env
.env.*
!.env.example
.npmrc
dist-cjs/
+6
View File
@@ -0,0 +1,6 @@
stages: [test]
test:
stage: test
image: node:22-alpine
script: [npm ci, npm run build, npm test]
+16
View File
@@ -0,0 +1,16 @@
# Changelog
## 0.1.0 - 2026-09-28
First public release under MIT.
- `assessService()` and `formatService()` assess whether self-service
provisioning is safe to run. The library gates provisioning; it does not
perform it.
- A disabled worker is NOT ASSESSED, and an enabled worker that has never
completed a real tenant gets its own finding.
- `preflight()` fails when no capacity limits are declared, and checks the
host ceiling, the per-account ceiling and a post-provision disk floor.
Unmeasured disk is NOT ASSESSED.
- `stuckJobs()` finds jobs claimed past a time window.
- Unverified reachability is NOT ASSESSED.
+21
View File
@@ -0,0 +1,21 @@
# Contributing
Issues and merge requests are welcome at
https://gitlab.com/extant2000/self-serve.
## Ground rules
- A test that cannot fail proves nothing. If you fix a bug, add a test and
check that it fails against the unfixed code before you submit.
- Measure, don't assume. Two modules with the same line count can still be
different programs.
- Keep dependencies minimal. Every new runtime dependency needs a reason.
- Explain why in comments, not what. The what is already in the code.
- A breaking change needs a major version bump and a note in CHANGELOG.md.
## Before you open a merge request
Run both of these and make sure they pass:
npm run build
npm test
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2026 Extant 2000 LLC
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+86
View File
@@ -0,0 +1,86 @@
# SelfServe
Decide whether self-service tenant provisioning is safe to run, and whether
the job queue running it is healthy.
SelfServe does not provision anything. It is the gate around a provisioning
worker you already have, for example one that claims jobs with
`FOR UPDATE SKIP LOCKED`, renders a per-tenant stack, starts it and wires
DNS. Reimplementing that worker would be duplication. The missing half is the
part that decides whether to let it run, which is the part that keeps
self-service from turning into an outage.
It is pure, with no clock and no I/O. `now` is a parameter.
## What it checks and why
1. Dormant is not proven. Built, enabled and proven in production are three
separate states. A worker that is switched off is NOT ASSESSED, never
ready, and an enabled worker that has never completed a real tenant gets
its own finding. Until one tenant has gone end to end, the first customer
is the integration test.
2. Self-service with no ceiling is a denial of service you run against
yourself. If every tenant is a stack on one host, a signup loop with no
quota fills that host, and a full disk on a shared host wedges every
container, not only the new one. Declaring no limits at all is a critical
failure: a quota that was never set is not unlimited, it is unconsidered.
`preflight()` also checks the host ceiling, the per-account ceiling and
the free disk left after provisioning. Unmeasured disk, or a stack with no
size estimate, is NOT ASSESSED. An unmeasured resource is not a plentiful
one.
3. A claimed job looks like progress. With `SKIP LOCKED` claiming, a worker
that dies mid-job leaves the job claimed forever. It is not queued, not
failed, and nothing retries it, so no dashboard alarms on it.
`stuckJobs()` finds jobs claimed for longer than a window (15 minutes by
default).
4. Provisioned is not reachable. DNS wired and the stack up is not the same
as the tenant's URL serving. Without a post-provision probe, the first
person to discover a broken tenant is the tenant.
## Usage
```ts
import { assessService, formatService, preflight, stuckJobs, type Job } from '@extant2000/self-serve'
const jobs: Job[] = [
{ id: 'j7', state: 'claimed', claimedAt: '2026-09-28T11:00:00Z', claimedBy: 'worker-1', where: 'jobs table' },
{ id: 'j8', state: 'queued', where: 'jobs table' },
]
const report = assessService(
{ workerEnabled: true, provenInProduction: false, verifiesReachability: true },
{
now: '2026-09-28T12:00:00Z',
request: { account: 'acme', tenantName: 'acme-prod', existingForAccount: 1, estimatedDiskBytes: 2e9, where: 'jobs table, j8' },
host: { tenants: 10, freeDiskBytes: null, where: 'df -P /data' },
limits: { maxTenants: 50, maxPerAccount: 3, minFreeDiskBytes: 10e9 },
jobs,
},
)
for (const f of report.findings) console.log(`${f.determination.padEnd(13)} ${f.id}: ${f.summary}`)
```
`assessService()` returns a `CapabilityReport` from
`@extant2000/evidence-record`, and `formatService()` renders it for a
terminal or CI log. `preflight()` and `stuckJobs()` can be used on their own.
## Sample output
Produced by running the example above:
```
not-assessed worker/unproven: The worker is enabled but has never completed a real tenant
not-assessed capacity/disk: Disk headroom after provisioning cannot be computed
fail job/j7: job j7 has been claimed by worker-1 since 2026-09-28T11:00:00Z
```
## Install
```
npm install @extant2000/self-serve
```
## License
MIT. Copyright (c) 2026 Extant 2000 LLC. See [LICENSE](LICENSE).
+11
View File
@@ -0,0 +1,11 @@
# Security policy
## Reporting a vulnerability
Please report vulnerabilities privately by email to security@extant2000.com.
Do not open a public issue or merge request for a security problem.
Include the affected version, a description of the issue, and steps or a test
that reproduce it if you have them.
We aim to acknowledge every report within 5 business days.
+1504
View File
File diff suppressed because it is too large Load Diff
+46
View File
@@ -0,0 +1,46 @@
{
"name": "@extant2000/self-serve",
"version": "0.1.0",
"private": false,
"description": "Partner-initiated provisioning with no engineer in the loop.",
"license": "MIT",
"type": "module",
"main": "./dist-cjs/index.js",
"files": [
"dist",
"dist-cjs",
"src",
"README.md",
"LICENSE"
],
"repository": {
"type": "git",
"url": "https://gitlab.com/extant2000/self-serve.git"
},
"module": "./dist/index.js",
"types": "./dist/index.d.ts",
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js",
"require": "./dist-cjs/index.js"
}
},
"scripts": {
"build": "tsc && tsc -p tsconfig.cjs.json && node -e \"require('fs').writeFileSync('dist-cjs/package.json', JSON.stringify({type:'commonjs'}))\"",
"test": "vitest run",
"prepublishOnly": "npm run build"
},
"devDependencies": {
"typescript": "^5.6.0",
"vitest": "^2.1.0"
},
"dependencies": {
"@extant2000/evidence-record": "^0.1.2"
},
"author": "Extant 2000 LLC",
"homepage": "https://gitlab.com/extant2000/self-serve",
"bugs": {
"url": "https://gitlab.com/extant2000/self-serve/-/issues"
}
}
+282
View File
@@ -0,0 +1,282 @@
/**
* SelfServe — decide whether partner-initiated provisioning is safe to run,
* and whether the queue running it is actually healthy.
*
* **This does not provision anything.** It assumes a provisioning worker
* that already exists (one that claims jobs via `FOR UPDATE SKIP LOCKED`,
* renders a per-tenant compose stack, brings it up and wires DNS).
* Reimplementing that here would be duplication. What is missing is the half that decides whether to let it
* run — which is the half that keeps self-service from being an outage.
*
* Four checks, each from a property of that kind of deployment:
*
* 1. **Dormant is not proven.** The worker ships behind
* `DISABLE_PROVISION_WORKER=1`. Built, enabled, and proven in production
* are three states, and reporting a dormant capability as ready is how
* the first real tenant becomes the test.
*
* 2. **Self-serve without a ceiling is a denial of service you run against
* yourself.** Every tenant is a compose stack on one host. A signup loop
* with no quota fills the host, and on a shared host a full disk wedges
* every container, not only the new one.
*
* 3. **A claimed job looks like progress.** `SKIP LOCKED` means a worker
* that dies mid-job leaves that job claimed forever. Not queued, not
* failed, nothing retrying it — the one state no dashboard alarms on.
*
* 4. **Provisioned is not reachable.** DNS wired and the stack up is not
* the same as the tenant's URL serving.
*
* Pure: no clock, no I/O. `now` is a parameter.
*/
import {
type CapabilityReport,
type Finding,
type FormatOptions,
evidence,
formatReport as renderReport,
} from '@extant2000/evidence-record'
export interface CapacityLimits {
/** Hard ceiling on tenants for this host. */
maxTenants?: number
/** Ceiling per account, so one account cannot take the host. */
maxPerAccount?: number
/** Free disk that must REMAIN after provisioning. */
minFreeDiskBytes?: number
}
export interface HostState {
tenants: number
/** `null` when not measured — never treated as plentiful. */
freeDiskBytes: number | null
/** Where these readings came from. */
where: string
}
export interface ProvisioningRequest {
account: string
tenantName: string
/** Tenants this account already has. */
existingForAccount: number
/** Estimated footprint of the new stack. `null` when unknown. */
estimatedDiskBytes: number | null
where: string
}
export type JobState = 'queued' | 'claimed' | 'done' | 'failed'
export interface Job {
id: string
state: JobState
/** ISO 8601, when a worker claimed it. */
claimedAt?: string
claimedBy?: string
where: string
}
export interface ServiceState {
/** False when the worker ships dormant behind a kill flag. */
workerEnabled: boolean
/** True once a real tenant has gone end to end in production. */
provenInProduction?: boolean
/** Seconds a job may stay claimed before it is presumed abandoned. */
maxClaimSeconds?: number
/** Whether provisioned tenants are verified reachable afterwards. */
verifiesReachability?: boolean
}
const DEFAULT_MAX_CLAIM = 900
const fmt = (n: number): string =>
n >= 1e9 ? `${(n / 1e9).toFixed(1)} GB` : n >= 1e6 ? `${(n / 1e6).toFixed(0)} MB` : `${n} B`
/**
* Whether the service may accept a new provisioning request.
*
* The absence of a limit is itself a finding. A quota that was never set is
* not an unlimited quota, it is an unconsidered one — and "unlimited" on a
* single host is not a decision anyone would make deliberately.
*/
export function preflight(
request: ProvisioningRequest,
host: HostState,
limits: CapacityLimits = {},
): Finding[] {
const out: Finding[] = []
const cite = evidence.file(host.where, undefined,
`${host.tenants} tenants, ${host.freeDiskBytes === null ? 'disk unmeasured' : `${fmt(host.freeDiskBytes)} free`}`)
const noLimits = limits.maxTenants === undefined
&& limits.maxPerAccount === undefined
&& limits.minFreeDiskBytes === undefined
if (noLimits) {
out.push({
id: 'capacity/no-limits',
summary: 'No capacity limits are declared',
determination: 'fail',
severity: 'critical',
detail: 'Self-serve provisioning with no ceiling is a denial of service you run against yourself. Every tenant is a stack on one host, and a signup loop fills it — a quota that was never set is not unlimited, it is unconsidered.',
evidence: [cite],
})
}
if (limits.maxTenants !== undefined && host.tenants + 1 > limits.maxTenants) {
out.push({
id: 'capacity/host',
summary: `Host is at ${host.tenants} of ${limits.maxTenants} tenants`,
determination: 'fail',
severity: 'high',
detail: 'Provisioning would exceed the host ceiling.',
evidence: [cite],
})
}
if (limits.maxPerAccount !== undefined && request.existingForAccount + 1 > limits.maxPerAccount) {
out.push({
id: 'capacity/account',
summary: `${request.account} has ${request.existingForAccount} of ${limits.maxPerAccount} allowed tenants`,
determination: 'fail',
severity: 'high',
detail: 'A per-account ceiling is what stops one account taking the host.',
evidence: [evidence.file(request.where, undefined, `account ${request.account}`)],
})
}
if (limits.minFreeDiskBytes !== undefined) {
if (host.freeDiskBytes === null || request.estimatedDiskBytes === null) {
out.push({
id: 'capacity/disk',
summary: 'Disk headroom after provisioning cannot be computed',
determination: 'not-assessed',
severity: 'high',
detail: `${host.freeDiskBytes === null ? 'Free disk was not measured' : 'The new stack has no size estimate'}. An unmeasured resource is not a plentiful one, and a full disk on this host wedges every container, not only the new tenant.`,
evidence: [cite],
})
} else {
const after = host.freeDiskBytes - request.estimatedDiskBytes
if (after < limits.minFreeDiskBytes) {
out.push({
id: 'capacity/disk',
summary: `${fmt(after)} free after provisioning, below the ${fmt(limits.minFreeDiskBytes)} floor`,
determination: 'fail',
severity: 'critical',
detail: 'A full disk on this host wedges every container, not only the new tenant.',
evidence: [cite],
})
}
}
}
if (out.length === 0) {
out.push({
id: 'capacity',
summary: `Capacity available for ${request.tenantName}`,
determination: 'pass',
severity: 'info',
evidence: [cite],
})
}
return out
}
/**
* Jobs claimed and never finished.
*
* `SKIP LOCKED` claiming means a worker that dies mid-job leaves that job
* claimed forever: not queued, not failed, nothing retries it. It is the one
* state that looks like progress, which is why nothing alarms on it.
*/
export function stuckJobs(jobs: Job[], now: string, maxClaimSeconds = DEFAULT_MAX_CLAIM): Job[] {
const n = Date.parse(now)
return (jobs ?? []).filter(j => {
if (j.state !== 'claimed' || !j.claimedAt) return false
const t = Date.parse(j.claimedAt)
if (!Number.isFinite(t) || !Number.isFinite(n)) return false
return (n - t) / 1000 > maxClaimSeconds
})
}
export interface AssessOptions {
/** ISO 8601. Injected, never read from the clock. */
now: string
request?: ProvisioningRequest
host?: HostState
limits?: CapacityLimits
jobs?: Job[]
}
/** Assess whether self-service provisioning is safe to run right now. */
export function assessService(service: ServiceState, opts: AssessOptions): CapabilityReport {
const findings: Finding[] = []
const maxClaim = service.maxClaimSeconds ?? DEFAULT_MAX_CLAIM
if (!service.workerEnabled) {
findings.push({
id: 'worker/dormant',
summary: 'The provisioning worker is disabled',
determination: 'not-assessed',
severity: 'high',
detail: 'Built, enabled and proven in production are three states. A dormant capability reported as ready is how the first real tenant becomes the test.',
evidence: [evidence.document('DISABLE_PROVISION_WORKER', undefined, undefined, 'the worker ships dormant behind this flag')],
})
} else if (!service.provenInProduction) {
findings.push({
id: 'worker/unproven',
summary: 'The worker is enabled but has never completed a real tenant',
determination: 'not-assessed',
severity: 'high',
detail: 'Enabled is not proven. Until one tenant has gone end to end in production, the first customer is the integration test.',
evidence: [],
})
}
if (opts.request && opts.host) {
findings.push(...preflight(opts.request, opts.host, opts.limits ?? {}))
}
const jobs = opts.jobs ?? []
const stuck = stuckJobs(jobs, opts.now, maxClaim)
for (const j of stuck) {
findings.push({
id: `job/${j.id}`,
summary: `job ${j.id} has been claimed by ${j.claimedBy ?? 'a worker'} since ${j.claimedAt}`,
determination: 'fail',
severity: 'critical',
detail: `Claimed longer than ${Math.round(maxClaim / 60)} minutes. With SKIP LOCKED claiming, a worker that died mid-job leaves it claimed forever — not queued, not failed, nothing retrying it. It is the one state that looks like progress.`,
evidence: [evidence.record('provisioning_jobs', j.id, 'state', 'claimed', j.where)],
})
}
if (service.verifiesReachability !== true) {
findings.push({
id: 'verify/reachability',
summary: 'Provisioned tenants are not verified reachable',
determination: 'not-assessed',
severity: 'high',
detail: "DNS wired and the stack up is not the same as the tenant's URL serving. Without a post-provision probe, the first person to discover a broken tenant is the tenant.",
evidence: [],
})
}
return {
capability: 'SelfServe',
scope: `worker ${service.workerEnabled ? 'enabled' : 'dormant'}, ${jobs.length} job(s) in flight`,
examined: 1 + (opts.request ? 1 : 0) + jobs.length,
findings,
notes: [
`${stuck.length} stuck job(s) of ${jobs.filter(j => j.state === 'claimed').length} claimed`,
'This library gates provisioning; it does not perform it — the worker already exists.',
],
}
}
/** Render a self-service assessment for a terminal, CI log or review. */
export function formatService(service: ServiceState, opts: AssessOptions & FormatOptions): string {
return renderReport(assessService(service, opts), opts)
}
export { DEFAULT_MAX_CLAIM, fmt as formatBytes }
+159
View File
@@ -0,0 +1,159 @@
import { describe, expect, it } from 'vitest'
import { overall, validateReport } from '@extant2000/evidence-record'
import {
assessService,
formatService,
preflight,
stuckJobs,
type HostState,
type Job,
type ProvisioningRequest,
type ServiceState,
} from '../src/index.js'
const NOW = '2026-08-01T12:00:00Z'
const healthy: ServiceState = {
workerEnabled: true,
provenInProduction: true,
verifiesReachability: true,
}
const host: HostState = { tenants: 10, freeDiskBytes: 50e9, where: 'df -P /data' }
const request: ProvisioningRequest = {
account: 'acme', tenantName: 'acme-prod', existingForAccount: 1,
estimatedDiskBytes: 2e9, where: 'provisioning_jobs#j1',
}
const limits = { maxTenants: 50, maxPerAccount: 3, minFreeDiskBytes: 10e9 }
describe('dormant is not proven', () => {
it('reports a disabled worker as not-assessed, never ready', () => {
// Built, enabled and proven in production are three states.
const r = assessService({ ...healthy, workerEnabled: false }, { now: NOW })
const f = r.findings.find(x => x.id === 'worker/dormant')!
expect(f.determination).toBe('not-assessed')
expect(f.detail).toContain('the first real tenant becomes the test')
expect(overall(r)).toBe('not-assessed')
})
it('distinguishes enabled from proven', () => {
const r = assessService({ ...healthy, provenInProduction: false }, { now: NOW })
const f = r.findings.find(x => x.id === 'worker/unproven')!
expect(f.determination).toBe('not-assessed')
expect(f.detail).toContain('the first customer is the integration test')
})
it('says nothing about the worker when it is enabled and proven', () => {
const r = assessService(healthy, { now: NOW })
expect(r.findings.some(f => f.id.startsWith('worker/'))).toBe(false)
})
})
describe('no ceiling is a denial of service you run against yourself', () => {
it('fails when no limits are declared at all', () => {
// A quota that was never set is not unlimited, it is unconsidered.
const f = preflight(request, host, {})[0]!
expect(f.determination).toBe('fail')
expect(f.severity).toBe('critical')
expect(f.detail).toContain('unlimited, it is unconsidered')
})
it('fails at the host ceiling', () => {
expect(preflight(request, { ...host, tenants: 50 }, limits)
.some(f => f.id === 'capacity/host' && f.determination === 'fail')).toBe(true)
})
it('fails at the per-account ceiling — one account cannot take the host', () => {
expect(preflight({ ...request, existingForAccount: 3 }, host, limits)
.some(f => f.id === 'capacity/account' && f.determination === 'fail')).toBe(true)
})
it('fails when provisioning would drop below the disk floor', () => {
const f = preflight({ ...request, estimatedDiskBytes: 45e9 }, host, limits)
.find(x => x.id === 'capacity/disk')!
expect(f.determination).toBe('fail')
expect(f.detail).toContain('wedges every container')
})
it('reports unmeasured disk as not-assessed, never plentiful', () => {
const f = preflight(request, { ...host, freeDiskBytes: null }, limits)
.find(x => x.id === 'capacity/disk')!
expect(f.determination).toBe('not-assessed')
expect(f.detail).toContain('An unmeasured resource is not a plentiful one')
})
it('reports an unestimated stack the same way', () => {
const f = preflight({ ...request, estimatedDiskBytes: null }, host, limits)
.find(x => x.id === 'capacity/disk')!
expect(f.determination).toBe('not-assessed')
})
it('passes when every limit is satisfied', () => {
const out = preflight(request, host, limits)
expect(out).toHaveLength(1)
expect(out[0]!.determination).toBe('pass')
})
})
describe('a claimed job looks like progress', () => {
const job = (p: Partial<Job> = {}): Job =>
({ id: 'j1', state: 'claimed', claimedAt: '2026-08-01T11:00:00Z', claimedBy: 'worker-1', where: 'db', ...p })
it('finds a job claimed past the limit', () => {
// SKIP LOCKED means a dead worker leaves it claimed forever: not queued,
// not failed, nothing retrying it.
expect(stuckJobs([job()], NOW, 900)).toHaveLength(1)
const r = assessService(healthy, { now: NOW, jobs: [job()] })
const f = r.findings.find(x => x.id === 'job/j1')!
expect(f.determination).toBe('fail')
expect(f.severity).toBe('critical')
expect(f.detail).toContain('the one state that looks like progress')
})
it('leaves a recently claimed job alone', () => {
expect(stuckJobs([job({ claimedAt: '2026-08-01T11:55:00Z' })], NOW, 900)).toHaveLength(0)
})
it('ignores queued, done and failed jobs', () => {
for (const state of ['queued', 'done', 'failed'] as const) {
expect(stuckJobs([job({ state })], NOW, 900)).toHaveLength(0)
}
})
it('honours a custom claim window', () => {
expect(stuckJobs([job()], NOW, 7200)).toHaveLength(0)
expect(stuckJobs([job()], NOW, 600)).toHaveLength(1)
})
})
describe('provisioned is not reachable', () => {
it('reports unverified reachability as not-assessed', () => {
const r = assessService({ ...healthy, verifiesReachability: false }, { now: NOW })
const f = r.findings.find(x => x.id === 'verify/reachability')!
expect(f.determination).toBe('not-assessed')
expect(f.detail).toContain('the first person to discover a broken tenant is the tenant')
})
it('says nothing when reachability is verified', () => {
expect(assessService(healthy, { now: NOW }).findings.some(f => f.id === 'verify/reachability')).toBe(false)
})
})
describe('conformance with the evidence-record standard', () => {
it('a fully healthy service with a valid request passes', () => {
const r = assessService(healthy, { now: NOW, request, host, limits })
expect(overall(r)).toBe('pass')
})
it('every conclusion carries a citation', () => {
expect(validateReport(assessService(
{ ...healthy, workerEnabled: false },
{ now: NOW, request, host, limits, jobs: [{ id: 'j1', state: 'claimed', claimedAt: '2026-08-01T09:00:00Z', where: 'db' }] },
))).toEqual([])
})
it('says it gates rather than provisions', () => {
expect(formatService(healthy, { now: NOW }))
.toContain('gates provisioning; it does not perform it')
})
})
+9
View File
@@ -0,0 +1,9 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"module": "CommonJS",
"moduleResolution": "Node",
"outDir": "dist-cjs",
"declaration": false
}
}
+8
View File
@@ -0,0 +1,8 @@
{
"compilerOptions": {
"target": "ES2022", "module": "ES2022", "moduleResolution": "bundler",
"declaration": true, "outDir": "dist", "rootDir": "src",
"strict": true, "skipLibCheck": true
},
"include": ["src/**/*.ts"]
}