diff --git a/.dev.vars.sample b/.dev.vars.sample new file mode 100644 index 0000000..477c3c4 --- /dev/null +++ b/.dev.vars.sample @@ -0,0 +1,3 @@ +# Generate with npx tsx backend/create-cloud-session-token-keypair.ts +CLOUD_SESSION_TOKEN_SECRET= +CLERK_SECRET_KEY= diff --git a/.eslintrc.cjs b/.eslintrc.cjs index 4f6f59e..0a90d39 100644 --- a/.eslintrc.cjs +++ b/.eslintrc.cjs @@ -66,11 +66,7 @@ module.exports = { }, }, }, - extends: [ - "plugin:@typescript-eslint/recommended", - "plugin:import/recommended", - "plugin:import/typescript", - ], + extends: ["plugin:@typescript-eslint/recommended", "plugin:import/recommended", "plugin:import/typescript"], }, // Node diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml new file mode 100644 index 0000000..a66d52e --- /dev/null +++ b/.github/workflows/ci.yaml @@ -0,0 +1,100 @@ +name: CI +on: + workflow_dispatch: + push: + tags: + - "**" + branches: + - "mabels/backend" + pull_request: + branches: + - "mabels/backend" +jobs: + # deploy-staging: + # environment: staging + # runs-on: ubuntu-24.04 + # name: Deploy to staging + # needs: [quality-checks] + # steps: + # + quality-checks: + name: Testit Runit Buildit + environment: ${{ startsWith(github.ref, 'refs/tags/s') && 'staging' || startsWith(github.ref, 'refs/tags/p') && 'production' || 'dev' }} + #runs-on: blacksmith-4vcpu-ubuntu-2204 + runs-on: blacksmith-4vcpu-ubuntu-2204-arm + steps: + - uses: pnpm/action-setup@v4 + name: Install pnpm + with: + run_install: false + version: 10 + cache: "pnpm" + + - uses: actions/checkout@v4 + + - uses: useblacksmith/setup-node@v5 + with: + node-version: 20 + cache: pnpm + + #runs-on: ubuntu-24.04 + #steps: + # - uses: actions/setup-node@v4 + # with: + # node-version: 20 + # cache: pnpm + + - name: install + run: pnpm install + + - name: format-check + run: pnpm run format --check + + - name: lint + run: pnpm run lint + + - name: build + env: + VITE_CLERK_PUBLISHABLE_KEY: ${{ vars.CLERK_PUBLISHABLE_KEY }} + run: pnpm run build + + - name: test + run: | + pnpm run test + + - name: deploy cf + id: attempt1 + env: + # need for drizzle + CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} + CLOUDFLARE_ACCOUNT_ID: ${{ vars.CLOUDFLARE_ACCOUNT_ID }} + CLOUDFLARE_D1_TOKEN: ${{ secrets.CLOUDFLARE_D1_TOKEN }} + CLOUDFLARE_DATABASE_ID: ${{ vars.CLOUDFLARE_DATABASE_ID }} + # need in CF to be Env + CLOUD_SESSION_TOKEN_PUBLIC: ${{ vars.CLOUD_SESSION_TOKEN_PUBLIC }} + CLOUD_SESSION_TOKEN_SECRET: ${{ secrets.CLOUD_SESSION_TOKEN_SECRET }} + CLERK_PUBLISHABLE_KEY: ${{ vars.CLERK_PUBLISHABLE_KEY }} + CLERK_PUB_JWT_KEY: ${{ vars.CLERK_PUB_JWT_KEY }} + # need during build + VITE_CLERK_PUBLISHABLE_KEY: ${{ vars.CLERK_PUBLISHABLE_KEY }} + run: | + # pnpm run build + pnpm run drizzle:d1-remote + pnpm exec fp-cli writeEnv --env ${{vars.CLOUDFLARE_ENV}} --out /dev/stdout --json \ + --fromEnv CLERK_PUBLISHABLE_KEY \ + --fromEnv CLERK_PUB_JWT_KEY \ + --fromEnv CLOUD_SESSION_TOKEN_SECRET \ + --fromEnv CLOUD_SESSION_TOKEN_PUBLIC | \ + pnpm exec wrangler -c ./wrangler.toml secret --env ${{vars.CLOUDFLARE_ENV}} bulk + pnpm run deploy:cf --env ${{vars.CLOUDFLARE_ENV}} + + # - name: deploy to production + # if: github.ref == 'refs/heads/main' + # env: + # CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} + # CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} + # CLOUDFLARE_DATABASE_ID: ${{ secrets.CLOUDFLARE_DATABASE_ID }} + # run: | + # pnpm run build + # pnpm run drizzle:d1-remote + # pnpm run deploy:cf --env production diff --git a/.github/workflows/staging.yaml b/.github/workflows/staging.yaml new file mode 100644 index 0000000..61694f0 --- /dev/null +++ b/.github/workflows/staging.yaml @@ -0,0 +1,56 @@ +name: Staging Deployment +on: + workflow_dispatch: + inputs: + environment: + description: "Target environment" + required: true + default: "staging" + +jobs: + deploy-staging: + name: + environment: staging + runs-on: blacksmith-4vcpu-ubuntu-2204 + steps: + - uses: useblacksmith/setup-node@v5 + with: + node-version: 20 + + - uses: actions/checkout@v4 + + - uses: pnpm/action-setup@v4 + name: Install pnpm + with: + run_install: false + version: 10 + cache: "pnpm" + + - name: install + run: pnpm install + + - name: format-check + run: pnpm run format --check + + - name: lint + run: pnpm run lint + + - name: build + run: pnpm run build + + - name: test + run: | + pnpm run drizzle:libsql + pnpm run test + + - name: deploy to preview + env: + CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} + CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} + CLOUDFLARE_D1_TOKEN: ${{ secrets.CLOUDFLARE_D1_TOKEN }} + CLOUDFLARE_DATABASE_ID: ${{ secrets.CLOUDFLARE_DATABASE_ID }} + VITE_CLERK_PUBLISHABLE_KEY: ${{ vars.CLERK_PUBLISHABLE_KEY }} + run: | + pnpm run build + pnpm run drizzle:d1-remote --verbose + pnpm run deploy:cf diff --git a/.gitignore b/.gitignore index 321dfd5..a753f40 100644 --- a/.gitignore +++ b/.gitignore @@ -1,9 +1,13 @@ node_modules +**/.wrangler/** + +stats.html /.cache /build /public/build .env +.dev.vars # Logs logs @@ -28,4 +32,4 @@ dist-ssr *.njsproj *.sln *.sw? -*.zip \ No newline at end of file +*.zip diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..181ae7d --- /dev/null +++ b/.prettierignore @@ -0,0 +1,5 @@ +**/pnpm-lock.yaml +scripts/ +**/.esm-cache/** +**/dist/** +**/coverage/** diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..5158fa8 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,6 @@ +{ + "printWidth": 132, + "singleQuote": false, + "semi": true, + "useTabs": false +} diff --git a/README.md b/README.md index b3b0cfa..2686a35 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,81 @@ # Fireproof Dashboard + https://dashboard.fireproof.storage/ + +## Environment Variables + +### Development + +To run this project in development mode, you need to set up the following environment files: + +1. Create a `.env` or `.env.local` file with these variables: + +``` +CLOUD_SESSION_TOKEN_SECRET=your_session_token_secret +CLERK_SECRET_KEY=your_clerk_secret_key +VITE_CLERK_PUBLISHABLE_KEY=your_clerk_publishable_key +``` + +2. Create a `.dev.vars` file (used by Wrangler for local development): + +``` +CLOUD_SESSION_TOKEN_SECRET=your_session_token_secret +CLERK_SECRET_KEY=your_clerk_secret_key +``` + +Additionally, you need to create a token template named `with-email` in your Clerk dashboard with the following configuration: + +```json +{ + "role": "authenticated", + "params": { + "last": "{{user.last_name}}", + "name": "{{user.username}}", + "email": "{{user.primary_email_address}}", + "first": "{{user.first_name}}", + "image_url": "{{user.image_url}}", + "external_id": "{{user.external_id}}", + "public_meta": "{{user.public_metadata}}", + "email_verified": "{{user.email_verified}}" + }, + "userId": "{{user.id}}" +} +``` + +## Local Development + +To set up and run the project locally: + +1. Set up the database schema: + + ``` + pnpm drizzle:d1-local + ``` + +2. Start the frontend development server: + + ``` + pnpm dev + ``` + +3. In a separate terminal, start one of the backend servers: + ``` + pnpm backend:d1 # For Cloudflare Workers D1 backend + ``` + OR + ``` + pnpm backend:deno # For Deno backend + ``` + +### Deployment + +The main deployment target for this project is **Cloudflare Workers**. + +For deployment, you'll need these additional environment variables: + +``` +CLOUDFLARE_ACCOUNT_ID=your_cloudflare_account_id +CLOUDFLARE_API_TOKEN=your_cloudflare_api_token +CLOUDFLARE_DATABASE_ID=your_cloudflare_database_id +VITE_CLERK_PUBLISHABLE_KEY= +``` diff --git a/backend/api.ts b/backend/api.ts new file mode 100644 index 0000000..97f47b6 --- /dev/null +++ b/backend/api.ts @@ -0,0 +1,2000 @@ +import { Result } from "@adviser/cement"; +import { SuperThis, ps, rt } from "@fireproof/core"; +import { gte, and, eq, gt, inArray, lt, ne, or } from "drizzle-orm/sql/expressions"; +import type { LibSQLDatabase } from "drizzle-orm/libsql"; +import { jwtVerify } from "jose"; +import { + AuthType, + ClerkClaim, + ClerkVerifyAuth, + InCreateTenantParams, + InviteTicket, + InvitedParams, + OutTenantParams, + QueryUser, + ReqCloudSessionToken, + ReqCreateLedger, + ReqCreateTenant, + ReqDeleteInvite, + ReqDeleteLedger, + ReqDeleteTenant, + ReqEnsureUser, + ReqExtendToken, + ReqFindUser, + ReqInviteUser, + ReqListInvites, + ReqListLedgersByUser, + ReqListTenantsByUser, + ReqRedeemInvite, + ReqTokenByResultId, + ReqUpdateLedger, + ReqUpdateTenant, + ReqUpdateUserTenant, + ResCloudSessionToken, + ResCreateLedger, + ResCreateTenant, + ResDeleteInvite, + ResDeleteLedger, + ResDeleteTenant, + ResEnsureUser, + ResExtendToken, + ResFindUser, + ResInviteUser, + ResListInvites, + ResListLedgersByUser, + ResListTenantsByUser, + ResRedeemInvite, + ResTokenByResultId, + ResUpdateLedger, + ResUpdateTenant, + ResUpdateUserTenant, + RoleType, + User, + UserStatus, +} from "./fp-dash-types.ts"; +import { prepareInviteTicket, sqlInviteTickets, sqlToInviteTickets } from "./invites.ts"; +import { sqlLedgerUsers, sqlLedgers, sqlToLedgers } from "./ledgers.ts"; +import { queryCondition, queryEmail, queryNick, toBoolean, toUndef } from "./sql-helper.ts"; +import { sqlTenantUsers, sqlTenants } from "./tenants.ts"; +import { sqlTokenByResultId } from "./token-by-result-id.ts"; +import { UserNotFoundError, getUser, isUserNotFound, queryUser, upsetUserByProvider } from "./users.ts"; +import { createFPToken, FPTokenContext, getFPTokenContext } from "./create-fp-token.ts"; + +function sqlToOutTenantParams(sql: typeof sqlTenants.$inferSelect): OutTenantParams { + return { + tenantId: sql.tenantId, + name: sql.name, + ownerUserId: sql.ownerUserId, + maxAdminUsers: sql.maxAdminUsers, + maxMemberUsers: sql.maxMemberUsers, + maxLedgers: sql.maxLedgers, + maxInvites: sql.maxInvites, + status: sql.status as UserStatus, + statusReason: sql.statusReason, + createdAt: new Date(sql.createdAt), + updatedAt: new Date(sql.updatedAt), + }; +} + +export interface TokenByResultIdParam { + readonly status: "found" | "not-found"; + readonly resultId: string; + readonly token?: string; // JWT + readonly now: Date; +} + +export interface FPApiInterface { + ensureUser(req: ReqEnsureUser): Promise>; + findUser(req: ReqFindUser): Promise>; + + createTenant(req: ReqCreateTenant): Promise>; + updateTenant(req: ReqUpdateTenant): Promise>; + deleteTenant(req: ReqDeleteTenant): Promise>; + + redeemInvite(req: ReqRedeemInvite): Promise>; + + listTenantsByUser(req: ReqListTenantsByUser): Promise>; + updateUserTenant(req: ReqUpdateUserTenant): Promise>; + + // creates / update invite + inviteUser(req: ReqInviteUser): Promise>; + listInvites(req: ReqListInvites): Promise>; + deleteInvite(req: ReqDeleteInvite): Promise>; + + createLedger(req: ReqCreateLedger): Promise>; + listLedgersByUser(req: ReqListLedgersByUser): Promise>; + updateLedger(req: ReqUpdateLedger): Promise>; + deleteLedger(req: ReqDeleteLedger): Promise>; + + // listLedgersByTenant(req: ReqListLedgerByTenant): Promise + + // attachUserToLedger(req: ReqAttachUserToLedger): Promise + getCloudSessionToken(req: ReqCloudSessionToken): Promise>; + getTokenByResultId(req: ReqTokenByResultId): Promise>; + extendToken(req: ReqExtendToken): Promise>; +} + +export const FPAPIMsg = new ps.dashboard.FAPIMsgImpl(); + +export interface FPApiToken { + verify(token: string): Promise>; +} + +interface ReqInsertTenant { + readonly tenantId: string; + readonly name?: string; + readonly ownerUserId: string; + readonly adminUserIds?: string[]; + readonly memberUserIds?: string[]; + readonly maxAdminUsers?: number; + readonly maxMemberUsers?: number; + readonly createdAt?: Date; + readonly updatedAt?: Date; +} + +// interface ResInsertTenant { +// readonly tenantId: string; +// readonly name?: string; +// readonly ownerUserId: string; +// readonly adminUserIds: string[]; +// readonly memberUserIds: string[]; +// readonly maxAdminUsers: number; +// readonly maxMemberUsers: number; +// readonly createdAt: Date; +// readonly updatedAt: Date; +// } + +// interface ReqInsertUser { +// readonly userId: string; +// readonly auth: ClerkVerifyAuth; +// readonly maxTenants?: number; +// readonly createdAt?: Date; +// readonly updatedAt?: Date; +// } + +interface AddUserToTenant { + readonly userName?: string; + readonly tenantName?: string; + readonly tenantId: string; + readonly userId: string; + readonly default?: boolean; + readonly role: ps.cloud.Role; + readonly status?: UserStatus; + readonly statusReason?: string; +} + +interface AddUserToLedger { + readonly userName?: string; + readonly ledgerName?: string; + readonly ledgerId: string; + readonly tenantId: string; + readonly userId: string; + readonly default?: boolean; + readonly status?: UserStatus; + readonly statusReason?: string; + readonly role: ps.cloud.Role; + readonly right: ps.cloud.ReadWrite; +} + +// interface ResAddUserToTenant { +// readonly name?: string; +// readonly tenantId: string; +// readonly userId: string; +// readonly default: boolean; +// readonly role: Role; +// } + +// type SQLTransaction = SQLiteTransaction< +// "async", +// ResultSet, +// Record, +// ExtractTablesWithRelations> +// >; + +interface WithAuth { + readonly auth: AuthType; +} + +interface ActiveUser { + readonly verifiedAuth: T; + readonly user?: User; +} + +type ActiveUserWithUserId = Omit, "user"> & { + user: { + userId: string; + maxTenants: number; + }; +}; + +function nameFromAuth(name: string | undefined, auth: ActiveUserWithUserId): string { + return name ?? `${auth.verifiedAuth.params.email ?? nickFromClarkClaim(auth.verifiedAuth.params) ?? auth.verifiedAuth.userId}`; +} + +function nickFromClarkClaim(auth: ClerkClaim): string | undefined { + return auth.nick ?? auth.name; +} + +export class FPApiSQL implements FPApiInterface { + readonly db: LibSQLDatabase; + readonly tokenApi: Record; + readonly sthis: SuperThis; + constructor(sthis: SuperThis, db: LibSQLDatabase, token: Record) { + this.db = db; + this.tokenApi = token; + this.sthis = sthis; + } + + private async _authVerifyAuth(req: { readonly auth: AuthType }): Promise> { + // console.log("_authVerify-1", req); + const tokenApi = this.tokenApi[req.auth.type]; + // console.log("_authVerify-2", req); + if (!tokenApi) { + return Result.Err(`invalid auth type:[${req.auth.type}]`); + } + const rAuth = await tokenApi.verify(req.auth.token); + if (rAuth.isErr()) { + return Result.Err(rAuth.Err()); + } + // if (rAuth.Ok().type !== "clerk") { + // return Result.Err("invalid auth type"); + // } + const auth = rAuth.Ok() as ClerkVerifyAuth; + return Result.Ok(auth); + } + + private async activeUser(req: WithAuth, status: UserStatus[] = ["active"]): Promise> { + // console.log("activeUser-1", req); + const rAuth = await this._authVerifyAuth(req); + if (rAuth.isErr()) { + return Result.Err(rAuth.Err()); + } + const auth = rAuth.Ok(); + const rExisting = await getUser(this.db, auth.userId); + if (rExisting.isErr()) { + if (isUserNotFound(rExisting)) { + return Result.Ok({ + verifiedAuth: auth, + }); + } + return Result.Err(rExisting.Err()); + } + return Result.Ok({ + verifiedAuth: auth, + user: rExisting.Ok(), + }); + } + + async ensureUser(req: ReqEnsureUser): Promise> { + // console.log("ensureUser-1", req); + const activeUser = await this.activeUser(req); + // console.log("ensureUser-2", req); + if (activeUser.isErr()) { + return Result.Err(activeUser.Err()); + } + const user = activeUser.Ok().user; + if (!user) { + const auth = activeUser.Ok().verifiedAuth; + const userId = this.sthis.nextId(12).str; + const now = new Date(); + await upsetUserByProvider( + this.db, + { + userId, + maxTenants: 10, + status: "active", + statusReason: "just created", + byProviders: [ + { + providerUserId: auth.userId, + queryProvider: nickFromClarkClaim(auth.params) ? "github" : "google", + queryEmail: queryEmail(auth.params.email), + cleanEmail: auth.params.email, + queryNick: queryNick(nickFromClarkClaim(auth.params)), + cleanNick: nickFromClarkClaim(auth.params), + params: auth.params, + used: now, + }, + ], + }, + now, + ); + const authWithUserId = { + ...activeUser.Ok(), + user: { + userId, + maxTenants: 10, + }, + }; + const rTenant = await this.insertTenant(authWithUserId, { + ownerUserId: userId, + maxAdminUsers: 5, + maxMemberUsers: 5, + }); + const res = await this.addUserToTenant(this.db, { + userName: nameFromAuth(undefined, authWithUserId), + tenantId: rTenant.Ok().tenantId, + userId: userId, + role: "admin", + default: true, + }); + + // }); + return this.ensureUser(req); + } + return Result.Ok({ + type: "resEnsureUser", + user: user, + tenants: await this.listTenantsByUser({ + type: "reqListTenantsByUser", + auth: req.auth, + }).then((r) => r.Ok().tenants), + }); + } + + private async addUserToTenant(db: LibSQLDatabase, req: Omit): Promise> { + const tenant = await db + .select() + .from(sqlTenants) + .where(and(eq(sqlTenants.tenantId, req.tenantId), eq(sqlTenants.status, "active"))) + .get(); + if (!tenant) { + return Result.Err("tenant not found"); + } + const roles = await this.getRoles(req.userId, [tenant], []); + if (roles.length > 1) { + return Result.Err("multiple roles found"); + } + if (roles.length && roles[0].role) { + const tenantUser = await db + .select() + .from(sqlTenantUsers) + .where( + and( + eq(sqlTenantUsers.tenantId, req.tenantId), + eq(sqlTenantUsers.userId, req.userId), + eq(sqlTenantUsers.status, "active"), + ), + ) + .get(); + if (!tenantUser) { + return Result.Err("ref not found"); + } + return Result.Ok({ + userName: toUndef(tenantUser.name), + tenantName: toUndef(tenant.name), + tenantId: req.tenantId, + userId: req.userId, + default: !!tenantUser.default, + role: ps.cloud.toRole(tenantUser.role), + status: tenantUser.status as UserStatus, + statusReason: tenantUser.statusReason, + }); + } + const rCheck = await this.checkMaxRoles(tenant, req.role); + if (rCheck.isErr()) { + return Result.Err(rCheck.Err()); + } + const now = new Date().toISOString(); + if (req.default) { + await db + .update(sqlTenantUsers) + .set({ + default: 0, + updatedAt: now, + }) + .where(and(eq(sqlTenantUsers.userId, req.userId), ne(sqlTenantUsers.default, 0))) + .run(); + } + const ret = ( + await db + .insert(sqlTenantUsers) + .values({ + tenantId: tenant.tenantId, + userId: req.userId, + name: req.userName, + role: req.role, + default: req.default ? 1 : 0, + createdAt: now, + updatedAt: now, + }) + .returning() + )[0]; + return Result.Ok({ + userName: toUndef(ret.name), + tenantName: tenant.name, + tenantId: tenant.tenantId, + userId: ret.userId, + default: ret.default ? true : false, + status: ret.status as UserStatus, + statusReason: ret.statusReason, + role: ps.cloud.toRole(ret.role), + }); + } + + private async checkMaxRoles(sqlTenant: typeof sqlTenants.$inferSelect, reqRole: string): Promise> { + const tenantUsers = await this.db + .select() + .from(sqlTenantUsers) + .where(and(eq(sqlTenantUsers.tenantId, sqlTenant.tenantId), eq(sqlTenantUsers.status, "active"))) + .all(); + const ledgerUsers = await this.db + .select() + .from(sqlLedgers) + .innerJoin(sqlLedgerUsers, and(eq(sqlLedgerUsers.ledgerId, sqlLedgers.ledgerId), eq(sqlLedgerUsers.status, "active"))) + .where(eq(sqlLedgers.tenantId, sqlTenant.tenantId)) + .all(); + const adminUsers = new Set([ + ...tenantUsers.filter((tu) => tu.role === "admin"), + ...ledgerUsers.filter((lu) => lu.LedgerUsers.role === "admin"), + ]); + const memberUsers = Array.from( + new Set([...tenantUsers.filter((tu) => tu.role !== "admin"), ...ledgerUsers.filter((lu) => lu.LedgerUsers.role !== "admin")]), + ).filter((u) => !adminUsers.has(u)); + if (reqRole === "admin") { + if (adminUsers.size + 1 >= sqlTenant.maxAdminUsers) { + return Result.Err("max admins reached"); + } + } + if (reqRole !== "admin") { + if (memberUsers.length + 1 >= sqlTenant.maxMemberUsers) { + return Result.Err("max members reached"); + } + } + return Result.Ok(undefined); + } + + private async addUserToLedger(db: LibSQLDatabase, req: AddUserToLedger): Promise> { + const ledger = await db + .select() + .from(sqlLedgers) + .innerJoin(sqlTenants, and(eq(sqlLedgers.tenantId, sqlTenants.tenantId))) + .where(and(eq(sqlLedgers.ledgerId, req.ledgerId), eq(sqlLedgers.status, "active"))) + .get(); + if (!ledger) { + return Result.Err("ledger not found"); + } + const roles = await this.getRoles(req.userId, [], [ledger.Ledgers]); + if (roles.length > 1) { + return Result.Err("multiple roles found"); + } + if (roles.length && roles[0].role) { + const ledgerUser = await db + .select() + .from(sqlLedgerUsers) + .innerJoin(sqlLedgers, and(eq(sqlLedgerUsers.ledgerId, sqlLedgers.ledgerId))) + .where( + and( + eq(sqlLedgerUsers.ledgerId, req.ledgerId), + eq(sqlLedgerUsers.userId, req.userId), + eq(sqlLedgerUsers.status, "active"), + ), + ) + .get(); + if (!ledgerUser) { + return Result.Err("ref not found"); + } + return Result.Ok({ + ledgerName: toUndef(ledgerUser.Ledgers.name), + userName: toUndef(ledgerUser.LedgerUsers.name), + ledgerId: ledgerUser.Ledgers.ledgerId, + tenantId: ledgerUser.Ledgers.tenantId, + userId: req.userId, + default: !!ledgerUser.LedgerUsers.default, + status: ledgerUser.LedgerUsers.status as UserStatus, + statusReason: ledgerUser.LedgerUsers.statusReason, + role: ps.cloud.toRole(ledgerUser.LedgerUsers.role), + right: ps.cloud.toReadWrite(ledgerUser.LedgerUsers.right), + }); + } + const rCheck = await this.checkMaxRoles(ledger.Tenants, req.role); + if (rCheck.isErr()) { + return Result.Err(rCheck.Err()); + } + const now = new Date().toISOString(); + if (req.default) { + await db + .update(sqlLedgerUsers) + .set({ + default: 0, + updatedAt: now, + }) + .where(and(eq(sqlLedgerUsers.userId, req.userId), ne(sqlLedgerUsers.default, 0))) + .run(); + } + const ret = ( + await db + .insert(sqlLedgerUsers) + .values({ + ledgerId: ledger.Ledgers.ledgerId, + userId: req.userId, + name: req.userName, + role: req.role, + right: req.right, + default: req.default ? 1 : 0, + createdAt: now, + updatedAt: now, + }) + .returning() + )[0]; + return Result.Ok({ + ledgerName: ledger.Ledgers.name, + userName: req.userName, + ledgerId: ledger.Ledgers.ledgerId, + tenantId: ledger.Ledgers.tenantId, + status: ret.status as UserStatus, + statusReason: ret.statusReason, + userId: req.userId, + default: req.default ?? false, + role: ps.cloud.toRole(ret.role), + right: ps.cloud.toReadWrite(ret.right), + }); + } + + async listTenantsByUser(req: ReqListTenantsByUser): Promise> { + const rAUR = await this.activeUser(req); + if (rAUR.isErr()) { + return Result.Err(rAUR.Err()); + } + const aur = rAUR.Ok(); + if (!aur.user) { + return Result.Err(new UserNotFoundError()); + } + const tenantUsers = await this.db + .select() + .from(sqlTenantUsers) + .innerJoin(sqlTenants, and(eq(sqlTenantUsers.tenantId, sqlTenants.tenantId))) + .where(eq(sqlTenantUsers.userId, aur.user.userId)) + .all(); + // console.log(">>>>>", tenantUser); + + return Result.Ok({ + type: "resListTenantsByUser", + userId: aur.user.userId, + authUserId: aur.verifiedAuth.userId, + tenants: ( + await Promise.all( + tenantUsers.map(async (t) => { + const common = { + user: { + name: toUndef(t.TenantUsers.name), + status: t.TenantUsers.status as UserStatus, + statusReason: t.TenantUsers.statusReason, + createdAt: new Date(t.TenantUsers.createdAt), + updatedAt: new Date(t.TenantUsers.updatedAt), + }, + tenant: { + name: toUndef(t.Tenants.name), + status: t.Tenants.status as UserStatus, + statusReason: t.Tenants.statusReason, + createdAt: new Date(t.Tenants.createdAt), + updatedAt: new Date(t.Tenants.updatedAt), + }, + }; + const roles = await this.getRoles(t.TenantUsers.userId, [t.Tenants], []); + if (roles.length > 1) { + throw new Error("multiple roles found"); + } + if (!roles.length) { + return undefined; + } + switch (roles[0].role) { + case "member": + return { + ...common, + tenantId: t.TenantUsers.tenantId, + role: roles[0].role, + default: toBoolean(t.TenantUsers.default), + }; + // case "owner": + case "admin": + return { + ...common, + tenantId: t.TenantUsers.tenantId, + role: roles[0].role, + default: toBoolean(t.TenantUsers.default), + adminUserIds: roles[0].adminUserIds, + memberUserIds: roles[0].memberUserIds, + maxAdminUsers: t.Tenants.maxAdminUsers, + maxMemberUsers: t.Tenants.maxMemberUsers, + }; + default: + throw new Error("invalid role"); + } + }), + ) + ).filter((t) => !!t), + }); + } + + private async getRoles( + userId: string, + tenants: (typeof sqlTenants.$inferSelect)[], + ledgers: (typeof sqlLedgers.$inferSelect)[], + ): Promise { + if (!tenants.length && !ledgers.length) { + throw new Error("tenant or ledger required"); + } + // if (tenants && !tenants.length) { + // throw new Error("tenant not found"); + // } + // if (ledgers && !ledgers.length) { + // throw new Error("ledger not found"); + // } + + // let myLedgerUsers: { + // Ledgers: typeof sqlLedgers.$inferSelect + // LedgerUsers: typeof sqlLedgerUsers.$inferSelect + // }[] | undefined; + let ledgerUsersFilter = new Map< + string, + { + ledger: typeof sqlLedgers.$inferSelect; + users: (typeof sqlLedgerUsers.$inferSelect)[]; + my?: typeof sqlLedgerUsers.$inferSelect; + } + >(); + if (ledgers.length) { + const ledgerUsers = await this.db + .select() + .from(sqlLedgerUsers) + .innerJoin(sqlLedgers, eq(sqlLedgerUsers.ledgerId, sqlLedgers.ledgerId)) + .where( + and( + inArray( + sqlLedgerUsers.ledgerId, + this.db + .select({ ledgerId: sqlLedgerUsers.ledgerId }) + .from(sqlLedgerUsers) + .where( + and( + inArray( + sqlLedgerUsers.ledgerId, + ledgers.map((l) => l.ledgerId), + ), + eq(sqlLedgerUsers.userId, userId), + ), + ), + ), + eq(sqlLedgerUsers.status, "active"), + ), + ) + .all(); + const myLedgerUsers = ledgerUsers.filter((lu) => lu.LedgerUsers.userId === userId); + if (!myLedgerUsers.length) { + // throw new Error("user is not attached to ledger"); + return []; + } + ledgerUsersFilter = ledgerUsers.reduce((acc, lu) => { + let item = acc.get(lu.Ledgers.ledgerId); + if (!item) { + item = { + ledger: lu.Ledgers, + users: [], + }; + acc.set(lu.Ledgers.ledgerId, item); + } + if (lu.LedgerUsers.userId === userId) { + item.my = lu.LedgerUsers; + } + item.users.push(lu.LedgerUsers); + return acc; + }, ledgerUsersFilter); + // remove other users if you are not admin + Array.from(ledgerUsersFilter.values()).forEach((item) => { + item.users = item.users.filter((u) => item.my!.role === "admin" || (item.my!.role !== "admin" && u.userId === userId)); + }); + } + const tenantIds = ledgers.length + ? Array.from(ledgerUsersFilter.values()).map((lu) => lu.ledger.tenantId) + : (tenants?.map((t) => t.tenantId) ?? []); + + const q = this.db + .select() + .from(sqlTenantUsers) + .where( + and( + inArray( + sqlTenantUsers.tenantId, + this.db + .select({ tenantId: sqlTenantUsers.tenantId }) + .from(sqlTenantUsers) + .where(and(inArray(sqlTenantUsers.tenantId, tenantIds), eq(sqlTenantUsers.userId, userId))), + ), + eq(sqlTenantUsers.status, "active"), + ), + ); + + let tenantUsers = await q.all(); + // console.log(">>>>>>", tenantUsers.toString()); + const tenantUserFilter = tenantUsers.reduce( + (acc, lu) => { + let item = acc.get(lu.tenantId); + if (!item) { + item = { + users: [], + }; + acc.set(lu.tenantId, item); + } + if (lu.userId === userId) { + item.my = lu; + } + item.users.push(lu); + return acc; + }, + new Map< + string, + { + users: (typeof sqlTenantUsers.$inferSelect)[]; + my?: typeof sqlTenantUsers.$inferSelect; + } + >(), + ); + // remove other users if you are not admin + Array.from(tenantUserFilter.values()).forEach((item) => { + item.users = item.users.filter((u) => item.my!.role === "admin" || (item.my!.role !== "admin" && u.userId === userId)); + }); + + return [ + ...Array.from(tenantUserFilter.values()).map((item) => ({ + userId: userId, + tenantId: item.users[0].tenantId, + role: ps.cloud.toRole(item.my!.role), + adminUserIds: item.users.filter((u) => u.role === "admin").map((u) => u.userId), + memberUserIds: item.users.filter((u) => u.role !== "admin").map((u) => u.userId), + })), + ...Array.from(ledgerUsersFilter.values()).map((item) => ({ + userId: userId, + ledgerId: item.ledger.ledgerId, + role: ps.cloud.toRole(item.my!.role), + right: ps.cloud.toReadWrite(item.my!.right), + adminUserIds: item.users.filter((u) => u.role === "admin").map((u) => u.userId), + memberUserIds: item.users.filter((u) => u.role !== "admin").map((u) => u.userId), + })), + ]; + } + + async redeemInvite(req: ReqRedeemInvite): Promise> { + const rAuth = await this.activeUser(req); + if (rAuth.isErr()) { + return Result.Err(rAuth.Err()); + } + const auth = rAuth.Ok(); + if (!auth.user) { + return Result.Err(new UserNotFoundError()); + } + return Result.Ok({ + type: "resRedeemInvite", + invites: sqlToInviteTickets( + await Promise.all( + ( + await this.findInvite({ + query: { + byString: auth.verifiedAuth.params.email, + byNick: auth.verifiedAuth.params.nick, + existingUserId: auth.user.userId, + // TODO + // andProvider: auth.verifiedAuth.provider, + }, + }) + ) + .filter((i) => i.status === "pending") + .map(async (invite) => { + if (invite.invitedParams.tenant) { + const tenant = await this.db + .select() + .from(sqlTenants) + .where(and(eq(sqlTenants.tenantId, invite.invitedParams.tenant.id), eq(sqlTenants.status, "active"))) + .get(); + if (!tenant) { + throw new Error("tenant not found"); + } + await this.addUserToTenant(this.db, { + userName: `invited from [${tenant.name}]`, + tenantId: tenant.tenantId, + userId: auth.user!.userId, + role: invite.invitedParams.tenant.role, + }); + } + if (invite.invitedParams.ledger) { + const ledger = await this.db + .select() + .from(sqlLedgers) + .where(and(eq(sqlLedgers.ledgerId, invite.invitedParams.ledger.id), eq(sqlLedgers.status, "active"))) + .get(); + if (!ledger) { + throw new Error("ledger not found"); + } + await this.addUserToLedger(this.db, { + userName: `invited-${ledger.name}`, + ledgerId: ledger.ledgerId, + tenantId: ledger.tenantId, + userId: auth.user!.userId, + role: invite.invitedParams.ledger.role, + right: invite.invitedParams.ledger.right, + }); + } + return ( + await this.db + .update(sqlInviteTickets) + .set({ + invitedUserId: auth.user!.userId, + status: "accepted", + statusReason: `accepted: ${auth.user!.userId}`, + updatedAt: new Date().toISOString(), + }) + .where(eq(sqlInviteTickets.inviteId, invite.inviteId)) + .returning() + )[0]; + }), + ), + ), + }); + + // const invite = await this.db.select().from(sqlInviteTickets).where(eq(sqlInviteTickets.inviteId, req.inviteId)).get(); + // if (!invite) { + // return Result.Err("invite not found"); + // } + // if ((invite.invitedLedgerId && invite.invitedTenantId) || + // !(invite.invitedLedgerId invite.invitedTenantId)) { + // if (invite.invitedTenantId) { + // const res = await this.db + // .select() + // .from(sqlTenants) + // .innerJoin( + // sqlInviteTickets, + // and(eq(sqlTenants.tenantId, sqlInviteTickets.invitedTenantId), eq(sqlInviteTickets.invitedTenantId, req.tenantId)), + // ) + // .where(eq(sqlTenants.tenantId, req.tenantId)) + // .get(); + + // if (!res) { + // return Result.Err("tenant not found"); + // } + // } + + // // const invite = sqlToInvite(res.InviteTickets); + // const val = await this.addUserToTenant(this.db, { + // name: req.name, + // tenantId: res.Tenants.tenantId, + // userId: auth.user.userId, + // default: false, + // role: invite.invitedParams.tenant?.role ?? "member", + // }); + // await this._deleteInvite(invite.inviteId); + // return Result.Ok({ + // type: "resConnectUserToTenant", + // name: val.Ok().name ?? res.Tenants.name, + // tenant: sqlToOutTenantParams(res.Tenants), + // userId: auth.user.userId, + // role: invite.invitedParams.tenant?.role ?? "member", + // }); + } + + async findUser(req: ReqFindUser): Promise> { + const rAuth = await this.activeUser(req); + if (rAuth.isErr()) { + return Result.Err(rAuth.Err()); + } + const auth = rAuth.Ok(); + if (!auth.user) { + return Result.Err(new UserNotFoundError()); + } + const rRows = await queryUser(this.db, req.query); + return Result.Ok({ + type: "resFindUser", + query: req.query, + results: rRows.Ok(), + // .map( + // (row) => + // ({ + // userId: row.userId, + // authProvider: row.queryProvider as AuthProvider, + // email: row.queryEmail as string, + // nick: row.queryNick as string, + // status: row.status as UserStatus, + // createdAt: new Date(row.createdAt), + // updatedAt: new Date(row.updatedAt), + // }) satisfies QueryResultUser, + // ), + }); + } + + private async createInviteTicket( + userId: string, + tenantId: string, + ledgerId: string | undefined, + req: ReqInviteUser, + ): Promise> { + // check maxInvites + const allowed = await this.db + .select() + .from(sqlTenants) + .where( + and( + eq(sqlTenants.tenantId, tenantId), + gt(sqlTenants.maxInvites, this.db.$count(sqlInviteTickets, eq(sqlInviteTickets.invitedTenantId, tenantId))), + ), + ) + .get(); + if (!allowed) { + return Result.Err("max invites reached"); + } + + const found = await this.findInvite({ query: req.ticket.query, tenantId, ledgerId }); + if (found.length) { + return Result.Err("invite already exists"); + } + + let ivp: InvitedParams = {}; + if (req.ticket.invitedParams?.ledger) { + ivp = { + ledger: { + id: req.ticket.invitedParams?.ledger.id, + role: req.ticket.invitedParams?.ledger.role ?? "member", + right: req.ticket.invitedParams?.ledger.right ?? "read", + }, + }; + } + if (req.ticket.invitedParams?.tenant) { + ivp = { + tenant: { + id: req.ticket.invitedParams?.tenant.id, + role: req.ticket.invitedParams?.tenant.role ?? "member", + }, + }; + } + + return Result.Ok( + sqlToInviteTickets( + await this.db + .insert(sqlInviteTickets) + .values( + prepareInviteTicket({ + sthis: this.sthis, + userId, + invitedTicketParams: { + query: req.ticket.query, + status: "pending", + invitedParams: ivp, + }, + }), + ) + .returning(), + )[0], + ); + } + + private async updateInviteTicket( + userId: string, + tenantId: string, + ledgerId: string | undefined, + req: ReqInviteUser, + ): Promise> { + const found = await this.findInvite({ inviteId: req.ticket.inviteId }); + if (!found.length) { + return Result.Err("invite not found"); + } + const invite = found[0]; + if (invite.status !== "pending") { + return Result.Err("invite not pending"); + } + let ivp: InvitedParams = {}; + if (req.ticket.invitedParams?.ledger) { + ivp = { + ledger: { + ...invite.invitedParams.ledger, + ...req.ticket.invitedParams.ledger, + }, + }; + } + if (req.ticket.invitedParams?.tenant) { + ivp = { + tenant: { + ...invite.invitedParams.tenant, + ...req.ticket.invitedParams.tenant, + }, + }; + } + const toInsert = prepareInviteTicket({ + sthis: this.sthis, + userId: userId, + invitedTicketParams: { + query: req.ticket.query, + status: "pending", + invitedParams: ivp, + }, + }); + // might be update query + return Result.Ok( + sqlToInviteTickets( + await this.db + .update(sqlInviteTickets) + .set({ + sendEmailCount: req.ticket.incSendEmailCount ? invite.sendEmailCount + 1 : invite.sendEmailCount, + invitedParams: toInsert.invitedParams, + updatedAt: new Date().toISOString(), + }) + .where(eq(sqlInviteTickets.inviteId, invite.inviteId)) + .returning(), + )[0], + ); + } + + async inviteUser(req: ReqInviteUser): Promise> { + const rAuth = await this.activeUser(req); + if (rAuth.isErr()) { + return Result.Err(rAuth.Err()); + } + const auth = rAuth.Ok(); + if (!auth.user) { + return Result.Err(new UserNotFoundError()); + } + const findUser = await queryUser(this.db, req.ticket.query); + if (findUser.isErr()) { + return Result.Err(findUser.Err()); + } + if (req.ticket.query.existingUserId && findUser.Ok().length !== 1) { + return Result.Err("existingUserId not found"); + } + if (req.ticket.query.existingUserId === auth.user.userId) { + return Result.Err("cannot invite self"); + } + + if ( + req.ticket.invitedParams?.ledger && + req.ticket.invitedParams?.tenant && + !req.ticket.invitedParams?.ledger && + !req.ticket.invitedParams?.tenant + ) { + return Result.Err("either ledger or tenant must be set"); + } + let tenantId: string | undefined; + let ledgerId: string | undefined; + if (req.ticket.invitedParams?.ledger) { + const ledger = await this.db + .select() + .from(sqlLedgers) + .where(eq(sqlLedgers.ledgerId, req.ticket.invitedParams.ledger.id)) + .get(); + if (!ledger) { + return Result.Err("ledger not found"); + } + ledgerId = ledger.ledgerId; + tenantId = ledger.tenantId; + } + if (req.ticket.invitedParams?.tenant) { + const tenant = await this.db + .select() + .from(sqlTenants) + .where(eq(sqlTenants.tenantId, req.ticket.invitedParams.tenant.id)) + .get(); + if (!tenant) { + return Result.Err("tenant not found"); + } + tenantId = tenant.tenantId; + } + if (!tenantId) { + return Result.Err("tenant not found"); + } + + let inviteTicket: InviteTicket; + if (!req.ticket.inviteId) { + const rInviteTicket = await this.createInviteTicket(auth.user.userId, tenantId, ledgerId, req); + if (rInviteTicket.isErr()) { + return Result.Err(rInviteTicket.Err()); + } + inviteTicket = rInviteTicket.Ok(); + } else { + const rInviteTicket = await this.updateInviteTicket(auth.user.userId, tenantId, ledgerId, req); + if (rInviteTicket.isErr()) { + return Result.Err(rInviteTicket.Err()); + } + inviteTicket = rInviteTicket.Ok(); + } + return Result.Ok({ + type: "resInviteUser", + invite: inviteTicket, + }); + } + + private async findInvite(req: { + query?: QueryUser; + inviteId?: string; + tenantId?: string; + ledgerId?: string; + // now?: Date + }): Promise { + if (!(req.inviteId || req.query)) { + throw new Error("inviteId or query is required"); + } + if (req.tenantId && req.ledgerId) { + throw new Error("invite only possible to ledger or tenant"); + } + // housekeeping + await this.db + .update(sqlInviteTickets) + .set({ status: "expired" }) + .where(and(eq(sqlInviteTickets.status, "pending"), lt(sqlInviteTickets.expiresAfter, new Date().toISOString()))) + .run(); + let condition = and(); + // eq(sqlInviteTickets.status, "pending"), + // gt(sqlInviteTickets.expiresAfter, (req.now ?? new Date()).toISOString()), + + if (req.tenantId) { + condition = and(eq(sqlInviteTickets.invitedTenantId, req.tenantId), condition); + } + if (req.ledgerId) { + condition = and(eq(sqlInviteTickets.invitedLedgerId, req.ledgerId), condition); + } + if (req.inviteId) { + condition = and(eq(sqlInviteTickets.inviteId, req.inviteId), condition); + } + if (req.query) { + condition = and( + queryCondition(req.query, { + ...sqlInviteTickets, + userId: sqlInviteTickets.invitedUserId, + }), + condition, + ); + } + const rows = await this.db.select().from(sqlInviteTickets).where(condition).all(); + return sqlToInviteTickets(rows); + } + + /** + * + * @description list invites for a user if user is owner of tenant or admin of tenant + */ + async listInvites(req: ReqListInvites): Promise> { + // console.log(`xxxxx`) + const rAuth = await this.activeUser(req); + if (rAuth.isErr()) { + return Result.Err(rAuth.Err()); + } + const auth = rAuth.Ok(); + if (!auth.user) { + return Result.Err(new UserNotFoundError()); + } + let tenantCond = and(eq(sqlTenantUsers.userId, auth.user.userId), eq(sqlTenantUsers.status, "active")); + if (req.tenantIds?.length) { + tenantCond = and(inArray(sqlTenantUsers.tenantId, req.tenantIds), tenantCond); + } + const tenants = await this.db + .select() + .from(sqlTenantUsers) + .innerJoin(sqlTenants, and(eq(sqlTenants.tenantId, sqlTenantUsers.tenantId), eq(sqlTenants.status, "active"))) + .where(tenantCond) + .all(); + + let ledgerCond = and(eq(sqlLedgerUsers.userId, auth.user.userId), eq(sqlLedgerUsers.status, "active")); + if (req.ledgerIds?.length) { + ledgerCond = and(inArray(sqlLedgerUsers.ledgerId, req.ledgerIds), ledgerCond); + } + const ledgers = await this.db + .select() + .from(sqlLedgerUsers) + .innerJoin(sqlLedgers, and(eq(sqlLedgers.ledgerId, sqlLedgerUsers.ledgerId), eq(sqlLedgers.status, "active"))) + .where(ledgerCond) + .all(); + + if (!tenants.length && !ledgers.length) { + return Result.Ok({ + type: "resListInvites", + tickets: [], + }); + } + + const roles = await this.getRoles( + auth.user.userId, + tenants.map((i) => i.Tenants), + ledgers.map((i) => i.Ledgers), + ); + // list invites from all tenants where i'm owner or admin + const invites = await this.db + .select() + .from(sqlInviteTickets) + .where( + or( + inArray( + sqlInviteTickets.invitedTenantId, + roles + .filter((i) => i.role === "admin" && i.tenantId) + .map((i) => i.tenantId!) + .flat(2), + ), + inArray( + sqlInviteTickets.invitedLedgerId, + roles + .filter((i) => i.role === "admin" && i.ledgerId) + .map((i) => i.ledgerId!) + .flat(2), + ), + ), + ); + return Result.Ok({ + type: "resListInvites", + tickets: sqlToInviteTickets(invites), + }); + + // list invites from all ledgers where i'm owner or admin + + // this.db.select() + // .from(sqlTenants) + // .innerJoin(sqlTenantUsers, and( + // eq(sqlTenantUsers.userId, auth.user.userId), + // eq(sqlTenants.tenantId, sqlTenantUsers.tenantId), + // )) + // .innerJoin(sqlTenantUserRoles, and( + // eq(sqlTenantUsers.userId, auth.user.userId), + // eq(sqlTenants.tenantId, sqlTenantUsers.tenantId) + // )) + // .where( + // eq(sqlTenants.ownerUserId, auth.user.userId) + // ).all(); + + // this.db.select().from(sqlInviteTickets) + // .where( + // eq(sqlInviteTickets.inviterUserId, auth.user.userId) + // ) + // .all(); + + // let rows: (typeof sqlInviteTickets.$inferSelect)[]; + // const ownerTenants = await this.db + // .select() + // .from(sqlTenants) + // .where(eq(sqlTenants.ownerUserId, auth.user.userId)) + // .all() + // .then((rows) => rows.map((row) => row.tenantId)); + // // get admin in tenant for this user + // let condition = and(eq(sqlTenantUserRoles.userId, auth.user.userId), eq(sqlTenantUserRoles.role, "admin")); + // if (req.tenantIds.length) { + // // filter by tenantIds if set + // condition = and(inArray(sqlTenantUserRoles.tenantId, req.tenantIds), condition); + // } + // const adminTenants = await this.db + // .select() + // .from(sqlTenantUserRoles) + // .where(condition) + // .all() + // .then((rows) => rows.map((row) => row.tenantId)); + // const setTenants = new Set(req.tenantIds); + // const filterAdminTenants = Array.from(new Set([...ownerTenants, ...adminTenants, ...req.tenantIds])).filter((x) => { + // return setTenants.size ? setTenants.has(x) : true; + // }); + // // console.log(">>>>", filterAdminTenants); + // rows = await this.db + // .select() + // .from(sqlInviteTickets) + // .where( + // and( + // inArray(sqlInviteTickets.invitedTenantId, filterAdminTenants), + // // inArray(inviteTickets.inv, req.tenantIds) + // ), + // ) + // .all(); + // // } + // return Result.Ok({ + // type: "resListInvites", + // tickets: Array.from( + // rows + // .reduce((acc, row) => { + // if (!row.inviterTenantId) { + // throw new Error("inviterTenantId is required"); + // } + // const invites = acc.get(row.inviterTenantId) ?? []; + // invites.push(sqlToInvite(row)); + // acc.set(row.inviterTenantId, invites); + // return acc; + // }, new Map()) + // .entries(), + // ) + // .map(([tenantId, invites]) => ({ + // tenantId, + // invites, + // })) + // .filter((x) => x.invites.length), + // }); + } + + async deleteInvite(req: ReqDeleteInvite): Promise> { + const rAuth = await this.activeUser(req); + if (rAuth.isErr()) { + return Result.Err(rAuth.Err()); + } + const auth = rAuth.Ok(); + if (!auth.user) { + return Result.Err(new UserNotFoundError()); + } + await this._deleteInvite(req.inviteId); + return Result.Ok({ + type: "resDeleteInvite", + inviteId: req.inviteId, + }); + } + + private async _deleteInvite(inviteId: string): Promise> { + await this.db.delete(sqlInviteTickets).where(eq(sqlInviteTickets.inviteId, inviteId)).run(); + return Result.Ok(undefined); + } + + async updateUserTenant(req: ReqUpdateUserTenant): Promise> { + const rAuth = await this.activeUser(req); + if (rAuth.isErr()) { + return Result.Err(rAuth.Err()); + } + const auth = rAuth.Ok(); + if (!auth.user) { + return Result.Err(new UserNotFoundError()); + } + const userId = req.userId ?? auth.user.userId; + if (req.role && (await this.isAdminOfTenant(userId, req.tenantId))) { + await this.db + .update(sqlTenantUsers) + .set({ + role: req.role, + }) + .where(and(eq(sqlTenantUsers.userId, userId), eq(sqlTenantUsers.tenantId, req.tenantId))) + .run(); + } + if (req.default) { + await this.db + .update(sqlTenantUsers) + .set({ + default: 0, + }) + .where(eq(sqlTenantUsers.userId, userId)); + } + if (req.default || req.name) { + const updateSet = {} as { + default?: number; + name?: string; + }; + if (req.default) { + updateSet.default = req.default ? 1 : 0; + } + if (req.name) { + updateSet.name = req.name; + } + const ret = await this.db + .update(sqlTenantUsers) + .set(updateSet) + .where(and(eq(sqlTenantUsers.userId, userId), eq(sqlTenantUsers.tenantId, req.tenantId))) + .returning(); + } + const ret = await this.db + .select() + .from(sqlTenantUsers) + .innerJoin( + sqlTenantUsers, + and(eq(sqlTenantUsers.userId, sqlTenantUsers.userId), eq(sqlTenantUsers.tenantId, sqlTenantUsers.tenantId)), + ) + .where(and(eq(sqlTenantUsers.userId, userId), eq(sqlTenantUsers.tenantId, req.tenantId))) + .get(); + if (!ret) { + return Result.Err("not found"); + } + return Result.Ok({ + type: "resUpdateUserTenant", + tenantId: ret.TenantUsers.tenantId, + userId: ret.TenantUsers.userId, + role: ps.cloud.toRole(ret.TenantUsers.role), + default: !!ret.TenantUsers.default, + name: toUndef(ret.TenantUsers.name), + }); + } + + async createTenant(req: ReqCreateTenant): Promise> { + const rAuth = await this.activeUser(req); + if (rAuth.isErr()) { + return Result.Err(rAuth.Err()); + } + const auth = rAuth.Ok(); + if (!auth.user) { + return Result.Err(new UserNotFoundError()); + } + const rTenant = await this.insertTenant(auth as ActiveUserWithUserId, { + ...req.tenant, + ownerUserId: auth.user.userId, + }); + if (rTenant.isErr()) { + return Result.Err(rTenant.Err()); + } + const tenant = rTenant.Ok(); + await this.addUserToTenant(this.db, { + userName: nameFromAuth(req.tenant.name, auth as ActiveUserWithUserId), + tenantId: tenant.tenantId, + userId: auth.user.userId, + role: "admin", + default: false, + }); + return Result.Ok({ + type: "resCreateTenant", + tenant, + }); + } + + private async insertTenant(auth: ActiveUserWithUserId, req: InCreateTenantParams): Promise> { + const tenantId = this.sthis.nextId(12).str; + const cnt = await this.db.$count(sqlTenants, eq(sqlTenants.ownerUserId, auth.user.userId)); + if (cnt + 1 >= auth.user.maxTenants) { + return Result.Err("max tenants reached"); + } + const nowStr = new Date().toISOString(); + const values = await this.db + .insert(sqlTenants) + .values({ + tenantId, + name: req.name ?? `my-tenant[${tenantId}]`, + ownerUserId: auth.user.userId, + maxAdminUsers: req.maxAdminUsers ?? 5, + maxMemberUsers: req.maxMemberUsers ?? 5, + maxInvites: req.maxInvites ?? 10, + createdAt: nowStr, + updatedAt: nowStr, + }) + .returning(); + return Result.Ok(sqlToOutTenantParams(values[0])); + } + + async updateTenant(req: ReqUpdateTenant): Promise> { + const rAuth = await this.activeUser(req); + if (rAuth.isErr()) { + return Result.Err(rAuth.Err()); + } + const auth = rAuth.Ok(); + if (!auth.user) { + return Result.Err(new UserNotFoundError()); + } + const prev = await this.db.select().from(sqlTenants).where(eq(sqlTenants.tenantId, req.tenant.tenantId)).get(); + if (!prev) { + return Result.Err("tenant not found"); + } + if (!(await this.isAdminOfTenant(auth.user.userId, req.tenant.tenantId))) { + return Result.Err("not owner of tenant"); + } + const now = new Date().toISOString(); + const result = await this.db + .update(sqlTenants) + .set({ + name: req.tenant.name, + maxAdminUsers: req.tenant.maxAdminUsers, + maxMemberUsers: req.tenant.maxMemberUsers, + maxInvites: req.tenant.maxInvites, + updatedAt: now, + }) + .where(eq(sqlTenants.tenantId, req.tenant.tenantId)) + .returning(); + return Result.Ok({ + type: "resUpdateTenant", + tenant: sqlToOutTenantParams(result[0]), + }); + } + + async deleteTenant(req: ReqDeleteTenant): Promise> { + const rAuth = await this.activeUser(req); + if (rAuth.isErr()) { + return Result.Err(rAuth.Err()); + } + const auth = rAuth.Ok(); + if (!auth.user) { + return Result.Err(new UserNotFoundError()); + } + // check if owner or admin of tenant + if (!(await this.isAdminOfTenant(auth.user.userId, req.tenantId))) { + return Result.Err("not owner or admin of tenant"); + } + // TODO remove ledgers + await this.db.delete(sqlInviteTickets).where(eq(sqlInviteTickets.invitedTenantId, req.tenantId)).run(); + await this.db.delete(sqlTenantUsers).where(eq(sqlTenantUsers.tenantId, req.tenantId)).run(); + await this.db.delete(sqlTenants).where(eq(sqlTenants.tenantId, req.tenantId)).run(); + return Result.Ok({ + type: "resDeleteTenant", + tenantId: req.tenantId, + }); + } + + private async isAdminOfTenant(userId: string, tenantId: string): Promise { + const adminRole = await this.db + .select() + .from(sqlTenantUsers) + .where( + and( + eq(sqlTenantUsers.userId, userId), + eq(sqlTenantUsers.tenantId, tenantId), + eq(sqlTenantUsers.role, "admin"), + eq(sqlTenantUsers.status, "active"), + ), + ) + .get(); + return !!adminRole; + } + + private async isAdminOfLedger(userId: string, ledgerId: string): Promise { + const adminRole = await this.db + .select() + .from(sqlLedgerUsers) + .innerJoin(sqlLedgers, and(eq(sqlLedgers.ledgerId, sqlLedgerUsers.ledgerId))) + .where(and(eq(sqlLedgerUsers.userId, userId), eq(sqlLedgerUsers.ledgerId, ledgerId))) + .get(); + if (adminRole?.LedgerUsers.role === "member") { + return this.isAdminOfTenant(userId, adminRole.Ledgers.tenantId); + } + return adminRole?.LedgerUsers.role === "admin"; + } + + async createLedger(req: ReqCreateLedger): Promise> { + const rAuth = await this.activeUser(req); + if (rAuth.isErr()) { + return Result.Err(rAuth.Err()); + } + const auth = rAuth.Ok(); + if (!auth.user) { + return Result.Err(new UserNotFoundError()); + } + // check if owner or admin of tenant + if (!(await this.isAdminOfTenant(auth.user.userId, req.ledger.tenantId))) { + return Result.Err("not owner or admin of tenant"); + } + + const allowed = await this.db + .select() + .from(sqlTenants) + .where( + and( + eq(sqlTenants.tenantId, req.ledger.tenantId), + gt(sqlTenants.maxLedgers, this.db.$count(sqlLedgers, eq(sqlLedgers.tenantId, req.ledger.tenantId))), + ), + ) + .get(); + if (!allowed) { + return Result.Err("max ledgers per tenant reached"); + } + + const ledgerId = this.sthis.nextId(12).str; + const now = new Date().toISOString(); + const ledger = await this.db + .insert(sqlLedgers) + .values({ + ledgerId, + tenantId: req.ledger.tenantId, + ownerId: auth.user.userId, + name: req.ledger.name, + createdAt: now, + updatedAt: now, + }) + .returning(); + const roles = await this.db + .insert(sqlLedgerUsers) + .values({ + ledgerId: ledgerId, + userId: auth.user.userId, + role: "admin", + name: req.ledger.name, + default: 0, + right: "write", + createdAt: now, + updatedAt: now, + }) + .returning(); + + return Result.Ok({ + type: "resCreateLedger", + ledger: sqlToLedgers([{ Ledgers: ledger[0], LedgerUsers: roles[0] }])[0], + }); + } + async updateLedger(req: ReqUpdateLedger): Promise> { + const rAuth = await this.activeUser(req); + if (rAuth.isErr()) { + return Result.Err(rAuth.Err()); + } + const auth = rAuth.Ok(); + if (!auth.user) { + return Result.Err(new UserNotFoundError()); + } + const now = new Date().toISOString(); + // check if owner or admin of tenant + if (!(await this.isAdminOfLedger(auth.user.userId, req.ledger.ledgerId))) { + if (req.ledger.name) { + await this.db + .update(sqlLedgerUsers) + .set({ + name: req.ledger.name, + updatedAt: now, + }) + .where(and(eq(sqlLedgerUsers.userId, auth.user.userId), eq(sqlLedgerUsers.ledgerId, req.ledger.ledgerId))) + .run(); + } + const rows = await this.db + .select() + .from(sqlLedgers) + .innerJoin(sqlLedgerUsers, and(eq(sqlLedgers.ledgerId, sqlLedgerUsers.ledgerId))) + .where( + and( + eq(sqlLedgerUsers.userId, auth.user.userId), + eq(sqlLedgerUsers.ledgerId, req.ledger.ledgerId), + ne(sqlLedgerUsers.role, "admin"), + ), + ) + .all(); + return Result.Ok({ + type: "resUpdateLedger", + ledger: sqlToLedgers(rows)[0], + }); + } + const role = { + updatedAt: now, + } as { + readonly updatedAt: string; + default?: number; + name?: string; + role?: ps.cloud.Role; + right?: ps.cloud.ReadWrite; + }; + if (typeof req.ledger.default === "boolean") { + role.default = req.ledger.default ? 1 : 0; + if (req.ledger.default) { + // switch default + await this.db + .update(sqlLedgerUsers) + .set({ + default: 0, + updatedAt: now, + }) + .where(and(eq(sqlLedgerUsers.userId, auth.user.userId), ne(sqlLedgerUsers.default, 0))) + .run(); + } + } + const ledger = { + name: req.ledger.name, + updatedAt: now, + }; + if (req.ledger.name) { + role.name = req.ledger.name; + ledger.name = req.ledger.name; + } + if (req.ledger.right) { + role.right = req.ledger.right; + } + if (req.ledger.role) { + role.role = req.ledger.role; + } + const roles = await this.db + .update(sqlLedgerUsers) + .set(role) + .where(eq(sqlLedgerUsers.ledgerId, req.ledger.ledgerId)) + .returning(); + const ledgers = await this.db.update(sqlLedgers).set(ledger).where(eq(sqlLedgers.ledgerId, req.ledger.ledgerId)).returning(); + return Result.Ok({ + type: "resUpdateLedger", + ledger: sqlToLedgers([ + { + Ledgers: ledgers[0], + LedgerUsers: roles[0], + }, + ])[0], + }); + } + async deleteLedger(req: ReqDeleteLedger): Promise> { + const rAuth = await this.activeUser(req); + if (rAuth.isErr()) { + return Result.Err(rAuth.Err()); + } + const auth = rAuth.Ok(); + if (!auth.user) { + return Result.Err(new UserNotFoundError()); + } + const now = new Date().toISOString(); + // check if owner or admin of tenant + if (!(await this.isAdminOfLedger(auth.user.userId, req.ledger.ledgerId))) { + return Result.Err("not owner or admin of tenant"); + } + await this.db.delete(sqlLedgerUsers).where(eq(sqlLedgerUsers.ledgerId, req.ledger.ledgerId)).run(); + await this.db.delete(sqlLedgers).where(eq(sqlLedgers.ledgerId, req.ledger.ledgerId)).run(); + return Result.Ok({ + type: "resDeleteLedger", + ledgerId: req.ledger.ledgerId, + }); + } + async listLedgersByUser(req: ReqListLedgersByUser): Promise> { + const rAuth = await this.activeUser(req); + if (rAuth.isErr()) { + return Result.Err(rAuth.Err()); + } + const auth = rAuth.Ok(); + if (!auth.user) { + return Result.Err(new UserNotFoundError()); + } + const now = new Date().toISOString(); + let condition = and(eq(sqlLedgerUsers.userId, auth.user.userId)); + if (req.tenantIds && req.tenantIds.length) { + condition = and(condition, inArray(sqlLedgers.tenantId, req.tenantIds)); + } + const rows = await this.db + .select() + .from(sqlLedgers) + .innerJoin(sqlLedgerUsers, and(eq(sqlLedgers.ledgerId, sqlLedgerUsers.ledgerId))) + .where(condition) + .all(); + return Result.Ok({ + type: "resListLedgersByUser", + userId: auth.user.userId, + ledgers: sqlToLedgers(rows), + }); + } + + async getCloudSessionToken(req: ReqCloudSessionToken, ictx: Partial = {}): Promise> { + const resListTenants = await this.listTenantsByUser({ + type: "reqListTenantsByUser", + auth: req.auth, + }); + if (resListTenants.isErr()) { + return Result.Err(resListTenants.Err()); + } + + const resListLedgers = await this.listLedgersByUser({ + type: "reqListLedgersByUser", + auth: req.auth, + }); + + if (resListLedgers.isErr()) { + return Result.Err(resListLedgers.Err()); + } + const rCtx = await getFPTokenContext(this.sthis, ictx); + if (rCtx.isErr()) { + return Result.Err(rCtx.Err()); + } + const ctx = rCtx.Ok(); + const rAuth = await this.activeUser(req); + if (rAuth.isErr()) { + return Result.Err(rAuth.Err()); + } + const auth = rAuth.Ok(); + if (!auth.user) { + return Result.Err(new UserNotFoundError()); + } + + // verify if tenant and ledger are valid + const selected = { + tenant: resListTenants.Ok().tenants[0]?.tenantId, + ledger: resListLedgers.Ok().ledgers[0]?.ledgerId, + }; + if ( + req.selected?.tenant && + resListTenants + .Ok() + .tenants.map((i) => i.tenantId) + .includes(req.selected?.tenant) + ) { + selected.tenant = req.selected?.tenant; + } + if ( + req.selected?.ledger && + resListLedgers + .Ok() + .ledgers.map((i) => i.ledgerId) + .includes(req.selected?.ledger) + ) { + selected.ledger = req.selected?.ledger; + } + const token = await createFPToken(ctx, { + userId: auth.user.userId, + tenants: resListTenants.Ok().tenants.map((i) => ({ + id: i.tenantId, + role: i.role, + })), + ledgers: resListLedgers + .Ok() + .ledgers.map((i) => { + const rights = i.users.find((u) => u.userId === auth.user!.userId); + if (!rights) { + return undefined; + } + return { + id: i.ledgerId, + role: rights.role, + right: rights.right, + }; + }) + .filter((i) => i) as ps.cloud.FPCloudClaim["ledgers"], + email: auth.verifiedAuth.params.email, + nickname: auth.verifiedAuth.params.nick, + provider: toProvider(auth.verifiedAuth), + created: auth.user.createdAt, + selected: { + tenant: req.selected?.tenant ?? resListTenants.Ok().tenants[0]?.tenantId, + ledger: req.selected?.ledger ?? resListLedgers.Ok().ledgers[0]?.ledgerId, + }, + } satisfies ps.cloud.FPCloudClaim); + + // console.log("getCloudSessionToken", { + // result: req.resultId, + // }); + if (req.resultId && req.resultId.length > "laenger".length) { + await this.addTokenByResultId({ + status: "found", + resultId: req.resultId, + token, + now: new Date(), + }); + // console.log("getCloudSessionToken-ok", { + // result: req.resultId, + // }); + } else if (req.resultId) { + this.sthis.logger.Warn().Any({ resultId: req.resultId }).Msg("resultId too short"); + console.log("getCloudSessionToken-failed", { + result: req.resultId, + }); + } + // console.log(">>>>-post:", ctx, privKey) + return Result.Ok({ + type: "resCloudSessionToken", + token, + }); + } + + async addTokenByResultId(req: TokenByResultIdParam): Promise> { + const now = (req.now ?? new Date()).toISOString(); + await this.db + .insert(sqlTokenByResultId) + .values({ + resultId: req.resultId, + status: req.status, + token: req.token, + createdAt: now, + updatedAt: now, + }) + .onConflictDoUpdate({ + target: [sqlTokenByResultId.resultId], + set: { + updatedAt: now, + resultId: req.resultId, + token: req.token, + status: req.status, + }, + }) + .run(); + const past = new Date(new Date(now).getTime() - 15 * 60 * 1000).toISOString(); + await this.db.delete(sqlTokenByResultId).where(lt(sqlTokenByResultId.updatedAt, past)).run(); + return Result.Ok({ + type: "resTokenByResultId", + ...req, + }); + } + + // this is why to expensive --- why not kv or other simple storage + async getTokenByResultId(req: ReqTokenByResultId): Promise> { + const past = new Date(new Date().getTime() - 15 * 60 * 1000).toISOString(); + const out = await this.db + .select() + .from(sqlTokenByResultId) + .where(and(eq(sqlTokenByResultId.resultId, req.resultId), gte(sqlTokenByResultId.updatedAt, past))) + .get(); + if (!out || out.status !== "found" || !out.token) { + return Result.Ok({ + type: "resTokenByResultId", + resultId: req.resultId, + status: "not-found", + }); + } + await this.db.delete(sqlTokenByResultId).where(eq(sqlTokenByResultId.resultId, req.resultId)).run(); + return Result.Ok({ + type: "resTokenByResultId", + resultId: out.resultId, + token: out.token, + status: "found", + }); + } + + /** + * Extract token from request, validate it, and extend expiry by 1 day + */ + async extendToken(req: ReqExtendToken, ictx: Partial = {}): Promise> { + const rCtx = await getFPTokenContext(this.sthis, ictx); + if (rCtx.isErr()) { + return Result.Err(rCtx.Err()); + } + const ctx = rCtx.Ok(); + try { + // Get the public key for verification + const pubKey = await rt.sts.env2jwk(ctx.publicToken, "ES256"); + + // Verify the token + const verifyResult = await jwtVerify(req.token, pubKey, { + issuer: ctx.issuer, + audience: ctx.audience, + }); + const payload = verifyResult.payload as ps.cloud.FPCloudClaim; + + // Check if token is expired + const now = Date.now(); + if (!payload.exp || payload.exp * 1000 <= now) { + return Result.Err("Token is expired"); + } + // Create new token with extended expiry using the private key + // JWT expects expiration time in seconds, not milliseconds + const newToken = await createFPToken( + { + ...ctx, + validFor: ctx.extendValidFor, + }, + payload, + ); + return Result.Ok({ + type: "resExtendToken", + token: newToken, + }); + } catch (error) { + return Result.Err(`Token validation failed: ${error instanceof Error ? error.message : String(error)}`); + } + } +} + +function toProvider(i: ClerkVerifyAuth): ps.cloud.FPCloudClaim["provider"] { + if (i.params.nick) { + return "github"; + } + return "google"; +} + +// // eslint-disable-next-line @typescript-eslint/no-unused-vars +// async attachUserToTenant(req: ReqAttachUserToTenant): Promise> { +// const maxTenants = await this.db.select({ +// maxTenants: users.maxTenants +// }).from(users).where(eq(users.userId, req.userId)).get() ?? { maxTenants: 5 } + +// const tendantCount = await this.db.$count(tenantUsers, +// and( +// eq(tenants.ownerUserId, req.userId), +// ne(tenantUsers.active, 0) +// )) + +// if (tendantCount >= maxTenants.maxTenants) { +// return Result.Err(`max tenants reached:${maxTenants.maxTenants}`) +// } + +// const now = new Date().toISOString(); +// const values = { +// userId: req.userId, +// tenantId: req.tenantId, +// name: req.name, +// active: 1, +// createdAt: now, +// updatedAt: now +// } +// const rRes = await this.db +// .insert(tenantUsers) +// .values(values) +// .onConflictDoNothing() +// .returning() +// .run() +// const res = rRes.toJSON()[0] +// return Result.Ok({ +// type: 'resAttachUserToTenant', +// name: req.name, +// tenant: { +// tenantId: res. +// name: req.name, +// ownerUserId: req.userId, +// adminUserIds: [], +// memberUserIds: [], +// maxAdminUsers: 5, +// maxMemberUsers: 5, +// createdAt: new Date(), +// updatedAt: new Date() +// }, +// userId: req.userId, +// role: req.role +// }) + +// // throw new Error("Method not implemented."); +// } +// // eslint-disable-next-line @typescript-eslint/no-unused-vars +// async listLedgersByTenant(req: ReqListLedgerByTenant): Promise { +// throw new Error("Method not implemented."); +// } +// // eslint-disable-next-line @typescript-eslint/no-unused-vars +// async attachUserToLedger(req: ReqAttachUserToLedger): Promise { +// throw new Error("Method not implemented."); +// } diff --git a/backend/cf-serve.ts b/backend/cf-serve.ts new file mode 100644 index 0000000..bcf7de1 --- /dev/null +++ b/backend/cf-serve.ts @@ -0,0 +1,65 @@ +import { drizzle } from "drizzle-orm/d1"; +import { D1Database, Fetcher, Request as CFRequest, Response as CFResponse } from "@cloudflare/workers-types"; +import { base58btc } from "multiformats/bases/base58"; +import { CORS, createHandler } from "./create-handler.ts"; +import { URI } from "@adviser/cement"; + +export interface Env { + DB: D1Database; + // CLERK_SECRET_KEY: string; + ASSETS: Fetcher; + /** ES256 public JWK used to verify Fireproof session tokens */ + CLOUD_SESSION_TOKEN_PUBLIC: string; +} +export default { + async fetch(request: Request, env: Env) { + const uri = URI.from(request.url); + let ares: Promise; + switch (true) { + // Well-known JWKS endpoint for public verification keys + case uri.pathname === "/.well-known/jwks.json": { + const jwkRaw = env.CLOUD_SESSION_TOKEN_PUBLIC; + let body: string; + let status = 200; + try { + const decodedKey = base58btc.decode(jwkRaw); + const jwk = JSON.parse(decodedKey.toString()); + body = JSON.stringify({ keys: [jwk] }); + } catch { + body = "Invalid CLOUD_SESSION_TOKEN_PUBLIC"; + status = 500; + } + const cfRes = new Response(body, { + status, + headers: { + "content-type": "application/json", + ...CORS, + }, + }) as unknown as CFResponse; + ares = Promise.resolve(cfRes); + break; + } + + case uri.pathname.startsWith("/api"): + // console.log("cf-serve", request.url, env); + ares = createHandler(drizzle(env.DB), env)(request) as unknown as Promise; + break; + + case uri.pathname.startsWith("/fp-logo.svg"): + case uri.pathname.startsWith("/assets/"): + ares = env.ASSETS.fetch(request as unknown as CFRequest); + break; + default: + ares = env.ASSETS.fetch(uri.build().pathname("/").asURL(), request as unknown as CFRequest); + } + const res = await ares; + return new Response(res.body as ReadableStream, { + status: res.status, + statusText: res.statusText, + headers: { + ...res.headers, + ...CORS, + }, + }); + }, +}; diff --git a/backend/create-fp-token.ts b/backend/create-fp-token.ts new file mode 100644 index 0000000..cc49354 --- /dev/null +++ b/backend/create-fp-token.ts @@ -0,0 +1,51 @@ +import { param, Result } from "@adviser/cement"; +import { rt, ps, SuperThis } from "@fireproof/core"; +import { SignJWT } from "jose/jwt/sign"; + +export interface FPTokenContext { + readonly secretToken: string; + readonly publicToken: string; + readonly issuer: string; + readonly audience: string; + readonly validFor: number; // seconds + readonly extendValidFor: number; // seconds +} + +export async function createFPToken(ctx: FPTokenContext, claim: ps.cloud.FPCloudClaim) { + const privKey = await rt.sts.env2jwk(ctx.secretToken, "ES256"); + let validFor = ctx.validFor; + if (validFor <= 0) { + validFor = 60 * 60; // 1 hour + } + return new SignJWT(claim) + .setProtectedHeader({ alg: "ES256" }) // algorithm + .setIssuedAt() + .setIssuer(ctx.issuer) // issuer + .setAudience(ctx.audience) // audience + .setExpirationTime(Math.floor((Date.now() + validFor * 1000) / 1000)) // expiration time + .sign(privKey); +} + +export async function getFPTokenContext(sthis: SuperThis, ictx: Partial = {}): Promise> { + const rCtx = sthis.env.gets({ + CLOUD_SESSION_TOKEN_SECRET: ictx.secretToken ?? param.REQUIRED, + CLOUD_SESSION_TOKEN_PUBLIC: ictx.publicToken ?? param.REQUIRED, + CLOUD_SESSION_TOKEN_ISSUER: "FP_CLOUD", + CLOUD_SESSION_TOKEN_AUDIENCE: "PUBLIC", + CLOUD_SESSION_TOKEN_VALID_FOR: "" + 60 * 60, + CLOUD_SESSION_TOKEN_EXTEND_VALID_FOR: "" + 6 * 60 * 60, + }); + if (rCtx.isErr()) { + return Result.Err(rCtx.Err()); + } + const ctx = rCtx.Ok(); + return Result.Ok({ + secretToken: ctx.CLOUD_SESSION_TOKEN_SECRET, + publicToken: ctx.CLOUD_SESSION_TOKEN_PUBLIC, + issuer: ctx.CLOUD_SESSION_TOKEN_ISSUER, + audience: ctx.CLOUD_SESSION_TOKEN_AUDIENCE, + validFor: parseInt(ctx.CLOUD_SESSION_TOKEN_VALID_FOR, 10), + extendValidFor: parseInt(ctx.CLOUD_SESSION_TOKEN_EXTEND_VALID_FOR, 10), + ...ictx, + } satisfies FPTokenContext); +} diff --git a/backend/create-handler.ts b/backend/create-handler.ts new file mode 100644 index 0000000..2b534d9 --- /dev/null +++ b/backend/create-handler.ts @@ -0,0 +1,250 @@ +// import { auth } from "./better-auth.ts"; +import { LoggerImpl, Result, exception2Result } from "@adviser/cement"; +import { verifyToken } from "@clerk/backend"; +import { SuperThis, SuperThisOpts, ensureLogger, ensureSuperThis, ps } from "@fireproof/core"; +import type { LibSQLDatabase } from "drizzle-orm/libsql"; +import { FPAPIMsg, FPApiSQL, FPApiToken } from "./api.ts"; +import type { Env } from "./cf-serve.ts"; +// import { jwtVerify } from "jose/jwt/verify"; +// import { JWK } from "jose"; + +export const CORS = { + "Access-Control-Allow-Origin": "*", + "Access-Control-Allow-Methods": "GET,POST,OPTIONS,PUT,DELETE", + "Access-Control-Allow-Headers": "Origin, Content-Type, Accept", + "Access-Control-Max-Age": "86400", +}; + +interface ClerkTemplate { + readonly app_metadata: {}; + readonly azp: string; + readonly exp: number; + readonly iat: number; + readonly iss: string; + readonly jti: string; + readonly nbf: number; + readonly role: string; + readonly sub: string; + readonly params: { + readonly email: string; + readonly first: string; + readonly last: string; + readonly name: null; + }; +} + +class ClerkApiToken implements FPApiToken { + readonly sthis: SuperThis; + constructor(sthis: SuperThis) { + this.sthis = sthis; + } + async verify(token: string): Promise> { + const jwtKey = this.sthis.env.get("CLERK_PUB_JWT_KEY"); + if (!jwtKey) { + return Result.Err("Invalid CLERK_PUB_JWT_KEY"); + } + const rt = await exception2Result(async () => { + return (await verifyToken(token, { jwtKey })) as unknown as ClerkTemplate; + }); + if (rt.isErr()) { + return Result.Err(rt.Err()); + } + const t = rt.Ok(); + return Result.Ok({ + type: "clerk", + token, + userId: t.sub, + provider: "TBD", + params: { + ...t.params, + }, + }); + } +} + +// class BetterApiToken implements FPApiToken { +// readonly sthis: SuperThis; +// readonly pk?: JWK; +// constructor(sthis: SuperThis) { +// this.sthis = sthis; +// try { +// this.pk = JSON.parse(this.sthis.env.get("BETTER_PUBLICSHABLE_KEY")!) as JWK; +// } catch (e) { +// this.sthis.logger.Error().Err(e).Msg("Invalid BETTER_PUBLICSHABLE_KEY"); +// } +// } +// async verify(token: string): Promise> { +// if (!this.pk) { +// return Result.Err("Invalid BETTER_PUBLICSHABLE_KEY"); +// } +// const rAuth = await jwtVerify(token, this.pk); +// console.log("rAuth", rAuth); +// if (!rAuth || !rAuth.payload.sub) { +// return Result.Err("invalid token"); +// } +// const params = (rAuth.payload as { params: ClerkClaim }).params; +// return Result.Ok({ +// type: "better", +// provider: "better", +// token, +// userId: rAuth.payload.sub as string, +// params, +// }); +// } +// } + +export function createHandler(db: T, env: Record | Env) { + // const stream = new utils.ConsoleWriterStream(); + const sthis = ensureSuperThis({ + logger: new LoggerImpl(), + } as unknown as SuperThisOpts); + sthis.env.sets(env as unknown as Record); + const logger = ensureLogger(sthis, "createHandler"); + const fpApi = new FPApiSQL(sthis, db, { + clerk: new ClerkApiToken(sthis), + // better: new BetterApiToken(sthis), + }); + return async (req: Request): Promise => { + const startTime = performance.now(); + if (req.method === "OPTIONS") { + return new Response("ok", { + status: 200, + headers: { + ...CORS, + "Content-Type": "application/json", + }, + }); + } + if (!["POST", "PUT"].includes(req.method)) { + return new Response("Invalid request", { status: 404, headers: CORS }); + } + const rJso = await exception2Result(async () => await req.json()); + if (rJso.isErr()) { + logger.Error().Err(rJso.Err()).Msg("Error"); + return new Response("Invalid request", { status: 404, headers: CORS }); + } + const jso = rJso.Ok(); + + // console.log(jso); + let res: Promise>; + switch (true) { + case FPAPIMsg.isDeleteTenant(jso): + res = fpApi.deleteTenant(jso); + break; + case FPAPIMsg.isUpdateTenant(jso): + res = fpApi.updateTenant(jso); + break; + case FPAPIMsg.isCreateTenant(jso): + res = fpApi.createTenant(jso); + break; + case FPAPIMsg.isDeleteInvite(jso): + res = fpApi.deleteInvite(jso); + break; + case FPAPIMsg.isListInvites(jso): + res = fpApi.listInvites(jso); + break; + case FPAPIMsg.isInviteUser(jso): + res = fpApi.inviteUser(jso); + break; + case FPAPIMsg.isFindUser(jso): + res = fpApi.findUser(jso); + break; + case FPAPIMsg.isRedeemInvite(jso): + res = fpApi.redeemInvite(jso); + break; + case FPAPIMsg.isEnsureUser(jso): + res = fpApi.ensureUser(jso); + break; + case FPAPIMsg.isListTenantsByUser(jso): + res = fpApi.listTenantsByUser(jso); + break; + case FPAPIMsg.isUpdateUserTenant(jso): + res = fpApi.updateUserTenant(jso); + break; + case FPAPIMsg.isListLedgersByUser(jso): + res = fpApi.listLedgersByUser(jso); + break; + + case FPAPIMsg.isCreateLedger(jso): + res = fpApi.createLedger(jso); + break; + + case FPAPIMsg.isUpdateLedger(jso): + res = fpApi.updateLedger(jso); + break; + + case FPAPIMsg.isDeleteLedger(jso): + res = fpApi.deleteLedger(jso); + break; + + case FPAPIMsg.isCloudSessionToken(jso): + res = fpApi.getCloudSessionToken(jso); + break; + + case FPAPIMsg.isReqTokenByResultId(jso): + res = fpApi.getTokenByResultId(jso); + break; + + case FPAPIMsg.isReqExtendToken(jso): + res = fpApi.extendToken(jso); + break; + + default: + return new Response("Invalid request", { status: 400, headers: CORS }); + } + try { + const rRes = await res; + // console.log("Response", rRes); + if (rRes.isErr()) { + logger.Error().Any({ request: jso.type }).Err(rRes).Msg("Result-Error"); + const endTime = performance.now(); + const duration = endTime - startTime; + return new Response( + JSON.stringify({ + type: "error", + message: rRes.Err().message, + }), + { + status: 500, + headers: { + ...CORS, + "Server-Timing": `total;dur=${duration.toFixed(2)}`, + }, + }, + ); + } + logger + .Info() + .Any({ request: jso.type, response: (rRes.Ok() as { type: string }).type }) + .Msg("Success"); + const endTime = performance.now(); + const duration = endTime - startTime; + return new Response(JSON.stringify(rRes.Ok()), { + status: 200, + headers: { + ...CORS, + "Content-Type": "application/json", + "Server-Timing": `total;dur=${duration.toFixed(2)}`, + }, + }); + } catch (e) { + logger.Error().Any({ request: jso.type }).Err(e).Msg("Error"); + const endTime = performance.now(); + const duration = endTime - startTime; + return new Response( + JSON.stringify({ + type: "error", + message: (e as Error).message, + }), + { + status: 500, + headers: { + ...CORS, + "Content-Type": "application/json", + "Server-Timing": `total;dur=${duration.toFixed(2)}`, + }, + }, + ); + } + }; +} diff --git a/backend/db-api-schema.ts b/backend/db-api-schema.ts new file mode 100644 index 0000000..bdae82f --- /dev/null +++ b/backend/db-api-schema.ts @@ -0,0 +1,5 @@ +export * from "./users.ts"; +export * from "./tenants.ts"; +export * from "./ledgers.ts"; +export * from "./invites.ts"; +export * from "./token-by-result-id.ts"; diff --git a/backend/db-api.test.ts b/backend/db-api.test.ts new file mode 100644 index 0000000..6395145 --- /dev/null +++ b/backend/db-api.test.ts @@ -0,0 +1,1030 @@ +// import { describe } from 'vitest/globals'; + +// import { BetterSQLite3Database, drizzle } from "drizzle-orm/better-sqlite3"; + +// import Database from 'better-sqlite3'; +// import { eq } from 'drizzle-orm'; +// import { userRef } from "./db-api-schema"; + +import { Result } from "@adviser/cement"; +import { SuperThis, ensureSuperThis, rt } from "@fireproof/core"; +import { createClient } from "@libsql/client/node"; +import { type LibSQLDatabase, drizzle } from "drizzle-orm/libsql"; +import { jwtVerify } from "jose/jwt/verify"; +import { FPApiSQL, type FPApiToken } from "./api.js"; +import { + type AdminTenant, + type AuthType, + type QueryUser, + type ReqEnsureUser, + type ResEnsureUser, + type VerifiedAuth, +} from "./fp-dash-types.ts"; +import { queryEmail, queryNick } from "./sql-helper.ts"; + +// // import { eq } from 'drizzle-orm' +// // import { drizzle } from 'drizzle-orm/libsql'; +// // import Database from 'better-sqlite3'; + +// const client = createClient({ +// url: '' +// }); +// export const db = drizzle(client); + +// const users = sqliteTable('users', { +// id: integer('id').primaryKey(), +// name: text('full_name'), +// }); + +// // const sqlite = new Database('sqlite.db'); +// // const db = drizzle({ client: sqlite }); + +// db.select().from(users).all(); +// db.select().from(users).where(eq(users.id, 42)).get(); + +class TestApiToken implements FPApiToken { + readonly sthis: SuperThis; + constructor(sthis: SuperThis) { + this.sthis = sthis; + } + verify(token: string): Promise> { + const id = `userId-${token}`; + return Promise.resolve( + Result.Ok({ + type: "clerk", + token, + userId: id, + provider: "Clerk", + params: { + email: `test${id}@test.de`, + first: `first${id}`, + last: `last${id}`, + name: `nick${id}`, + nick: `nick${id}`, + }, + }), + ); + } +} + +describe("db-api", () => { + // let db: BetterSQLite3Database + let db: LibSQLDatabase; + const sthis = ensureSuperThis(); + let fpApi: FPApiSQL; + const data = [] as { + reqs: ReqEnsureUser; + ress: ResEnsureUser; + }[]; + beforeAll(async () => { + const client = createClient({ url: `file://${process.cwd()}/dist/sqlite.db` }); + db = drizzle(client); + fpApi = new FPApiSQL(sthis, db, { clerk: new TestApiToken(sthis) }); + + data.push( + ...Array(10) + .fill(0) + .map((_, i) => ({ + ress: {} as ResEnsureUser, + reqs: { + type: "reqEnsureUser", + auth: { + token: `test-${i}-${sthis.nextId().str}`, + type: "clerk", + }, + } satisfies ReqEnsureUser, + })), + ); + for (const d of data) { + const rRes = await fpApi.ensureUser(d.reqs); + const res = rRes.Ok(); + d.ress = res; + // console.log("res", res); + expect(res).toEqual({ + type: "resEnsureUser", + user: { + byProviders: [ + { + cleanEmail: res.user.byProviders[0].cleanEmail, + cleanNick: res.user.byProviders[0].cleanNick, + createdAt: res.user.byProviders[0].createdAt, + params: res.user.byProviders[0].params, + providerUserId: `userId-${d.reqs.auth.token}`, + queryEmail: queryEmail(res.user.byProviders[0].cleanEmail), + queryNick: queryNick(res.user.byProviders[0].cleanNick), + queryProvider: "github", + updatedAt: res.user.byProviders[0].updatedAt, + used: res.user.byProviders[0].used, + }, + ], + createdAt: res.user.createdAt, + maxTenants: 10, + status: "active", + statusReason: "just created", + updatedAt: res.user.updatedAt, + userId: res.user.userId, + }, + tenants: [ + { + adminUserIds: [res.user.userId], + default: true, + maxAdminUsers: 5, + maxMemberUsers: 5, + memberUserIds: [], + role: "admin", + tenantId: res.tenants[0].tenantId, + user: res.tenants[0].user, + tenant: res.tenants[0].tenant, + }, + ], + }); + } + }); + it("check ensureUser", async () => { + for (const d of data.map((d) => d.reqs)) { + const rRes = await fpApi.ensureUser(d); + const res = rRes.Ok(); + expect(res).toEqual({ + type: "resEnsureUser", + user: { + byProviders: [ + { + cleanEmail: res.user.byProviders[0].cleanEmail, + cleanNick: res.user.byProviders[0].cleanNick, + createdAt: res.user.byProviders[0].createdAt, + params: res.user.byProviders[0].params, + providerUserId: `userId-${d.auth.token}`, + queryEmail: queryEmail(res.user.byProviders[0].cleanEmail), + queryNick: queryNick(res.user.byProviders[0].cleanNick), + queryProvider: "github", + updatedAt: res.user.byProviders[0].updatedAt, + used: res.user.byProviders[0].used, + }, + ], + createdAt: res.user.createdAt, + maxTenants: 10, + status: "active", + statusReason: "just created", + updatedAt: res.user.updatedAt, + userId: res.user.userId, + }, + tenants: [ + { + adminUserIds: [res.user.userId], + default: true, + maxAdminUsers: 5, + maxMemberUsers: 5, + memberUserIds: [], + role: "admin", + tenantId: res.tenants[0].tenantId, + user: res.tenants[0].user, + tenant: res.tenants[0].tenant, + }, + ], + }); + } + }); + it("should list tenants by user", async () => { + for (const d of data) { + const rRes = await fpApi.listTenantsByUser({ + type: "reqListTenantsByUser", + auth: d.reqs.auth, + }); + const res = rRes.Ok(); + const ownerTenant = d.ress.tenants[0] as AdminTenant; + expect(res).toEqual({ + authUserId: d.ress.user.byProviders[0].providerUserId, + tenants: [ + { + user: d.ress.tenants[0].user, + tenant: d.ress.tenants[0].tenant, + adminUserIds: ownerTenant.adminUserIds, + memberUserIds: ownerTenant.memberUserIds, + maxAdminUsers: 5, + maxMemberUsers: 5, + default: true, + role: "admin", + tenantId: d.ress.tenants[0].tenantId, + }, + ], + type: "resListTenantsByUser", + userId: d.ress.user.userId, + }); + } + }); + + it("invite to self", async () => { + const auth: AuthType = data[0].reqs.auth; + // const key = `test@${sthis.nextId().str}.de`; + const resinsert = await fpApi.inviteUser({ + type: "reqInviteUser", + auth, + ticket: { + // inviterTenantId: data[0].ress.tenants[0].tenantId, + query: { + existingUserId: data[0].ress.user.userId, + }, + invitedParams: { + tenant: { + id: data[0].ress.tenants[0].tenantId, + role: "admin", + }, + }, + }, + }); + expect(resinsert.Err().message).toEqual("cannot invite self"); + }); + + it("invite to not existing id", async () => { + const auth: AuthType = data[0].reqs.auth; + // const key = `test@${sthis.nextId().str}.de`; + const resinsert = await fpApi.inviteUser({ + type: "reqInviteUser", + auth, + ticket: { + query: { + existingUserId: "not-existing", + }, + invitedParams: { + tenant: { + id: data[0].ress.tenants[0].tenantId, + role: "admin", + }, + }, + }, + }); + expect(resinsert.Err().message).toEqual("existingUserId not found"); + }); + + it("invite existing user to a tenant", async () => { + const auth: AuthType = data[0].reqs.auth; + // const key = `test@${sthis.nextId().str}.de`; + const resinsert = await fpApi.inviteUser({ + type: "reqInviteUser", + auth, + ticket: { + query: { + existingUserId: data[1].ress.user.userId, + }, + invitedParams: { + tenant: { + id: data[0].ress.tenants[0].tenantId, + role: "member", + }, + }, + }, + }); + expect(resinsert.Ok()).toEqual({ + invite: { + createdAt: resinsert.Ok().invite.createdAt, + expiresAfter: resinsert.Ok().invite.expiresAfter, + inviteId: resinsert.Ok().invite.inviteId, + // invitedLedgerId: undefined, + invitedParams: { + tenant: { + id: data[0].ress.tenants[0].tenantId, + role: "member", + }, + }, + // inviterTenantId: data[0].ress.tenants[0].tenantId, + invitedUserId: data[1].ress.user.userId, + inviterUserId: data[0].ress.user.userId, + query: { + andProvider: undefined, + byEmail: undefined, + byNick: undefined, + existingUserId: data[1].ress.user.userId, + }, + status: "pending", + statusReason: "just invited", + sendEmailCount: 0, + updatedAt: resinsert.Ok().invite.updatedAt, + // userID: data[1].ress.user.userId, + }, + type: "resInviteUser", + }); + }); + + it("invite non existing user to a tenant", async () => { + const auth: AuthType = data[0].reqs.auth; + const key = `test@${sthis.nextId().str}.de`; + const resinsert = await fpApi.inviteUser({ + type: "reqInviteUser", + auth, + ticket: { + query: { + byEmail: key, + }, + invitedParams: { + tenant: { + id: data[0].ress.tenants[0].tenantId, + role: "admin", + }, + }, + }, + }); + expect(resinsert.isOk()).toBeTruthy(); + const resupdate = await fpApi.inviteUser({ + type: "reqInviteUser", + auth, + ticket: { + inviteId: resinsert.Ok().invite.inviteId, + incSendEmailCount: true, + query: { + // to be ignored + byEmail: `test@${sthis.nextId().str}.de`, + byNick: `nick${sthis.nextId().str}`, + }, + invitedParams: { + tenant: { + id: data[0].ress.tenants[0].tenantId, + role: "member", + }, + }, + }, + }); + expect(resinsert.Ok().invite.createdAt.getTime()).toBeLessThan(resupdate.Ok().invite.expiresAfter.getTime()); + expect(resupdate.Ok()).toEqual({ + invite: { + createdAt: resinsert.Ok().invite.createdAt, + expiresAfter: resinsert.Ok().invite.expiresAfter, + inviteId: resinsert.Ok().invite.inviteId, + // invitedLedgerId: undefined, + invitedParams: { + ledger: undefined, + tenant: { + id: data[0].ress.tenants[0].tenantId, + role: "member", + }, + }, + // inviterTenantId: data[0].ress.tenants[0].tenantId, + inviterUserId: data[0].ress.user.userId, + query: { + andProvider: undefined, + byEmail: queryEmail(key), + byNick: undefined, + existingUserId: undefined, + }, + sendEmailCount: 1, + status: "pending", + statusReason: "just invited", + updatedAt: resupdate.Ok().invite.updatedAt, + userID: undefined, + }, + type: "resInviteUser", + }); + }); + + it("invite non existing user to a ledger", async () => {}); + + it("try find an user by string(email)", async () => { + const res = await fpApi.findUser({ + type: "reqFindUser", + auth: data[0].reqs.auth, + query: { + byString: data[0].ress.user.byProviders[0].cleanEmail, + }, + }); + expect(res.Ok()).toEqual({ + type: "resFindUser", + query: { + byString: data[0].ress.user.byProviders[0].cleanEmail, + }, + results: [data[0].ress.user], + }); + }); + + it("try find an user by string(nick)", async () => { + const res = await fpApi.findUser({ + type: "reqFindUser", + auth: data[0].reqs.auth, + query: { + byString: data[0].ress.user.byProviders[0].cleanNick, + }, + }); + expect(res.Ok()).toEqual({ + type: "resFindUser", + query: { + byString: data[0].ress.user.byProviders[0].cleanNick, + }, + results: [data[0].ress.user], + }); + }); + + it("try find an user by string(userId)", async () => { + const res = await fpApi.findUser({ + type: "reqFindUser", + auth: data[0].reqs.auth, + query: { + byString: data[0].ress.user.userId, + }, + }); + expect(res.Ok()).toEqual({ + type: "resFindUser", + query: { + byString: data[0].ress.user.userId, + }, + results: [data[0].ress.user], + }); + }); + + it("try find a existing user", async () => { + const res = await fpApi.findUser({ + type: "reqFindUser", + auth: data[0].reqs.auth, + query: { + byEmail: "exact@email.com", + byNick: "exactnick", + andProvider: "fp", + }, + }); + expect(res.Ok()).toEqual({ + type: "resFindUser", + query: { + andProvider: "fp", + byEmail: "exact@email.com", + byNick: "exactnick", + }, + results: [], + }); + }); + + it("find by id", async () => { + const query = { + existingUserId: data[0].ress.user.userId, + } satisfies QueryUser; + const res = await fpApi.findUser({ + type: "reqFindUser", + auth: data[0].reqs.auth, + query, + }); + expect(res.Ok()).toEqual({ + type: "resFindUser", + query, + results: [data[0].ress.user], + }); + }); + + it("find a per email", async () => { + const query = { + byEmail: data[0].ress.user.byProviders[0].cleanEmail, + }; + const res = await fpApi.findUser({ + type: "reqFindUser", + auth: data[0].reqs.auth, + query, + }); + expect(res.Ok()).toEqual({ + type: "resFindUser", + query, + results: [data[0].ress.user], + }); + }); + + it("find a per nick", async () => { + const query = { + byNick: data[0].ress.user.byProviders[0].cleanNick, + }; + const res = await fpApi.findUser({ + type: "reqFindUser", + auth: data[0].reqs.auth, + query, + }); + expect(res.Ok()).toEqual({ + type: "resFindUser", + query, + results: [data[0].ress.user], + }); + }); + + it("CRUD tenant", async () => { + const tenant = await fpApi.createTenant({ + type: "reqCreateTenant", + auth: data[0].reqs.auth, + tenant: { + // ownerUserId: data[0].ress.user.userId, + }, + }); + expect(tenant.Ok()).toEqual({ + tenant: { + createdAt: tenant.Ok().tenant.createdAt, + maxAdminUsers: 5, + maxInvites: 10, + maxLedgers: 5, + maxMemberUsers: 5, + name: tenant.Ok().tenant.name, + ownerUserId: data[0].ress.user.userId, + status: "active", + statusReason: "just created", + tenantId: tenant.Ok().tenant.tenantId, + updatedAt: tenant.Ok().tenant.updatedAt, + }, + type: "resCreateTenant", + }); + const rUpdate = await fpApi.updateTenant({ + type: "reqUpdateTenant", + auth: data[0].reqs.auth, + tenant: { + tenantId: tenant.Ok().tenant.tenantId, + name: "new name", + }, + }); + expect(rUpdate.isOk()).toBeTruthy(); + + const listOwnersTenant = await fpApi.listTenantsByUser({ + type: "reqListTenantsByUser", + auth: data[0].reqs.auth, + }); + const myOwnersTenant = listOwnersTenant.Ok().tenants.filter((i) => i.tenantId === tenant.Ok().tenant.tenantId); + expect(myOwnersTenant.length).toEqual(1); + expect(myOwnersTenant[0]).toEqual({ + adminUserIds: [data[0].ress.user.userId], + default: false, + maxAdminUsers: 5, + maxMemberUsers: 5, + memberUserIds: [], + user: myOwnersTenant[0].user, + role: "admin", + tenant: { + ...myOwnersTenant[0].tenant, + name: "new name", + }, + tenantId: tenant.Ok().tenant.tenantId, + }); + const invite = await fpApi.inviteUser({ + type: "reqInviteUser", + auth: data[0].reqs.auth, + ticket: { + query: { + existingUserId: data[1].ress.user.userId, + }, + invitedParams: { + tenant: { + id: tenant.Ok().tenant.tenantId, + role: "member", + }, + }, + }, + }); + const rRedeem = await fpApi.redeemInvite({ + type: "reqRedeemInvite", + auth: data[1].reqs.auth, + }); + expect(rRedeem.isOk()).toBeTruthy(); + const rRedeemedInvites = rRedeem.Ok().invites?.find((i) => i.inviteId === invite.Ok().invite.inviteId); + if (!rRedeemedInvites) { + throw new Error("Redeemed invite not found"); + } + expect(rRedeemedInvites).toEqual({ + createdAt: rRedeemedInvites.createdAt, + expiresAfter: rRedeemedInvites.expiresAfter, + inviteId: rRedeemedInvites.inviteId, + invitedParams: { + tenant: { + id: rRedeemedInvites.invitedParams.tenant?.id, + role: "member", + }, + }, + invitedUserId: data[1].ress.user.userId, + inviterUserId: rRedeemedInvites.inviterUserId, + query: { + andProvider: undefined, + byEmail: undefined, + byNick: undefined, + existingUserId: rRedeemedInvites.query.existingUserId, + }, + sendEmailCount: 0, + status: "accepted", + statusReason: rRedeemedInvites.statusReason, + updatedAt: rRedeemedInvites.updatedAt, + }); + + const listInvites = await fpApi.listInvites({ + type: "reqListInvites", + auth: data[0].reqs.auth, + tenantIds: [tenant.Ok().tenant.tenantId], + }); + + expect( + listInvites.Ok().tickets.filter((i) => i.inviteId === rRedeemedInvites.inviteId), + + // .tickets.filter((i) => i.invitedParams.tenant?.id === rUpdate.Ok().tenant.tenantId) + // .filter((i) => i.inviteId === invite.Ok().invite.inviteId), + ).toEqual([ + { + createdAt: rRedeemedInvites.createdAt, + expiresAfter: rRedeemedInvites.expiresAfter, + inviteId: rRedeemedInvites.inviteId, + invitedParams: { + tenant: { + id: rRedeemedInvites.invitedParams.tenant?.id, + role: "member", + }, + }, + invitedUserId: rRedeemedInvites.invitedUserId, + inviterUserId: rRedeemedInvites.inviterUserId, + query: { + andProvider: undefined, + byEmail: undefined, + byNick: undefined, + existingUserId: rRedeemedInvites.query.existingUserId, + }, + sendEmailCount: 0, + status: "accepted", + statusReason: rRedeemedInvites.statusReason, + updatedAt: rRedeemedInvites.updatedAt, + }, + ]); + + const tenantWithNew = await fpApi.listTenantsByUser({ + type: "reqListTenantsByUser", + auth: data[1].reqs.auth, + }); + const myWith = tenantWithNew.Ok().tenants.filter((i) => i.tenantId === tenant.Ok().tenant.tenantId); + expect(myWith).toEqual([ + { + default: false, + user: { + ...myWith[0].user, + name: "invited from [new name]", + }, + + role: "member", + tenant: { + ...myWith[0].tenant, + name: "new name", + }, + tenantId: tenant.Ok().tenant.tenantId, + }, + ]); + const rDelete = await fpApi.deleteTenant({ + type: "reqDeleteTenant", + auth: data[0].reqs.auth, + tenantId: tenant.Ok().tenant.tenantId, + }); + expect(rDelete.isOk()).toBeTruthy(); + const tenantWithoutNew = await fpApi.listTenantsByUser({ + type: "reqListTenantsByUser", + auth: data[0].reqs.auth, + }); + expect(tenantWithoutNew.Ok().tenants.filter((i) => i.tenantId === tenant.Ok().tenant.tenantId).length).toBe(0); + + const tickets = await fpApi.listInvites({ + type: "reqListInvites", + auth: data[0].reqs.auth, + tenantIds: [tenant.Ok().tenant.tenantId], + }); + expect( + tickets + .Ok() + .tickets.filter((i) => i.invitedParams.tenant?.id === rUpdate.Ok().tenant.tenantId) + .map((i) => i.inviteId === invite.Ok().invite.inviteId), + ).toEqual([]); + }); + + it("listInvites with a user with all tenants", async () => {}); + + it("listInvites with one tenant per user", async () => { + const invites = await Promise.all( + data.slice(3).map(async (d) => { + return ( + await fpApi.inviteUser({ + type: "reqInviteUser", + auth: d.reqs.auth, + ticket: { + query: { + existingUserId: data[0].ress.user.userId, + }, + invitedParams: { + tenant: { + id: d.ress.tenants[0].tenantId, + role: "member", + }, + }, + }, + }) + ).Ok().invite; + }), + ); + for (let didx = 0; didx < data.length - 3; ++didx) { + const d = data[didx + 3]; + const res = await fpApi.listInvites({ + type: "reqListInvites", + auth: d.reqs.auth, + tenantIds: [data.slice(3)[didx].ress.tenants[0].tenantId], + // .map((i) => i.ress.tenants[0].tenantId), + }); + expect(res.Ok()).toEqual({ + type: "resListInvites", + tickets: [invites[didx]], + }); + } + await Promise.all( + data.slice(3).map(async (d, didx) => { + return fpApi.deleteInvite({ type: "reqDeleteInvite", auth: d.reqs.auth, inviteId: invites[didx].inviteId }); + }), + ); + for (let didx = 0; didx < data.length - 3; ++didx) { + const d = data[didx + 3]; + const res = await fpApi.listInvites({ + type: "reqListInvites", + auth: d.reqs.auth, + tenantIds: data.slice(3).map((i) => i.ress.tenants[0].tenantId), + }); + expect(res.Ok()).toEqual({ + type: "resListInvites", + tickets: [], + }); + } + }); + + it("CRUD an ledger", async () => { + const createLedger = await fpApi.createLedger({ + type: "reqCreateLedger", + auth: data[0].reqs.auth, + ledger: { + tenantId: data[0].ress.tenants[0].tenantId, + name: `ledger[${data[0].ress.tenants[0].tenantId}]`, + }, + }); + expect(createLedger.Ok()).toEqual({ + ledger: { + createdAt: createLedger.Ok().ledger.createdAt, + ledgerId: createLedger.Ok().ledger.ledgerId, + maxShares: 5, + name: `ledger[${data[0].ress.tenants[0].tenantId}]`, + ownerId: data[0].ress.user.userId, + users: [ + { + createdAt: createLedger.Ok().ledger.users[0].createdAt, + default: false, + name: `ledger[${data[0].ress.tenants[0].tenantId}]`, + right: "write", + role: "admin", + updatedAt: createLedger.Ok().ledger.users[0].updatedAt, + userId: data[0].ress.user.userId, + }, + ], + tenantId: data[0].ress.tenants[0].tenantId, + updatedAt: createLedger.Ok().ledger.updatedAt, + }, + type: "resCreateLedger", + }); + const rUpdate = await fpApi.updateLedger({ + type: "reqUpdateLedger", + auth: data[0].reqs.auth, + ledger: { + name: "new name", + right: "read", + role: "member", + default: true, + ledgerId: createLedger.Ok().ledger.ledgerId, + tenantId: data[0].ress.tenants[0].tenantId, + }, + }); + expect(rUpdate.isOk()).toBeTruthy(); + + const listOwnersLedger = await fpApi.listLedgersByUser({ + type: "reqListLedgersByUser", + auth: data[0].reqs.auth, + }); + const myOwnersLedger = listOwnersLedger.Ok().ledgers.filter((i) => i.ledgerId === createLedger.Ok().ledger.ledgerId); + expect(myOwnersLedger.length).toEqual(1); + expect(myOwnersLedger[0]).toEqual({ + createdAt: createLedger.Ok().ledger.createdAt, + ledgerId: createLedger.Ok().ledger.ledgerId, + maxShares: 5, + name: "new name", + ownerId: data[0].ress.user.userId, + users: [ + { + createdAt: createLedger.Ok().ledger.users[0].createdAt, + default: true, + name: "new name", + right: "read", + role: "member", + updatedAt: rUpdate.Ok().ledger.updatedAt, + userId: data[0].ress.user.userId, + }, + ], + tenantId: data[0].ress.tenants[0].tenantId, + updatedAt: rUpdate.Ok().ledger.updatedAt, + }); + + const rDelete = await fpApi.deleteLedger({ + type: "reqDeleteLedger", + auth: data[0].reqs.auth, + ledger: { + ledgerId: createLedger.Ok().ledger.ledgerId, + tenantId: data[0].ress.tenants[0].tenantId, + }, + }); + + const afterListOwnersLedger = await fpApi.listLedgersByUser({ + type: "reqListLedgersByUser", + auth: data[0].reqs.auth, + }); + const myAfterDelete = afterListOwnersLedger.Ok().ledgers.filter((i) => i.ledgerId === createLedger.Ok().ledger.ledgerId); + expect(myAfterDelete.length).toEqual(0); + }); + + it("create session with claim", async () => { + const auth: AuthType = data[0].reqs.auth; + // fpApi.sthis.env.set("CLOUD_SESSION_TOKEN_SECRET", " + + const resultId = sthis.nextId(12).str; + const rledger = await fpApi.createLedger({ + type: "reqCreateLedger", + auth: data[0].reqs.auth, + ledger: { + tenantId: data[0].ress.tenants[0].tenantId, + name: `Session Ledger`, + }, + }); + await fpApi.updateLedger({ + type: "reqUpdateLedger", + auth: data[0].reqs.auth, + ledger: { + ledgerId: rledger.Ok().ledger.ledgerId, + tenantId: data[0].ress.tenants[0].tenantId, + name: `Session X-Ledger`, + right: "read", + role: "member", + }, + }); + + const res1 = await fpApi.getTokenByResultId({ + type: "reqTokenByResultId", + resultId, + }); + expect(res1.Ok()).toEqual({ + type: "resTokenByResultId", + resultId, + status: "not-found", + }); + + const resSt = await fpApi.getCloudSessionToken( + { + type: "reqCloudSessionToken", + auth, + resultId, + selected: { + ledger: rledger.Ok().ledger.ledgerId, + tenant: data[0].ress.tenants[0].tenantId, + }, + }, + { + secretToken: + "z33KxHvFS3jLz72v9DeyGBqo7H34SCC1RA5LvQFCyDiU4r4YBR4jEZxZwA9TqBgm6VB5QzwjrZJoVYkpmHgH7kKJ6Sasat3jTDaBCkqWWfJAVrBL7XapUstnKW3AEaJJKvAYWrKYF9JGqrHNU8WVjsj3MZNyqqk8iAtTPPoKtPTLo2c657daVMkxibmvtz2egnK5wPeYEUtkbydrtBzteN25U7zmGqhS4BUzLjDiYKMLP8Tayi", + publicToken: + "zeWndr5LEoaySgKSo2aZniYqcrEJBPswFRe3bwyxY7Nmr3bznXkHhFm77VxHprvCskpKVHEwVzgQpM6SAYkUZpZcEdEunwKmLUYd1yJ4SSteExyZw4GC1SvJPLDpGxKBKb6jkkCsaQ3MJ5YFMKuGUkqpKH31Dw7cFfjdQr5XUiXue", + issuer: "TEST_I", + audience: "TEST_A", + validFor: 40000000, + }, + ); + expect(resSt.isOk()).toBeTruthy(); + const pub = await rt.sts.env2jwk( + "zeWndr5LEoaySgKSo2aZniYqcrEJBPswFRe3bwyxY7Nmr3bznXkHhFm77VxHprvCskpKVHEwVzgQpM6SAYkUZpZcEdEunwKmLUYd1yJ4SSteExyZw4GC1SvJPLDpGxKBKb6jkkCsaQ3MJ5YFMKuGUkqpKH31Dw7cFfjdQr5XUiXue", + "ES256", + ); + const v = await jwtVerify(resSt.Ok().token, pub); + expect(v.payload.exp).toBeLessThanOrEqual(new Date().getTime() + 3700000); + + const res2 = await fpApi.getTokenByResultId({ + type: "reqTokenByResultId", + resultId, + }); + expect(res2.Ok()).toEqual({ + type: "resTokenByResultId", + resultId, + status: "found", + token: resSt.Ok().token, + }); + + const res3 = await fpApi.getTokenByResultId({ + type: "reqTokenByResultId", + resultId, + }); + expect(res3.Ok()).toEqual({ + type: "resTokenByResultId", + resultId, + status: "not-found", + }); + + expect(v.payload).toEqual({ + aud: "TEST_A", + created: v.payload.created, + email: v.payload.email, + nickname: v.payload.nickname, + provider: "github", + selected: v.payload.selected, + exp: v.payload.exp, + iat: v.payload.iat, + iss: "TEST_I", + ledgers: [ + { + id: rledger.Ok().ledger.ledgerId, + right: "read", + role: "member", + }, + ], + tenants: [ + { + id: data[0].ress.tenants[0].tenantId, + role: "admin", + }, + ], + userId: data[0].ress.user.userId, + }); + + await fpApi.deleteLedger({ + type: "reqDeleteLedger", + auth: data[0].reqs.auth, + ledger: { + ledgerId: rledger.Ok().ledger.ledgerId, + tenantId: data[0].ress.tenants[0].tenantId, + }, + }); + }); + + it("extend token with 6 hours expiry", async () => { + const auth: AuthType = data[0].reqs.auth; + + // Create a session token first + const resSt = await fpApi.getCloudSessionToken( + { + type: "reqCloudSessionToken", + auth, + selected: { + tenant: data[0].ress.tenants[0].tenantId, + }, + }, + { + secretToken: + "z33KxHvFS3jLz72v9DeyGBqo7H34SCC1RA5LvQFCyDiU4r4YBR4jEZxZwA9TqBgm6VB5QzwjrZJoVYkpmHgH7kKJ6Sasat3jTDaBCkqWWfJAVrBL7XapUstnKW3AEaJJKvAYWrKYF9JGqrHNU8WVjsj3MZNyqqk8iAtTPPoKtPTLo2c657daVMkxibmvtz2egnK5wPeYEUtkbydrtBzteN25U7zmGqhS4BUzLjDiYKMLP8Tayi", + publicToken: + "zeWndr5LEoaySgKSo2aZniYqcrEJBPswFRe3bwyxY7Nmr3bznXkHhFm77VxHprvCskpKVHEwVzgQpM6SAYkUZpZcEdEunwKmLUYd1yJ4SSteExyZw4GC1SvJPLDpGxKBKb6jkkCsaQ3MJ5YFMKuGUkqpKH31Dw7cFfjdQr5XUiXue", + issuer: "TEST_I", + audience: "TEST_A", + validFor: 3600000, // 1 hour + }, + ); + expect(resSt.isOk()).toBeTruthy(); + + const validFor = 40000; // 40000 seconds + // Extend the token + const extendResult = await fpApi.extendToken( + { + type: "reqExtendToken", + token: resSt.Ok().token, + }, + { + secretToken: + "z33KxHvFS3jLz72v9DeyGBqo7H34SCC1RA5LvQFCyDiU4r4YBR4jEZxZwA9TqBgm6VB5QzwjrZJoVYkpmHgH7kKJ6Sasat3jTDaBCkqWWfJAVrBL7XapUstnKW3AEaJJKvAYWrKYF9JGqrHNU8WVjsj3MZNyqqk8iAtTPPoKtPTLo2c657daVMkxibmvtz2egnK5wPeYEUtkbydrtBzteN25U7zmGqhS4BUzLjDiYKMLP8Tayi", + publicToken: + "zeWndr5LEoaySgKSo2aZniYqcrEJBPswFRe3bwyxY7Nmr3bznXkHhFm77VxHprvCskpKVHEwVzgQpM6SAYkUZpZcEdEunwKmLUYd1yJ4SSteExyZw4GC1SvJPLDpGxKBKb6jkkCsaQ3MJ5YFMKuGUkqpKH31Dw7cFfjdQr5XUiXue", + issuer: "TEST_I", + audience: "TEST_A", + validFor: 60 * 60, // 1 hour + extendValidFor: validFor, // 40000 seconds (approximately 6 hours) + }, + ); + + if (extendResult.isErr()) { + console.log("extendToken error:", extendResult.Err()); + } + expect(extendResult.isOk()).toBeTruthy(); + const extendedResponse = extendResult.Ok(); + + // Verify the response structure + expect(extendedResponse.type).toBe("resExtendToken"); + expect(typeof extendedResponse.token).toBe("string"); + + // Verify the new token is valid and has extended expiry + const pub = await rt.sts.env2jwk( + "zeWndr5LEoaySgKSo2aZniYqcrEJBPswFRe3bwyxY7Nmr3bznXkHhFm77VxHprvCskpKVHEwVzgQpM6SAYkUZpZcEdEunwKmLUYd1yJ4SSteExyZw4GC1SvJPLDpGxKBKb6jkkCsaQ3MJ5YFMKuGUkqpKH31Dw7cFfjdQr5XUiXue", + "ES256", + ); + const verifyExtended = await jwtVerify(extendedResponse.token, pub); + + // Check that the new expiry is approximately 6 hours from now + const tokenExpiry = verifyExtended.payload.exp ?? 0; + + // Allow for some variance (within 1 minute) + expect(Math.abs(tokenExpiry - Date.now() / 1000) - validFor).toBeLessThanOrEqual(60); + + // Verify the payload content is preserved + expect(verifyExtended.payload.userId).toBe(data[0].ress.user.userId); + expect(verifyExtended.payload.iss).toBe("TEST_I"); + expect(verifyExtended.payload.aud).toBe("TEST_A"); + }); +}); + +it("queryEmail strips +....@", async () => { + expect(queryEmail("a.C@b.de")).toBe("ac@b.de"); + expect(queryEmail("a.C+@b.de")).toBe("ac@b.de"); + expect(queryEmail("a.C+bla@b.de")).toBe("ac@b.de"); + expect(queryEmail("a.C+huhu+@b.de")).toBe("achuhu@b.de"); + expect(queryEmail("a.C+huhu+bla@b.de")).toBe("achuhu@b.de"); +}); diff --git a/backend/deno-serve.ts b/backend/deno-serve.ts new file mode 100644 index 0000000..fd0becb --- /dev/null +++ b/backend/deno-serve.ts @@ -0,0 +1,22 @@ +// deno run --unstable-sloppy-imports --allow-net --allow-read --allow-ffi --allow-env backend/deno-serve.ts +import { createClient } from "@libsql/client/node"; +import { createHandler } from "./create-handler.ts"; +import { drizzle } from "drizzle-orm/libsql"; + +function getClient() { + console.log(`file://${process.cwd()}/dist/sqlite.db`); + const client = createClient({ url: `file://${process.cwd()}/dist/sqlite.db` }); + return drizzle(client); +} + +async function main() { + Deno.serve({ + port: 7370, + handler: createHandler(getClient(), Deno.env.toObject()), + }); +} + +main().catch((err) => { + console.error(err); + Deno.exit(1); +}); diff --git a/backend/fp-dash-types.ts b/backend/fp-dash-types.ts new file mode 100644 index 0000000..fd207ef --- /dev/null +++ b/backend/fp-dash-types.ts @@ -0,0 +1,78 @@ +import { ps } from "@fireproof/core"; + +export type Queryable = ps.dashboard.Queryable; + +export type LedgerUser = ps.dashboard.LedgerUser; +export type UserTenant = ps.dashboard.UserTenant; + +export type AuthProvider = ps.dashboard.AuthProvider; +export type InviteTicketStatus = ps.dashboard.InviteTicketStatus; +export type SqlInvitedParams = ps.dashboard.SqlInvitedParams; + +export type VerifiedAuth = ps.dashboard.VerifiedAuth; +export type QueryUser = ps.dashboard.QueryUser; +export type User = ps.dashboard.User; +export type UserStatus = ps.dashboard.UserStatus; +export type OutTenantParams = ps.dashboard.OutTenantParams; +export type AuthType = ps.dashboard.AuthType; +export type ClerkClaim = ps.dashboard.ClerkClaim; +export type RoleType = ps.dashboard.RoleType; +export type ClerkVerifyAuth = ps.dashboard.ClerkVerifyAuth; +export type InviteTicket = ps.dashboard.InviteTicket; +export type InvitedParams = ps.dashboard.InvitedParams; +export type InCreateTenantParams = ps.dashboard.InCreateTenantParams; +export type AdminTenant = ps.dashboard.AdminTenant; + +export type ReqFindUser = ps.dashboard.ReqFindUser; +export type ResFindUser = ps.dashboard.ResFindUser; + +export type ReqEnsureUser = ps.dashboard.ReqEnsureUser; +export type ResEnsureUser = ps.dashboard.ResEnsureUser; + +export type ReqCreateTenant = ps.dashboard.ReqCreateTenant; +export type ResCreateTenant = ps.dashboard.ResCreateTenant; + +export type ReqUpdateTenant = ps.dashboard.ReqUpdateTenant; +export type ResUpdateTenant = ps.dashboard.ResUpdateTenant; + +export type ReqDeleteTenant = ps.dashboard.ReqDeleteTenant; +export type ResDeleteTenant = ps.dashboard.ResDeleteTenant; + +export type ReqRedeemInvite = ps.dashboard.ReqRedeemInvite; +export type ResRedeemInvite = ps.dashboard.ResRedeemInvite; + +export type ReqListTenantsByUser = ps.dashboard.ReqListTenantsByUser; +export type ResListTenantsByUser = ps.dashboard.ResListTenantsByUser; + +export type ReqUpdateUserTenant = ps.dashboard.ReqUpdateUserTenant; +export type ResUpdateUserTenant = ps.dashboard.ResUpdateUserTenant; + +export type ReqInviteUser = ps.dashboard.ReqInviteUser; +export type ResInviteUser = ps.dashboard.ResInviteUser; + +export type ReqListInvites = ps.dashboard.ReqListInvites; +export type ResListInvites = ps.dashboard.ResListInvites; + +export type ReqDeleteInvite = ps.dashboard.ReqDeleteInvite; +export type ResDeleteInvite = ps.dashboard.ResDeleteInvite; + +export type ReqCreateLedger = ps.dashboard.ReqCreateLedger; +export type ResCreateLedger = ps.dashboard.ResCreateLedger; + +export type ReqListLedgersByUser = ps.dashboard.ReqListLedgersByUser; +export type ResListLedgersByUser = ps.dashboard.ResListLedgersByUser; + +export type ReqUpdateLedger = ps.dashboard.ReqUpdateLedger; +export type ResUpdateLedger = ps.dashboard.ResUpdateLedger; + +export type ReqDeleteLedger = ps.dashboard.ReqDeleteLedger; +export type ResDeleteLedger = ps.dashboard.ResDeleteLedger; + +export type ReqCloudSessionToken = ps.dashboard.ReqCloudSessionToken; +export type ResCloudSessionToken = ps.dashboard.ResCloudSessionToken; + +export type ReqTokenByResultId = ps.dashboard.ReqTokenByResultId; +export type ResTokenByResultId = ps.dashboard.ResTokenByResultId; + +export type ReqExtendToken = ps.dashboard.ReqExtendToken; +export type ResExtendToken = ps.dashboard.ResExtendToken; diff --git a/backend/invites.ts b/backend/invites.ts new file mode 100644 index 0000000..49c2065 --- /dev/null +++ b/backend/invites.ts @@ -0,0 +1,164 @@ +import { stripper } from "@adviser/cement/utils"; +import { int, sqliteTable, text, index } from "drizzle-orm/sqlite-core"; +import { sqlUsers } from "./users.ts"; +import { sqlTenants } from "./tenants.ts"; +import { sqlLedgers } from "./ledgers.ts"; +import { queryEmail, queryNick, toUndef } from "./sql-helper.ts"; +import { SuperThis, ps } from "@fireproof/core"; +import { InviteTicket, SqlInvitedParams, AuthProvider, InviteTicketStatus, QueryUser, InvitedParams } from "./fp-dash-types.ts"; + +export const sqlInviteTickets = sqliteTable( + "InviteTickets", + { + inviteId: text().primaryKey(), + + inviterUserId: text() + .notNull() + .references(() => sqlUsers.userId), + // inviterTenantId: text() + // .notNull() + // .references(() => sqlTenants.tenantId), + + status: text().notNull(), // pending | accepted | rejected | expired + statusReason: text().notNull().default("just invited"), + // pending | accepted | rejected | expired + + // set if accepted + invitedUserId: text().references(() => sqlUsers.userId), + + // bind on login Invite + queryProvider: text(), + // email key for QueryUser -> tolower - remove + and . + queryEmail: text(), + // nick key for QueryUser -> tolower + queryNick: text(), + + sendEmailCount: int().notNull(), + + // invite to tenant + invitedTenantId: text().references(() => sqlTenants.tenantId), + // invite to ledger + invitedLedgerId: text().references(() => sqlLedgers.ledgerId), + // depending on target a JSON with e.g. the role and right + invitedParams: text().notNull(), + + expiresAfter: text().notNull(), + createdAt: text().notNull(), + updatedAt: text().notNull(), + }, + (table) => [ + index("invitesEmail").on(table.queryEmail), + index("invitesNick").on(table.queryNick), + index("invitesExpiresAfter").on(table.expiresAfter), + ], +); + +export function sqlToInviteTickets(sqls: (typeof sqlInviteTickets.$inferSelect)[]): InviteTicket[] { + return sqls.map((sql) => { + const ivp = JSON.parse(sql.invitedParams) ?? ({} as SqlInvitedParams); + if (ivp.tenant) { + ivp.tenant = { ...ivp.tenant, id: sql.invitedTenantId }; + } + if (ivp.ledger) { + ivp.ledger = { ...ivp.ledger, id: sql.invitedLedgerId }; + } + const objInvitedUserId: { invitedUserId?: string } = {}; + if (sql.invitedUserId) { + objInvitedUserId.invitedUserId = sql.invitedUserId; + } + return { + inviteId: sql.inviteId, + sendEmailCount: sql.sendEmailCount, + inviterUserId: sql.inviterUserId, + ...objInvitedUserId, + query: { + existingUserId: toUndef(sql.invitedUserId), + byEmail: toUndef(sql.queryEmail), + byNick: toUndef(sql.queryNick), + andProvider: toUndef(sql.queryProvider) as AuthProvider, + }, + invitedParams: ivp, + status: sql.status as InviteTicketStatus, + statusReason: sql.statusReason, + expiresAfter: new Date(sql.expiresAfter), + createdAt: new Date(sql.createdAt), + updatedAt: new Date(sql.updatedAt), + }; + }); +} + +export interface InviteTicketParams { + // readonly auth: AuthType; + readonly query: QueryUser; + // to update + readonly inviteId?: string; + readonly status: InviteTicketStatus; + readonly invitedUserId?: string; // must set if status is not pending + readonly incSendEmailCount?: boolean; + readonly invitedParams: InvitedParams; +} + +export interface PrepareInviteTicketParams { + readonly sthis: SuperThis; + readonly userId: string; + readonly invitedTicketParams: InviteTicketParams; + readonly expiresAfter?: Date; + readonly now?: Date; +} + +export function prepareInviteTicket({ + sthis, + userId, + invitedTicketParams: { inviteId, status, query, invitedParams: ivp, invitedUserId }, + now, + expiresAfter, +}: PrepareInviteTicketParams): typeof sqlInviteTickets.$inferInsert { + const nowDate = new Date(); + const nowStr = (now ?? nowDate).toISOString(); + const expiresAfterStr = (expiresAfter ?? new Date(nowDate.getTime() + 1000 * 60 * 60 * 24 * 7)).toISOString(); + + if ((ivp.ledger && ivp.tenant) || (!ivp.ledger && !ivp.tenant)) { + throw new Error("only one target ledger or tenant allowed"); + } + let sqlLedgerId: string | undefined = undefined; + let sqlTenantId: string | undefined = undefined; + let sqlInvitedParams: SqlInvitedParams | undefined = undefined; + if (ivp.ledger) { + sqlLedgerId = ivp.ledger.id; + sqlInvitedParams = { ledger: stripper("id", ivp.ledger) as SqlInvitedParams["ledger"] }; + } + if (ivp.tenant) { + sqlTenantId = ivp.tenant.id; + sqlInvitedParams = { tenant: stripper("id", ivp.tenant) as SqlInvitedParams["tenant"] }; + } + // let target: "tenant" | "ledger" = "tenant"; + const objInvitedUserId: { invitedUserId?: string } = {}; + if (status !== "pending") { + if (invitedUserId) { + objInvitedUserId.invitedUserId = invitedUserId; + } else { + throw new Error("invitedUserId required if status is not pending"); + } + } else { + if (query.existingUserId) { + objInvitedUserId.invitedUserId = query.existingUserId; + } + } + return { + inviteId: inviteId ?? sthis.nextId(12).str, + inviterUserId: userId, + status: status || "pending", + // inviterTenantId: tenantId, + queryEmail: queryEmail(query.byEmail), + queryNick: queryNick(query.byNick), + queryProvider: query.andProvider, + ...objInvitedUserId, + sendEmailCount: 0, + invitedTenantId: sqlTenantId, + invitedLedgerId: sqlLedgerId, + invitedParams: JSON.stringify(sqlInvitedParams), + expiresAfter: expiresAfterStr, + createdAt: nowStr, + updatedAt: nowStr, + }; +} diff --git a/backend/ledgers.ts b/backend/ledgers.ts new file mode 100644 index 0000000..0c503e8 --- /dev/null +++ b/backend/ledgers.ts @@ -0,0 +1,87 @@ +import { int, sqliteTable, text, primaryKey, index, unique } from "drizzle-orm/sqlite-core"; +import { sqlTenants } from "./tenants.ts"; +import { sqlUsers } from "./users.ts"; +import { toUndef } from "./sql-helper.ts"; +import { LedgerUser } from "./fp-dash-types.ts"; + +export const sqlLedgers = sqliteTable( + "Ledgers", + { + ledgerId: text().primaryKey(), + tenantId: text() + .notNull() + .references(() => sqlTenants.tenantId), + ownerId: text() + .notNull() + .references(() => sqlUsers.userId), + name: text().notNull(), + status: text().notNull().default("active"), + statusReason: text().notNull().default("just created"), + maxShares: int().notNull().default(5), + createdAt: text().notNull(), + updatedAt: text().notNull(), + }, + (table) => [unique("ledgerNamespace").on(table.tenantId, table.name)], +); + +export function sqlToLedgers( + rows: { + Ledgers: typeof sqlLedgers.$inferSelect; + LedgerUsers: typeof sqlLedgerUsers.$inferSelect; + }[], +): LedgerUser[] { + return Array.from( + rows + .reduce((acc, { Ledgers: l, LedgerUsers: lur }) => { + let ledger = acc.get(l.ledgerId); + if (!ledger) { + ledger = { + ledgerId: l.ledgerId, + tenantId: l.tenantId, + name: l.name, + ownerId: l.ownerId, + maxShares: l.maxShares, + users: [], + createdAt: new Date(l.createdAt), + updatedAt: new Date(l.updatedAt), + }; + acc.set(l.ledgerId, ledger); + } + ledger.users.push({ + userId: lur.userId, + role: lur.role as "admin" | "member", + right: lur.right as "read" | "write", + name: toUndef(lur.name), + default: !!lur.default, + createdAt: new Date(lur.createdAt), + updatedAt: new Date(lur.updatedAt), + }); + return acc; + }, new Map()) + .values(), + ); +} + +export const sqlLedgerUsers = sqliteTable( + "LedgerUsers", + { + ledgerId: text() + .notNull() + .references(() => sqlLedgers.ledgerId), + userId: text() + .notNull() + .references(() => sqlUsers.userId), + role: text().notNull(), // "admin" | "member" + right: text().notNull(), // "read" | "write" + default: int().notNull(), + status: text().notNull().default("active"), + statusReason: text().notNull().default("just created"), + name: text(), + createdAt: text().notNull(), + updatedAt: text().notNull(), + }, + (table) => [ + primaryKey({ columns: [table.ledgerId, table.userId, table.role] }), + index("luUserIdx").on(table.userId), // to enable delete by userRefId + ], +); diff --git a/backend/lib.deno.ns.d.ts b/backend/lib.deno.ns.d.ts new file mode 100644 index 0000000..f6e90a8 --- /dev/null +++ b/backend/lib.deno.ns.d.ts @@ -0,0 +1,18921 @@ +// Copyright 2018-2025 the Deno authors. MIT license. + +/// +/// +/// + +/** Deno provides extra properties on `import.meta`. These are included here + * to ensure that these are still available when using the Deno namespace in + * conjunction with other type libs, like `dom`. + * + * @category Platform + */ +interface ImportMeta { + /** A string representation of the fully qualified module URL. When the + * module is loaded locally, the value will be a file URL (e.g. + * `file:///path/module.ts`). + * + * You can also parse the string as a URL to determine more information about + * how the current module was loaded. For example to determine if a module was + * local or not: + * + * ```ts + * const url = new URL(import.meta.url); + * if (url.protocol === "file:") { + * console.log("this module was loaded locally"); + * } + * ``` + */ + url: string; + + /** The absolute path of the current module. + * + * This property is only provided for local modules (ie. using `file://` URLs). + * + * Example: + * ``` + * // Unix + * console.log(import.meta.filename); // /home/alice/my_module.ts + * + * // Windows + * console.log(import.meta.filename); // C:\alice\my_module.ts + * ``` + */ + filename?: string; + + /** The absolute path of the directory containing the current module. + * + * This property is only provided for local modules (ie. using `file://` URLs). + * + * * Example: + * ``` + * // Unix + * console.log(import.meta.dirname); // /home/alice + * + * // Windows + * console.log(import.meta.dirname); // C:\alice + * ``` + */ + dirname?: string; + + /** A flag that indicates if the current module is the main module that was + * called when starting the program under Deno. + * + * ```ts + * if (import.meta.main) { + * // this was loaded as the main module, maybe do some bootstrapping + * } + * ``` + */ + main: boolean; + + /** A function that returns resolved specifier as if it would be imported + * using `import(specifier)`. + * + * ```ts + * console.log(import.meta.resolve("./foo.js")); + * // file:///dev/foo.js + * ``` + */ + resolve(specifier: string): string; +} + +/** Deno supports [User Timing Level 3](https://w3c.github.io/user-timing) + * which is not widely supported yet in other runtimes. + * + * Check out the + * [Performance API](https://developer.mozilla.org/en-US/docs/Web/API/Performance) + * documentation on MDN for further information about how to use the API. + * + * @category Performance + */ +interface Performance { + /** Stores a timestamp with the associated name (a "mark"). */ + mark(markName: string, options?: PerformanceMarkOptions): PerformanceMark; + + /** Stores the `DOMHighResTimeStamp` duration between two marks along with the + * associated name (a "measure"). */ + measure(measureName: string, options?: PerformanceMeasureOptions): PerformanceMeasure; +} + +/** + * Options which are used in conjunction with `performance.mark`. Check out the + * MDN + * [`performance.mark()`](https://developer.mozilla.org/en-US/docs/Web/API/Performance/mark#markoptions) + * documentation for more details. + * + * @category Performance + */ +interface PerformanceMarkOptions { + /** Metadata to be included in the mark. */ + // deno-lint-ignore no-explicit-any + detail?: any; + + /** Timestamp to be used as the mark time. */ + startTime?: number; +} + +/** + * Options which are used in conjunction with `performance.measure`. Check out the + * MDN + * [`performance.mark()`](https://developer.mozilla.org/en-US/docs/Web/API/Performance/measure#measureoptions) + * documentation for more details. + * + * @category Performance + */ +interface PerformanceMeasureOptions { + /** Metadata to be included in the measure. */ + // deno-lint-ignore no-explicit-any + detail?: any; + + /** Timestamp to be used as the start time or string to be used as start + * mark. */ + start?: string | number; + + /** Duration between the start and end times. */ + duration?: number; + + /** Timestamp to be used as the end time or string to be used as end mark. */ + end?: string | number; +} + +/** The global namespace where Deno specific, non-standard APIs are located. */ +declare namespace Deno { + /** A set of error constructors that are raised by Deno APIs. + * + * Can be used to provide more specific handling of failures within code + * which is using Deno APIs. For example, handling attempting to open a file + * which does not exist: + * + * ```ts + * try { + * const file = await Deno.open("./some/file.txt"); + * } catch (error) { + * if (error instanceof Deno.errors.NotFound) { + * console.error("the file was not found"); + * } else { + * // otherwise re-throw + * throw error; + * } + * } + * ``` + * + * @category Errors + */ + export namespace errors { + /** + * Raised when the underlying operating system indicates that the file + * was not found. + * + * @category Errors */ + export class NotFound extends Error {} + /** + * Raised when the underlying operating system indicates the current user + * which the Deno process is running under does not have the appropriate + * permissions to a file or resource. + * + * Before Deno 2.0, this error was raised when the user _did not_ provide + * required `--allow-*` flag. As of Deno 2.0, that case is now handled by + * the {@link NotCapable} error. + * + * @category Errors */ + export class PermissionDenied extends Error {} + /** + * Raised when the underlying operating system reports that a connection to + * a resource is refused. + * + * @category Errors */ + export class ConnectionRefused extends Error {} + /** + * Raised when the underlying operating system reports that a connection has + * been reset. With network servers, it can be a _normal_ occurrence where a + * client will abort a connection instead of properly shutting it down. + * + * @category Errors */ + export class ConnectionReset extends Error {} + /** + * Raised when the underlying operating system reports an `ECONNABORTED` + * error. + * + * @category Errors */ + export class ConnectionAborted extends Error {} + /** + * Raised when the underlying operating system reports an `ENOTCONN` error. + * + * @category Errors */ + export class NotConnected extends Error {} + /** + * Raised when attempting to open a server listener on an address and port + * that already has a listener. + * + * @category Errors */ + export class AddrInUse extends Error {} + /** + * Raised when the underlying operating system reports an `EADDRNOTAVAIL` + * error. + * + * @category Errors */ + export class AddrNotAvailable extends Error {} + /** + * Raised when trying to write to a resource and a broken pipe error occurs. + * This can happen when trying to write directly to `stdout` or `stderr` + * and the operating system is unable to pipe the output for a reason + * external to the Deno runtime. + * + * @category Errors */ + export class BrokenPipe extends Error {} + /** + * Raised when trying to create a resource, like a file, that already + * exits. + * + * @category Errors */ + export class AlreadyExists extends Error {} + /** + * Raised when an operation to returns data that is invalid for the + * operation being performed. + * + * @category Errors */ + export class InvalidData extends Error {} + /** + * Raised when the underlying operating system reports that an I/O operation + * has timed out (`ETIMEDOUT`). + * + * @category Errors */ + export class TimedOut extends Error {} + /** + * Raised when the underlying operating system reports an `EINTR` error. In + * many cases, this underlying IO error will be handled internally within + * Deno, or result in an @{link BadResource} error instead. + * + * @category Errors */ + export class Interrupted extends Error {} + /** + * Raised when the underlying operating system would need to block to + * complete but an asynchronous (non-blocking) API is used. + * + * @category Errors */ + export class WouldBlock extends Error {} + /** + * Raised when expecting to write to a IO buffer resulted in zero bytes + * being written. + * + * @category Errors */ + export class WriteZero extends Error {} + /** + * Raised when attempting to read bytes from a resource, but the EOF was + * unexpectedly encountered. + * + * @category Errors */ + export class UnexpectedEof extends Error {} + /** + * The underlying IO resource is invalid or closed, and so the operation + * could not be performed. + * + * @category Errors */ + export class BadResource extends Error {} + /** + * Raised in situations where when attempting to load a dynamic import, + * too many redirects were encountered. + * + * @category Errors */ + export class Http extends Error {} + /** + * Raised when the underlying IO resource is not available because it is + * being awaited on in another block of code. + * + * @category Errors */ + export class Busy extends Error {} + /** + * Raised when the underlying Deno API is asked to perform a function that + * is not currently supported. + * + * @category Errors */ + export class NotSupported extends Error {} + /** + * Raised when too many symbolic links were encountered when resolving the + * filename. + * + * @category Errors */ + export class FilesystemLoop extends Error {} + /** + * Raised when trying to open, create or write to a directory. + * + * @category Errors */ + export class IsADirectory extends Error {} + /** + * Raised when performing a socket operation but the remote host is + * not reachable. + * + * @category Errors */ + export class NetworkUnreachable extends Error {} + /** + * Raised when trying to perform an operation on a path that is not a + * directory, when directory is required. + * + * @category Errors */ + export class NotADirectory extends Error {} + + /** + * Raised when trying to perform an operation while the relevant Deno + * permission (like `--allow-read`) has not been granted. + * + * Before Deno 2.0, this condition was covered by the {@link PermissionDenied} + * error. + * + * @category Errors */ + export class NotCapable extends Error {} + + export {}; // only export exports + } + + /** The current process ID of this instance of the Deno CLI. + * + * ```ts + * console.log(Deno.pid); + * ``` + * + * @category Runtime + */ + export const pid: number; + + /** + * The process ID of parent process of this instance of the Deno CLI. + * + * ```ts + * console.log(Deno.ppid); + * ``` + * + * @category Runtime + */ + export const ppid: number; + + /** @category Runtime */ + export interface MemoryUsage { + /** The number of bytes of the current Deno's process resident set size, + * which is the amount of memory occupied in main memory (RAM). */ + rss: number; + /** The total size of the heap for V8, in bytes. */ + heapTotal: number; + /** The amount of the heap used for V8, in bytes. */ + heapUsed: number; + /** Memory, in bytes, associated with JavaScript objects outside of the + * JavaScript isolate. */ + external: number; + } + + /** + * Returns an object describing the memory usage of the Deno process and the + * V8 subsystem measured in bytes. + * + * @category Runtime + */ + export function memoryUsage(): MemoryUsage; + + /** + * Get the `hostname` of the machine the Deno process is running on. + * + * ```ts + * console.log(Deno.hostname()); + * ``` + * + * Requires `allow-sys` permission. + * + * @tags allow-sys + * @category Runtime + */ + export function hostname(): string; + + /** + * Returns an array containing the 1, 5, and 15 minute load averages. The + * load average is a measure of CPU and IO utilization of the last one, five, + * and 15 minute periods expressed as a fractional number. Zero means there + * is no load. On Windows, the three values are always the same and represent + * the current load, not the 1, 5 and 15 minute load averages. + * + * ```ts + * console.log(Deno.loadavg()); // e.g. [ 0.71, 0.44, 0.44 ] + * ``` + * + * Requires `allow-sys` permission. + * + * On Windows there is no API available to retrieve this information and this method returns `[ 0, 0, 0 ]`. + * + * @tags allow-sys + * @category Runtime + */ + export function loadavg(): number[]; + + /** + * The information for a network interface returned from a call to + * {@linkcode Deno.networkInterfaces}. + * + * @category Network + */ + export interface NetworkInterfaceInfo { + /** The network interface name. */ + name: string; + /** The IP protocol version. */ + family: "IPv4" | "IPv6"; + /** The IP address bound to the interface. */ + address: string; + /** The netmask applied to the interface. */ + netmask: string; + /** The IPv6 scope id or `null`. */ + scopeid: number | null; + /** The CIDR range. */ + cidr: string; + /** The MAC address. */ + mac: string; + } + + /** + * Returns an array of the network interface information. + * + * ```ts + * console.log(Deno.networkInterfaces()); + * ``` + * + * Requires `allow-sys` permission. + * + * @tags allow-sys + * @category Network + */ + export function networkInterfaces(): NetworkInterfaceInfo[]; + + /** + * Displays the total amount of free and used physical and swap memory in the + * system, as well as the buffers and caches used by the kernel. + * + * This is similar to the `free` command in Linux + * + * ```ts + * console.log(Deno.systemMemoryInfo()); + * ``` + * + * Requires `allow-sys` permission. + * + * @tags allow-sys + * @category Runtime + */ + export function systemMemoryInfo(): SystemMemoryInfo; + + /** + * Information returned from a call to {@linkcode Deno.systemMemoryInfo}. + * + * @category Runtime + */ + export interface SystemMemoryInfo { + /** Total installed memory in bytes. */ + total: number; + /** Unused memory in bytes. */ + free: number; + /** Estimation of how much memory, in bytes, is available for starting new + * applications, without swapping. Unlike the data provided by the cache or + * free fields, this field takes into account page cache and also that not + * all reclaimable memory will be reclaimed due to items being in use. + */ + available: number; + /** Memory used by kernel buffers. */ + buffers: number; + /** Memory used by the page cache and slabs. */ + cached: number; + /** Total swap memory. */ + swapTotal: number; + /** Unused swap memory. */ + swapFree: number; + } + + /** Reflects the `NO_COLOR` environment variable at program start. + * + * When the value is `true`, the Deno CLI will attempt to not send color codes + * to `stderr` or `stdout` and other command line programs should also attempt + * to respect this value. + * + * See: https://no-color.org/ + * + * @category Runtime + */ + export const noColor: boolean; + + /** + * Returns the release version of the Operating System. + * + * ```ts + * console.log(Deno.osRelease()); + * ``` + * + * Requires `allow-sys` permission. + * Under consideration to possibly move to Deno.build or Deno.versions and if + * it should depend sys-info, which may not be desirable. + * + * @tags allow-sys + * @category Runtime + */ + export function osRelease(): string; + + /** + * Returns the Operating System uptime in number of seconds. + * + * ```ts + * console.log(Deno.osUptime()); + * ``` + * + * Requires `allow-sys` permission. + * + * @tags allow-sys + * @category Runtime + */ + export function osUptime(): number; + + /** + * Options which define the permissions within a test or worker context. + * + * `"inherit"` ensures that all permissions of the parent process will be + * applied to the test context. `"none"` ensures the test context has no + * permissions. A `PermissionOptionsObject` provides a more specific + * set of permissions to the test context. + * + * @category Permissions */ + export type PermissionOptions = "inherit" | "none" | PermissionOptionsObject; + + /** + * A set of options which can define the permissions within a test or worker + * context at a highly specific level. + * + * @category Permissions */ + export interface PermissionOptionsObject { + /** Specifies if the `env` permission should be requested or revoked. + * If set to `"inherit"`, the current `env` permission will be inherited. + * If set to `true`, the global `env` permission will be requested. + * If set to `false`, the global `env` permission will be revoked. + * + * @default {false} + */ + env?: "inherit" | boolean | string[]; + + /** Specifies if the `ffi` permission should be requested or revoked. + * If set to `"inherit"`, the current `ffi` permission will be inherited. + * If set to `true`, the global `ffi` permission will be requested. + * If set to `false`, the global `ffi` permission will be revoked. + * + * @default {false} + */ + ffi?: "inherit" | boolean | Array; + + /** Specifies if the `import` permission should be requested or revoked. + * If set to `"inherit"` the current `import` permission will be inherited. + * If set to `true`, the global `import` permission will be requested. + * If set to `false`, the global `import` permission will be revoked. + * If set to `Array`, the `import` permissions will be requested with the + * specified domains. + */ + import?: "inherit" | boolean | Array; + + /** Specifies if the `net` permission should be requested or revoked. + * if set to `"inherit"`, the current `net` permission will be inherited. + * if set to `true`, the global `net` permission will be requested. + * if set to `false`, the global `net` permission will be revoked. + * if set to `string[]`, the `net` permission will be requested with the + * specified host strings with the format `"[:]`. + * + * @default {false} + * + * Examples: + * + * ```ts + * import { assertEquals } from "jsr:@std/assert"; + * + * Deno.test({ + * name: "inherit", + * permissions: { + * net: "inherit", + * }, + * async fn() { + * const status = await Deno.permissions.query({ name: "net" }) + * assertEquals(status.state, "granted"); + * }, + * }); + * ``` + * + * ```ts + * import { assertEquals } from "jsr:@std/assert"; + * + * Deno.test({ + * name: "true", + * permissions: { + * net: true, + * }, + * async fn() { + * const status = await Deno.permissions.query({ name: "net" }); + * assertEquals(status.state, "granted"); + * }, + * }); + * ``` + * + * ```ts + * import { assertEquals } from "jsr:@std/assert"; + * + * Deno.test({ + * name: "false", + * permissions: { + * net: false, + * }, + * async fn() { + * const status = await Deno.permissions.query({ name: "net" }); + * assertEquals(status.state, "denied"); + * }, + * }); + * ``` + * + * ```ts + * import { assertEquals } from "jsr:@std/assert"; + * + * Deno.test({ + * name: "localhost:8080", + * permissions: { + * net: ["localhost:8080"], + * }, + * async fn() { + * const status = await Deno.permissions.query({ name: "net", host: "localhost:8080" }); + * assertEquals(status.state, "granted"); + * }, + * }); + * ``` + */ + net?: "inherit" | boolean | string[]; + + /** Specifies if the `read` permission should be requested or revoked. + * If set to `"inherit"`, the current `read` permission will be inherited. + * If set to `true`, the global `read` permission will be requested. + * If set to `false`, the global `read` permission will be revoked. + * If set to `Array`, the `read` permission will be requested with the + * specified file paths. + * + * @default {false} + */ + read?: "inherit" | boolean | Array; + + /** Specifies if the `run` permission should be requested or revoked. + * If set to `"inherit"`, the current `run` permission will be inherited. + * If set to `true`, the global `run` permission will be requested. + * If set to `false`, the global `run` permission will be revoked. + * + * @default {false} + */ + run?: "inherit" | boolean | Array; + + /** Specifies if the `sys` permission should be requested or revoked. + * If set to `"inherit"`, the current `sys` permission will be inherited. + * If set to `true`, the global `sys` permission will be requested. + * If set to `false`, the global `sys` permission will be revoked. + * + * @default {false} + */ + sys?: "inherit" | boolean | string[]; + + /** Specifies if the `write` permission should be requested or revoked. + * If set to `"inherit"`, the current `write` permission will be inherited. + * If set to `true`, the global `write` permission will be requested. + * If set to `false`, the global `write` permission will be revoked. + * If set to `Array`, the `write` permission will be requested with the + * specified file paths. + * + * @default {false} + */ + write?: "inherit" | boolean | Array; + } + + /** + * Context that is passed to a testing function, which can be used to either + * gain information about the current test, or register additional test + * steps within the current test. + * + * @category Testing */ + export interface TestContext { + /** The current test name. */ + name: string; + /** The string URL of the current test. */ + origin: string; + /** If the current test is a step of another test, the parent test context + * will be set here. */ + parent?: TestContext; + + /** Run a sub step of the parent test or step. Returns a promise + * that resolves to a boolean signifying if the step completed successfully. + * + * The returned promise never rejects unless the arguments are invalid. + * + * If the test was ignored the promise returns `false`. + * + * ```ts + * Deno.test({ + * name: "a parent test", + * async fn(t) { + * console.log("before the step"); + * await t.step({ + * name: "step 1", + * fn(t) { + * console.log("current step:", t.name); + * } + * }); + * console.log("after the step"); + * } + * }); + * ``` + */ + step(definition: TestStepDefinition): Promise; + + /** Run a sub step of the parent test or step. Returns a promise + * that resolves to a boolean signifying if the step completed successfully. + * + * The returned promise never rejects unless the arguments are invalid. + * + * If the test was ignored the promise returns `false`. + * + * ```ts + * Deno.test( + * "a parent test", + * async (t) => { + * console.log("before the step"); + * await t.step( + * "step 1", + * (t) => { + * console.log("current step:", t.name); + * } + * ); + * console.log("after the step"); + * } + * ); + * ``` + */ + step(name: string, fn: (t: TestContext) => void | Promise): Promise; + + /** Run a sub step of the parent test or step. Returns a promise + * that resolves to a boolean signifying if the step completed successfully. + * + * The returned promise never rejects unless the arguments are invalid. + * + * If the test was ignored the promise returns `false`. + * + * ```ts + * Deno.test(async function aParentTest(t) { + * console.log("before the step"); + * await t.step(function step1(t) { + * console.log("current step:", t.name); + * }); + * console.log("after the step"); + * }); + * ``` + */ + step(fn: (t: TestContext) => void | Promise): Promise; + } + + /** @category Testing */ + export interface TestStepDefinition { + /** The test function that will be tested when this step is executed. The + * function can take an argument which will provide information about the + * current step's context. */ + fn: (t: TestContext) => void | Promise; + /** The name of the step. */ + name: string; + /** If truthy the current test step will be ignored. + * + * This is a quick way to skip over a step, but also can be used for + * conditional logic, like determining if an environment feature is present. + */ + ignore?: boolean; + /** Check that the number of async completed operations after the test step + * is the same as number of dispatched operations. This ensures that the + * code tested does not start async operations which it then does + * not await. This helps in preventing logic errors and memory leaks + * in the application code. + * + * Defaults to the parent test or step's value. */ + sanitizeOps?: boolean; + /** Ensure the test step does not "leak" resources - like open files or + * network connections - by ensuring the open resources at the start of the + * step match the open resources at the end of the step. + * + * Defaults to the parent test or step's value. */ + sanitizeResources?: boolean; + /** Ensure the test step does not prematurely cause the process to exit, + * for example via a call to {@linkcode Deno.exit}. + * + * Defaults to the parent test or step's value. */ + sanitizeExit?: boolean; + } + + /** @category Testing */ + export interface TestDefinition { + fn: (t: TestContext) => void | Promise; + /** The name of the test. */ + name: string; + /** If truthy the current test step will be ignored. + * + * It is a quick way to skip over a step, but also can be used for + * conditional logic, like determining if an environment feature is present. + */ + ignore?: boolean; + /** If at least one test has `only` set to `true`, only run tests that have + * `only` set to `true` and fail the test suite. */ + only?: boolean; + /** Check that the number of async completed operations after the test step + * is the same as number of dispatched operations. This ensures that the + * code tested does not start async operations which it then does + * not await. This helps in preventing logic errors and memory leaks + * in the application code. + * + * @default {true} */ + sanitizeOps?: boolean; + /** Ensure the test step does not "leak" resources - like open files or + * network connections - by ensuring the open resources at the start of the + * test match the open resources at the end of the test. + * + * @default {true} */ + sanitizeResources?: boolean; + /** Ensure the test case does not prematurely cause the process to exit, + * for example via a call to {@linkcode Deno.exit}. + * + * @default {true} */ + sanitizeExit?: boolean; + /** Specifies the permissions that should be used to run the test. + * + * Set this to "inherit" to keep the calling runtime permissions, set this + * to "none" to revoke all permissions, or set a more specific set of + * permissions using a {@linkcode PermissionOptionsObject}. + * + * @default {"inherit"} */ + permissions?: PermissionOptions; + } + + /** Register a test which will be run when `deno test` is used on the command + * line and the containing module looks like a test module. + * + * `fn` can be async if required. + * + * ```ts + * import { assertEquals } from "jsr:@std/assert"; + * + * Deno.test({ + * name: "example test", + * fn() { + * assertEquals("world", "world"); + * }, + * }); + * + * Deno.test({ + * name: "example ignored test", + * ignore: Deno.build.os === "windows", + * fn() { + * // This test is ignored only on Windows machines + * }, + * }); + * + * Deno.test({ + * name: "example async test", + * async fn() { + * const decoder = new TextDecoder("utf-8"); + * const data = await Deno.readFile("hello_world.txt"); + * assertEquals(decoder.decode(data), "Hello world"); + * } + * }); + * ``` + * + * @category Testing + */ + export const test: DenoTest; + + /** + * @category Testing + */ + export interface DenoTest { + /** Register a test which will be run when `deno test` is used on the command + * line and the containing module looks like a test module. + * + * `fn` can be async if required. + * + * ```ts + * import { assertEquals } from "jsr:@std/assert"; + * + * Deno.test({ + * name: "example test", + * fn() { + * assertEquals("world", "world"); + * }, + * }); + * + * Deno.test({ + * name: "example ignored test", + * ignore: Deno.build.os === "windows", + * fn() { + * // This test is ignored only on Windows machines + * }, + * }); + * + * Deno.test({ + * name: "example async test", + * async fn() { + * const decoder = new TextDecoder("utf-8"); + * const data = await Deno.readFile("hello_world.txt"); + * assertEquals(decoder.decode(data), "Hello world"); + * } + * }); + * ``` + * + * @category Testing + */ + (t: TestDefinition): void; + + /** Register a test which will be run when `deno test` is used on the command + * line and the containing module looks like a test module. + * + * `fn` can be async if required. + * + * ```ts + * import { assertEquals } from "jsr:@std/assert"; + * + * Deno.test("My test description", () => { + * assertEquals("hello", "hello"); + * }); + * + * Deno.test("My async test description", async () => { + * const decoder = new TextDecoder("utf-8"); + * const data = await Deno.readFile("hello_world.txt"); + * assertEquals(decoder.decode(data), "Hello world"); + * }); + * ``` + * + * @category Testing + */ + (name: string, fn: (t: TestContext) => void | Promise): void; + + /** Register a test which will be run when `deno test` is used on the command + * line and the containing module looks like a test module. + * + * `fn` can be async if required. Declared function must have a name. + * + * ```ts + * import { assertEquals } from "jsr:@std/assert"; + * + * Deno.test(function myTestName() { + * assertEquals("hello", "hello"); + * }); + * + * Deno.test(async function myOtherTestName() { + * const decoder = new TextDecoder("utf-8"); + * const data = await Deno.readFile("hello_world.txt"); + * assertEquals(decoder.decode(data), "Hello world"); + * }); + * ``` + * + * @category Testing + */ + (fn: (t: TestContext) => void | Promise): void; + + /** Register a test which will be run when `deno test` is used on the command + * line and the containing module looks like a test module. + * + * `fn` can be async if required. + * + * ```ts + * import { assert, fail, assertEquals } from "jsr:@std/assert"; + * + * Deno.test("My test description", { permissions: { read: true } }, (): void => { + * assertEquals("hello", "hello"); + * }); + * + * Deno.test("My async test description", { permissions: { read: false } }, async (): Promise => { + * const decoder = new TextDecoder("utf-8"); + * const data = await Deno.readFile("hello_world.txt"); + * assertEquals(decoder.decode(data), "Hello world"); + * }); + * ``` + * + * @category Testing + */ + (name: string, options: Omit, fn: (t: TestContext) => void | Promise): void; + + /** Register a test which will be run when `deno test` is used on the command + * line and the containing module looks like a test module. + * + * `fn` can be async if required. + * + * ```ts + * import { assertEquals } from "jsr:@std/assert"; + * + * Deno.test( + * { + * name: "My test description", + * permissions: { read: true }, + * }, + * () => { + * assertEquals("hello", "hello"); + * }, + * ); + * + * Deno.test( + * { + * name: "My async test description", + * permissions: { read: false }, + * }, + * async () => { + * const decoder = new TextDecoder("utf-8"); + * const data = await Deno.readFile("hello_world.txt"); + * assertEquals(decoder.decode(data), "Hello world"); + * }, + * ); + * ``` + * + * @category Testing + */ + (options: Omit, fn: (t: TestContext) => void | Promise): void; + + /** Register a test which will be run when `deno test` is used on the command + * line and the containing module looks like a test module. + * + * `fn` can be async if required. Declared function must have a name. + * + * ```ts + * import { assertEquals } from "jsr:@std/assert"; + * + * Deno.test( + * { permissions: { read: true } }, + * function myTestName() { + * assertEquals("hello", "hello"); + * }, + * ); + * + * Deno.test( + * { permissions: { read: false } }, + * async function myOtherTestName() { + * const decoder = new TextDecoder("utf-8"); + * const data = await Deno.readFile("hello_world.txt"); + * assertEquals(decoder.decode(data), "Hello world"); + * }, + * ); + * ``` + * + * @category Testing + */ + (options: Omit, fn: (t: TestContext) => void | Promise): void; + + /** Shorthand property for ignoring a particular test case. + * + * @category Testing + */ + ignore(t: Omit): void; + + /** Shorthand property for ignoring a particular test case. + * + * @category Testing + */ + ignore(name: string, fn: (t: TestContext) => void | Promise): void; + + /** Shorthand property for ignoring a particular test case. + * + * @category Testing + */ + ignore(fn: (t: TestContext) => void | Promise): void; + + /** Shorthand property for ignoring a particular test case. + * + * @category Testing + */ + ignore( + name: string, + options: Omit, + fn: (t: TestContext) => void | Promise, + ): void; + + /** Shorthand property for ignoring a particular test case. + * + * @category Testing + */ + ignore(options: Omit, fn: (t: TestContext) => void | Promise): void; + + /** Shorthand property for ignoring a particular test case. + * + * @category Testing + */ + ignore(options: Omit, fn: (t: TestContext) => void | Promise): void; + + /** Shorthand property for focusing a particular test case. + * + * @category Testing + */ + only(t: Omit): void; + + /** Shorthand property for focusing a particular test case. + * + * @category Testing + */ + only(name: string, fn: (t: TestContext) => void | Promise): void; + + /** Shorthand property for focusing a particular test case. + * + * @category Testing + */ + only(fn: (t: TestContext) => void | Promise): void; + + /** Shorthand property for focusing a particular test case. + * + * @category Testing + */ + only(name: string, options: Omit, fn: (t: TestContext) => void | Promise): void; + + /** Shorthand property for focusing a particular test case. + * + * @category Testing + */ + only(options: Omit, fn: (t: TestContext) => void | Promise): void; + + /** Shorthand property for focusing a particular test case. + * + * @category Testing + */ + only(options: Omit, fn: (t: TestContext) => void | Promise): void; + } + + /** + * Context that is passed to a benchmarked function. The instance is shared + * between iterations of the benchmark. Its methods can be used for example + * to override of the measured portion of the function. + * + * @category Testing + */ + export interface BenchContext { + /** The current benchmark name. */ + name: string; + /** The string URL of the current benchmark. */ + origin: string; + + /** Restarts the timer for the bench measurement. This should be called + * after doing setup work which should not be measured. + * + * Warning: This method should not be used for benchmarks averaging less + * than 10μs per iteration. In such cases it will be disabled but the call + * will still have noticeable overhead, resulting in a warning. + * + * ```ts + * Deno.bench("foo", async (t) => { + * const data = await Deno.readFile("data.txt"); + * t.start(); + * // some operation on `data`... + * }); + * ``` + */ + start(): void; + + /** End the timer early for the bench measurement. This should be called + * before doing teardown work which should not be measured. + * + * Warning: This method should not be used for benchmarks averaging less + * than 10μs per iteration. In such cases it will be disabled but the call + * will still have noticeable overhead, resulting in a warning. + * + * ```ts + * Deno.bench("foo", async (t) => { + * using file = await Deno.open("data.txt"); + * t.start(); + * // some operation on `file`... + * t.end(); + * }); + * ``` + */ + end(): void; + } + + /** + * The interface for defining a benchmark test using {@linkcode Deno.bench}. + * + * @category Testing + */ + export interface BenchDefinition { + /** The test function which will be benchmarked. */ + fn: (b: BenchContext) => void | Promise; + /** The name of the test, which will be used in displaying the results. */ + name: string; + /** If truthy, the benchmark test will be ignored/skipped. */ + ignore?: boolean; + /** Group name for the benchmark. + * + * Grouped benchmarks produce a group time summary, where the difference + * in performance between each test of the group is compared. */ + group?: string; + /** Benchmark should be used as the baseline for other benchmarks. + * + * If there are multiple baselines in a group, the first one is used as the + * baseline. */ + baseline?: boolean; + /** If at least one bench has `only` set to true, only run benches that have + * `only` set to `true` and fail the bench suite. */ + only?: boolean; + /** Number of iterations to perform. */ + n?: number; + /** Number of warmups to do before running the benchmark. */ + warmup?: number; + /** Ensure the bench case does not prematurely cause the process to exit, + * for example via a call to {@linkcode Deno.exit}. + * + * @default {true} */ + sanitizeExit?: boolean; + /** Specifies the permissions that should be used to run the bench. + * + * Set this to `"inherit"` to keep the calling thread's permissions. + * + * Set this to `"none"` to revoke all permissions. + * + * @default {"inherit"} + */ + permissions?: PermissionOptions; + } + + /** + * Register a benchmark test which will be run when `deno bench` is used on + * the command line and the containing module looks like a bench module. + * + * If the test function (`fn`) returns a promise or is async, the test runner + * will await resolution to consider the test complete. + * + * ```ts + * import { assertEquals } from "jsr:@std/assert"; + * + * Deno.bench({ + * name: "example test", + * fn() { + * assertEquals("world", "world"); + * }, + * }); + * + * Deno.bench({ + * name: "example ignored test", + * ignore: Deno.build.os === "windows", + * fn() { + * // This test is ignored only on Windows machines + * }, + * }); + * + * Deno.bench({ + * name: "example async test", + * async fn() { + * const decoder = new TextDecoder("utf-8"); + * const data = await Deno.readFile("hello_world.txt"); + * assertEquals(decoder.decode(data), "Hello world"); + * } + * }); + * ``` + * + * @category Testing + */ + export function bench(b: BenchDefinition): void; + + /** + * Register a benchmark test which will be run when `deno bench` is used on + * the command line and the containing module looks like a bench module. + * + * If the test function (`fn`) returns a promise or is async, the test runner + * will await resolution to consider the test complete. + * + * ```ts + * import { assertEquals } from "jsr:@std/assert"; + * + * Deno.bench("My test description", () => { + * assertEquals("hello", "hello"); + * }); + * + * Deno.bench("My async test description", async () => { + * const decoder = new TextDecoder("utf-8"); + * const data = await Deno.readFile("hello_world.txt"); + * assertEquals(decoder.decode(data), "Hello world"); + * }); + * ``` + * + * @category Testing + */ + export function bench(name: string, fn: (b: BenchContext) => void | Promise): void; + + /** + * Register a benchmark test which will be run when `deno bench` is used on + * the command line and the containing module looks like a bench module. + * + * If the test function (`fn`) returns a promise or is async, the test runner + * will await resolution to consider the test complete. + * + * ```ts + * import { assertEquals } from "jsr:@std/assert"; + * + * Deno.bench(function myTestName() { + * assertEquals("hello", "hello"); + * }); + * + * Deno.bench(async function myOtherTestName() { + * const decoder = new TextDecoder("utf-8"); + * const data = await Deno.readFile("hello_world.txt"); + * assertEquals(decoder.decode(data), "Hello world"); + * }); + * ``` + * + * @category Testing + */ + export function bench(fn: (b: BenchContext) => void | Promise): void; + + /** + * Register a benchmark test which will be run when `deno bench` is used on + * the command line and the containing module looks like a bench module. + * + * If the test function (`fn`) returns a promise or is async, the test runner + * will await resolution to consider the test complete. + * + * ```ts + * import { assertEquals } from "jsr:@std/assert"; + * + * Deno.bench( + * "My test description", + * { permissions: { read: true } }, + * () => { + * assertEquals("hello", "hello"); + * } + * ); + * + * Deno.bench( + * "My async test description", + * { permissions: { read: false } }, + * async () => { + * const decoder = new TextDecoder("utf-8"); + * const data = await Deno.readFile("hello_world.txt"); + * assertEquals(decoder.decode(data), "Hello world"); + * } + * ); + * ``` + * + * @category Testing + */ + export function bench( + name: string, + options: Omit, + fn: (b: BenchContext) => void | Promise, + ): void; + + /** + * Register a benchmark test which will be run when `deno bench` is used on + * the command line and the containing module looks like a bench module. + * + * If the test function (`fn`) returns a promise or is async, the test runner + * will await resolution to consider the test complete. + * + * ```ts + * import { assertEquals } from "jsr:@std/assert"; + * + * Deno.bench( + * { name: "My test description", permissions: { read: true } }, + * () => { + * assertEquals("hello", "hello"); + * } + * ); + * + * Deno.bench( + * { name: "My async test description", permissions: { read: false } }, + * async () => { + * const decoder = new TextDecoder("utf-8"); + * const data = await Deno.readFile("hello_world.txt"); + * assertEquals(decoder.decode(data), "Hello world"); + * } + * ); + * ``` + * + * @category Testing + */ + export function bench(options: Omit, fn: (b: BenchContext) => void | Promise): void; + + /** + * Register a benchmark test which will be run when `deno bench` is used on + * the command line and the containing module looks like a bench module. + * + * If the test function (`fn`) returns a promise or is async, the test runner + * will await resolution to consider the test complete. + * + * ```ts + * import { assertEquals } from "jsr:@std/assert"; + * + * Deno.bench( + * { permissions: { read: true } }, + * function myTestName() { + * assertEquals("hello", "hello"); + * } + * ); + * + * Deno.bench( + * { permissions: { read: false } }, + * async function myOtherTestName() { + * const decoder = new TextDecoder("utf-8"); + * const data = await Deno.readFile("hello_world.txt"); + * assertEquals(decoder.decode(data), "Hello world"); + * } + * ); + * ``` + * + * @category Testing + */ + export function bench(options: Omit, fn: (b: BenchContext) => void | Promise): void; + + /** Exit the Deno process with optional exit code. + * + * If no exit code is supplied then Deno will exit with return code of `0`. + * + * In worker contexts this is an alias to `self.close();`. + * + * ```ts + * Deno.exit(5); + * ``` + * + * @category Runtime + */ + export function exit(code?: number): never; + + /** The exit code for the Deno process. + * + * If no exit code has been supplied, then Deno will assume a return code of `0`. + * + * When setting an exit code value, a number or non-NaN string must be provided, + * otherwise a TypeError will be thrown. + * + * ```ts + * console.log(Deno.exitCode); //-> 0 + * Deno.exitCode = 1; + * console.log(Deno.exitCode); //-> 1 + * ``` + * + * @category Runtime + */ + export var exitCode: number; + + /** An interface containing methods to interact with the process environment + * variables. + * + * @tags allow-env + * @category Runtime + */ + export interface Env { + /** Retrieve the value of an environment variable. + * + * Returns `undefined` if the supplied environment variable is not defined. + * + * ```ts + * console.log(Deno.env.get("HOME")); // e.g. outputs "/home/alice" + * console.log(Deno.env.get("MADE_UP_VAR")); // outputs "undefined" + * ``` + * + * Requires `allow-env` permission. + * + * @tags allow-env + */ + get(key: string): string | undefined; + + /** Set the value of an environment variable. + * + * ```ts + * Deno.env.set("SOME_VAR", "Value"); + * Deno.env.get("SOME_VAR"); // outputs "Value" + * ``` + * + * Requires `allow-env` permission. + * + * @tags allow-env + */ + set(key: string, value: string): void; + + /** Delete the value of an environment variable. + * + * ```ts + * Deno.env.set("SOME_VAR", "Value"); + * Deno.env.delete("SOME_VAR"); // outputs "undefined" + * ``` + * + * Requires `allow-env` permission. + * + * @tags allow-env + */ + delete(key: string): void; + + /** Check whether an environment variable is present or not. + * + * ```ts + * Deno.env.set("SOME_VAR", "Value"); + * Deno.env.has("SOME_VAR"); // outputs true + * ``` + * + * Requires `allow-env` permission. + * + * @tags allow-env + */ + has(key: string): boolean; + + /** Returns a snapshot of the environment variables at invocation as a + * simple object of keys and values. + * + * ```ts + * Deno.env.set("TEST_VAR", "A"); + * const myEnv = Deno.env.toObject(); + * console.log(myEnv.SHELL); + * Deno.env.set("TEST_VAR", "B"); + * console.log(myEnv.TEST_VAR); // outputs "A" + * ``` + * + * Requires `allow-env` permission. + * + * @tags allow-env + */ + toObject(): { [index: string]: string }; + } + + /** An interface containing methods to interact with the process environment + * variables. + * + * @tags allow-env + * @category Runtime + */ + export const env: Env; + + /** + * Returns the path to the current deno executable. + * + * ```ts + * console.log(Deno.execPath()); // e.g. "/home/alice/.local/bin/deno" + * ``` + * + * Requires `allow-read` permission. + * + * @tags allow-read + * @category Runtime + */ + export function execPath(): string; + + /** + * Change the current working directory to the specified path. + * + * ```ts + * Deno.chdir("/home/userA"); + * Deno.chdir("../userB"); + * Deno.chdir("C:\\Program Files (x86)\\Java"); + * ``` + * + * Throws {@linkcode Deno.errors.NotFound} if directory not found. + * + * Throws {@linkcode Deno.errors.PermissionDenied} if the user does not have + * operating system file access rights. + * + * Requires `allow-read` permission. + * + * @tags allow-read + * @category Runtime + */ + export function chdir(directory: string | URL): void; + + /** + * Return a string representing the current working directory. + * + * If the current directory can be reached via multiple paths (due to symbolic + * links), `cwd()` may return any one of them. + * + * ```ts + * const currentWorkingDirectory = Deno.cwd(); + * ``` + * + * Throws {@linkcode Deno.errors.NotFound} if directory not available. + * + * Requires `allow-read` permission. + * + * @tags allow-read + * @category Runtime + */ + export function cwd(): string; + + /** + * Creates `newpath` as a hard link to `oldpath`. + * + * ```ts + * await Deno.link("old/name", "new/name"); + * ``` + * + * Requires `allow-read` and `allow-write` permissions. + * + * @tags allow-read, allow-write + * @category File System + */ + export function link(oldpath: string, newpath: string): Promise; + + /** + * Synchronously creates `newpath` as a hard link to `oldpath`. + * + * ```ts + * Deno.linkSync("old/name", "new/name"); + * ``` + * + * Requires `allow-read` and `allow-write` permissions. + * + * @tags allow-read, allow-write + * @category File System + */ + export function linkSync(oldpath: string, newpath: string): void; + + /** + * A enum which defines the seek mode for IO related APIs that support + * seeking. + * + * @category I/O */ + export enum SeekMode { + /* Seek from the start of the file/resource. */ + Start = 0, + /* Seek from the current position within the file/resource. */ + Current = 1, + /* Seek from the end of the current file/resource. */ + End = 2, + } + + /** Open a file and resolve to an instance of {@linkcode Deno.FsFile}. The + * file does not need to previously exist if using the `create` or `createNew` + * open options. The caller may have the resulting file automatically closed + * by the runtime once it's out of scope by declaring the file variable with + * the `using` keyword. + * + * ```ts + * using file = await Deno.open("/foo/bar.txt", { read: true, write: true }); + * // Do work with file + * ``` + * + * Alternatively, the caller may manually close the resource when finished with + * it. + * + * ```ts + * const file = await Deno.open("/foo/bar.txt", { read: true, write: true }); + * // Do work with file + * file.close(); + * ``` + * + * Requires `allow-read` and/or `allow-write` permissions depending on + * options. + * + * @tags allow-read, allow-write + * @category File System + */ + export function open(path: string | URL, options?: OpenOptions): Promise; + + /** Synchronously open a file and return an instance of + * {@linkcode Deno.FsFile}. The file does not need to previously exist if + * using the `create` or `createNew` open options. The caller may have the + * resulting file automatically closed by the runtime once it's out of scope + * by declaring the file variable with the `using` keyword. + * + * ```ts + * using file = Deno.openSync("/foo/bar.txt", { read: true, write: true }); + * // Do work with file + * ``` + * + * Alternatively, the caller may manually close the resource when finished with + * it. + * + * ```ts + * const file = Deno.openSync("/foo/bar.txt", { read: true, write: true }); + * // Do work with file + * file.close(); + * ``` + * + * Requires `allow-read` and/or `allow-write` permissions depending on + * options. + * + * @tags allow-read, allow-write + * @category File System + */ + export function openSync(path: string | URL, options?: OpenOptions): FsFile; + + /** Creates a file if none exists or truncates an existing file and resolves to + * an instance of {@linkcode Deno.FsFile}. + * + * ```ts + * const file = await Deno.create("/foo/bar.txt"); + * ``` + * + * Requires `allow-read` and `allow-write` permissions. + * + * @tags allow-read, allow-write + * @category File System + */ + export function create(path: string | URL): Promise; + + /** Creates a file if none exists or truncates an existing file and returns + * an instance of {@linkcode Deno.FsFile}. + * + * ```ts + * const file = Deno.createSync("/foo/bar.txt"); + * ``` + * + * Requires `allow-read` and `allow-write` permissions. + * + * @tags allow-read, allow-write + * @category File System + */ + export function createSync(path: string | URL): FsFile; + + /** The Deno abstraction for reading and writing files. + * + * This is the most straight forward way of handling files within Deno and is + * recommended over using the discrete functions within the `Deno` namespace. + * + * ```ts + * using file = await Deno.open("/foo/bar.txt", { read: true }); + * const fileInfo = await file.stat(); + * if (fileInfo.isFile) { + * const buf = new Uint8Array(100); + * const numberOfBytesRead = await file.read(buf); // 11 bytes + * const text = new TextDecoder().decode(buf); // "hello world" + * } + * ``` + * + * @category File System + */ + export class FsFile implements Disposable { + /** A {@linkcode ReadableStream} instance representing to the byte contents + * of the file. This makes it easy to interoperate with other web streams + * based APIs. + * + * ```ts + * using file = await Deno.open("my_file.txt", { read: true }); + * const decoder = new TextDecoder(); + * for await (const chunk of file.readable) { + * console.log(decoder.decode(chunk)); + * } + * ``` + */ + readonly readable: ReadableStream>; + /** A {@linkcode WritableStream} instance to write the contents of the + * file. This makes it easy to interoperate with other web streams based + * APIs. + * + * ```ts + * const items = ["hello", "world"]; + * using file = await Deno.open("my_file.txt", { write: true }); + * const encoder = new TextEncoder(); + * const writer = file.writable.getWriter(); + * for (const item of items) { + * await writer.write(encoder.encode(item)); + * } + * ``` + */ + readonly writable: WritableStream>; + /** Write the contents of the array buffer (`p`) to the file. + * + * Resolves to the number of bytes written. + * + * **It is not guaranteed that the full buffer will be written in a single + * call.** + * + * ```ts + * const encoder = new TextEncoder(); + * const data = encoder.encode("Hello world"); + * using file = await Deno.open("/foo/bar.txt", { write: true }); + * const bytesWritten = await file.write(data); // 11 + * ``` + * + * @category I/O + */ + write(p: Uint8Array): Promise; + /** Synchronously write the contents of the array buffer (`p`) to the file. + * + * Returns the number of bytes written. + * + * **It is not guaranteed that the full buffer will be written in a single + * call.** + * + * ```ts + * const encoder = new TextEncoder(); + * const data = encoder.encode("Hello world"); + * using file = Deno.openSync("/foo/bar.txt", { write: true }); + * const bytesWritten = file.writeSync(data); // 11 + * ``` + */ + writeSync(p: Uint8Array): number; + /** Truncates (or extends) the file to reach the specified `len`. If `len` + * is not specified, then the entire file contents are truncated. + * + * ### Truncate the entire file + * + * ```ts + * using file = await Deno.open("my_file.txt", { write: true }); + * await file.truncate(); + * ``` + * + * ### Truncate part of the file + * + * ```ts + * // if "my_file.txt" contains the text "hello world": + * using file = await Deno.open("my_file.txt", { write: true }); + * await file.truncate(7); + * const buf = new Uint8Array(100); + * await file.read(buf); + * const text = new TextDecoder().decode(buf); // "hello w" + * ``` + */ + truncate(len?: number): Promise; + /** Synchronously truncates (or extends) the file to reach the specified + * `len`. If `len` is not specified, then the entire file contents are + * truncated. + * + * ### Truncate the entire file + * + * ```ts + * using file = Deno.openSync("my_file.txt", { write: true }); + * file.truncateSync(); + * ``` + * + * ### Truncate part of the file + * + * ```ts + * // if "my_file.txt" contains the text "hello world": + * using file = Deno.openSync("my_file.txt", { write: true }); + * file.truncateSync(7); + * const buf = new Uint8Array(100); + * file.readSync(buf); + * const text = new TextDecoder().decode(buf); // "hello w" + * ``` + */ + truncateSync(len?: number): void; + /** Read the file into an array buffer (`p`). + * + * Resolves to either the number of bytes read during the operation or EOF + * (`null`) if there was nothing more to read. + * + * It is possible for a read to successfully return with `0` bytes. This + * does not indicate EOF. + * + * **It is not guaranteed that the full buffer will be read in a single + * call.** + * + * ```ts + * // if "/foo/bar.txt" contains the text "hello world": + * using file = await Deno.open("/foo/bar.txt"); + * const buf = new Uint8Array(100); + * const numberOfBytesRead = await file.read(buf); // 11 bytes + * const text = new TextDecoder().decode(buf); // "hello world" + * ``` + */ + read(p: Uint8Array): Promise; + /** Synchronously read from the file into an array buffer (`p`). + * + * Returns either the number of bytes read during the operation or EOF + * (`null`) if there was nothing more to read. + * + * It is possible for a read to successfully return with `0` bytes. This + * does not indicate EOF. + * + * **It is not guaranteed that the full buffer will be read in a single + * call.** + * + * ```ts + * // if "/foo/bar.txt" contains the text "hello world": + * using file = Deno.openSync("/foo/bar.txt"); + * const buf = new Uint8Array(100); + * const numberOfBytesRead = file.readSync(buf); // 11 bytes + * const text = new TextDecoder().decode(buf); // "hello world" + * ``` + */ + readSync(p: Uint8Array): number | null; + /** Seek to the given `offset` under mode given by `whence`. The call + * resolves to the new position within the resource (bytes from the start). + * + * ```ts + * // Given the file contains "Hello world" text, which is 11 bytes long: + * using file = await Deno.open( + * "hello.txt", + * { read: true, write: true, truncate: true, create: true }, + * ); + * await file.write(new TextEncoder().encode("Hello world")); + * + * // advance cursor 6 bytes + * const cursorPosition = await file.seek(6, Deno.SeekMode.Start); + * console.log(cursorPosition); // 6 + * const buf = new Uint8Array(100); + * await file.read(buf); + * console.log(new TextDecoder().decode(buf)); // "world" + * ``` + * + * The seek modes work as follows: + * + * ```ts + * // Given the file contains "Hello world" text, which is 11 bytes long: + * const file = await Deno.open( + * "hello.txt", + * { read: true, write: true, truncate: true, create: true }, + * ); + * await file.write(new TextEncoder().encode("Hello world")); + * + * // Seek 6 bytes from the start of the file + * console.log(await file.seek(6, Deno.SeekMode.Start)); // "6" + * // Seek 2 more bytes from the current position + * console.log(await file.seek(2, Deno.SeekMode.Current)); // "8" + * // Seek backwards 2 bytes from the end of the file + * console.log(await file.seek(-2, Deno.SeekMode.End)); // "9" (i.e. 11-2) + * ``` + */ + seek(offset: number | bigint, whence: SeekMode): Promise; + /** Synchronously seek to the given `offset` under mode given by `whence`. + * The new position within the resource (bytes from the start) is returned. + * + * ```ts + * using file = Deno.openSync( + * "hello.txt", + * { read: true, write: true, truncate: true, create: true }, + * ); + * file.writeSync(new TextEncoder().encode("Hello world")); + * + * // advance cursor 6 bytes + * const cursorPosition = file.seekSync(6, Deno.SeekMode.Start); + * console.log(cursorPosition); // 6 + * const buf = new Uint8Array(100); + * file.readSync(buf); + * console.log(new TextDecoder().decode(buf)); // "world" + * ``` + * + * The seek modes work as follows: + * + * ```ts + * // Given the file contains "Hello world" text, which is 11 bytes long: + * using file = Deno.openSync( + * "hello.txt", + * { read: true, write: true, truncate: true, create: true }, + * ); + * file.writeSync(new TextEncoder().encode("Hello world")); + * + * // Seek 6 bytes from the start of the file + * console.log(file.seekSync(6, Deno.SeekMode.Start)); // "6" + * // Seek 2 more bytes from the current position + * console.log(file.seekSync(2, Deno.SeekMode.Current)); // "8" + * // Seek backwards 2 bytes from the end of the file + * console.log(file.seekSync(-2, Deno.SeekMode.End)); // "9" (i.e. 11-2) + * ``` + */ + seekSync(offset: number | bigint, whence: SeekMode): number; + /** Resolves to a {@linkcode Deno.FileInfo} for the file. + * + * ```ts + * import { assert } from "jsr:@std/assert"; + * + * using file = await Deno.open("hello.txt"); + * const fileInfo = await file.stat(); + * assert(fileInfo.isFile); + * ``` + */ + stat(): Promise; + /** Synchronously returns a {@linkcode Deno.FileInfo} for the file. + * + * ```ts + * import { assert } from "jsr:@std/assert"; + * + * using file = Deno.openSync("hello.txt") + * const fileInfo = file.statSync(); + * assert(fileInfo.isFile); + * ``` + */ + statSync(): FileInfo; + /** + * Flushes any pending data and metadata operations of the given file + * stream to disk. + * + * ```ts + * const file = await Deno.open( + * "my_file.txt", + * { read: true, write: true, create: true }, + * ); + * await file.write(new TextEncoder().encode("Hello World")); + * await file.truncate(1); + * await file.sync(); + * console.log(await Deno.readTextFile("my_file.txt")); // H + * ``` + * + * @category I/O + */ + sync(): Promise; + /** + * Synchronously flushes any pending data and metadata operations of the given + * file stream to disk. + * + * ```ts + * const file = Deno.openSync( + * "my_file.txt", + * { read: true, write: true, create: true }, + * ); + * file.writeSync(new TextEncoder().encode("Hello World")); + * file.truncateSync(1); + * file.syncSync(); + * console.log(Deno.readTextFileSync("my_file.txt")); // H + * ``` + * + * @category I/O + */ + syncSync(): void; + /** + * Flushes any pending data operations of the given file stream to disk. + * ```ts + * using file = await Deno.open( + * "my_file.txt", + * { read: true, write: true, create: true }, + * ); + * await file.write(new TextEncoder().encode("Hello World")); + * await file.syncData(); + * console.log(await Deno.readTextFile("my_file.txt")); // Hello World + * ``` + * + * @category I/O + */ + syncData(): Promise; + /** + * Synchronously flushes any pending data operations of the given file stream + * to disk. + * + * ```ts + * using file = Deno.openSync( + * "my_file.txt", + * { read: true, write: true, create: true }, + * ); + * file.writeSync(new TextEncoder().encode("Hello World")); + * file.syncDataSync(); + * console.log(Deno.readTextFileSync("my_file.txt")); // Hello World + * ``` + * + * @category I/O + */ + syncDataSync(): void; + /** + * Changes the access (`atime`) and modification (`mtime`) times of the + * file stream resource. Given times are either in seconds (UNIX epoch + * time) or as `Date` objects. + * + * ```ts + * using file = await Deno.open("file.txt", { create: true, write: true }); + * await file.utime(1556495550, new Date()); + * ``` + * + * @category File System + */ + utime(atime: number | Date, mtime: number | Date): Promise; + /** + * Synchronously changes the access (`atime`) and modification (`mtime`) + * times of the file stream resource. Given times are either in seconds + * (UNIX epoch time) or as `Date` objects. + * + * ```ts + * using file = Deno.openSync("file.txt", { create: true, write: true }); + * file.utime(1556495550, new Date()); + * ``` + * + * @category File System + */ + utimeSync(atime: number | Date, mtime: number | Date): void; + /** **UNSTABLE**: New API, yet to be vetted. + * + * Checks if the file resource is a TTY (terminal). + * + * ```ts + * // This example is system and context specific + * using file = await Deno.open("/dev/tty6"); + * file.isTerminal(); // true + * ``` + */ + isTerminal(): boolean; + /** **UNSTABLE**: New API, yet to be vetted. + * + * Set TTY to be under raw mode or not. In raw mode, characters are read and + * returned as is, without being processed. All special processing of + * characters by the terminal is disabled, including echoing input + * characters. Reading from a TTY device in raw mode is faster than reading + * from a TTY device in canonical mode. + * + * ```ts + * using file = await Deno.open("/dev/tty6"); + * file.setRaw(true, { cbreak: true }); + * ``` + */ + setRaw(mode: boolean, options?: SetRawOptions): void; + /** + * Acquire an advisory file-system lock for the file. + * + * @param [exclusive=false] + */ + lock(exclusive?: boolean): Promise; + /** + * Synchronously acquire an advisory file-system lock synchronously for the file. + * + * @param [exclusive=false] + */ + lockSync(exclusive?: boolean): void; + /** + * Release an advisory file-system lock for the file. + */ + unlock(): Promise; + /** + * Synchronously release an advisory file-system lock for the file. + */ + unlockSync(): void; + /** Close the file. Closing a file when you are finished with it is + * important to avoid leaking resources. + * + * ```ts + * using file = await Deno.open("my_file.txt"); + * // do work with "file" object + * ``` + */ + close(): void; + + [Symbol.dispose](): void; + } + + /** Gets the size of the console as columns/rows. + * + * ```ts + * const { columns, rows } = Deno.consoleSize(); + * ``` + * + * This returns the size of the console window as reported by the operating + * system. It's not a reflection of how many characters will fit within the + * console window, but can be used as part of that calculation. + * + * @category I/O + */ + export function consoleSize(): { + columns: number; + rows: number; + }; + + /** @category I/O */ + export interface SetRawOptions { + /** + * The `cbreak` option can be used to indicate that characters that + * correspond to a signal should still be generated. When disabling raw + * mode, this option is ignored. This functionality currently only works on + * Linux and Mac OS. + */ + cbreak: boolean; + } + + /** A reference to `stdin` which can be used to read directly from `stdin`. + * + * It implements the Deno specific + * {@linkcode https://jsr.io/@std/io/doc/types/~/Reader | Reader}, + * {@linkcode https://jsr.io/@std/io/doc/types/~/ReaderSync | ReaderSync}, + * and {@linkcode https://jsr.io/@std/io/doc/types/~/Closer | Closer} + * interfaces as well as provides a {@linkcode ReadableStream} interface. + * + * ### Reading chunks from the readable stream + * + * ```ts + * const decoder = new TextDecoder(); + * for await (const chunk of Deno.stdin.readable) { + * const text = decoder.decode(chunk); + * // do something with the text + * } + * ``` + * + * @category I/O + */ + export const stdin: { + /** Read the incoming data from `stdin` into an array buffer (`p`). + * + * Resolves to either the number of bytes read during the operation or EOF + * (`null`) if there was nothing more to read. + * + * It is possible for a read to successfully return with `0` bytes. This + * does not indicate EOF. + * + * **It is not guaranteed that the full buffer will be read in a single + * call.** + * + * ```ts + * // If the text "hello world" is piped into the script: + * const buf = new Uint8Array(100); + * const numberOfBytesRead = await Deno.stdin.read(buf); // 11 bytes + * const text = new TextDecoder().decode(buf); // "hello world" + * ``` + * + * @category I/O + */ + read(p: Uint8Array): Promise; + /** Synchronously read from the incoming data from `stdin` into an array + * buffer (`p`). + * + * Returns either the number of bytes read during the operation or EOF + * (`null`) if there was nothing more to read. + * + * It is possible for a read to successfully return with `0` bytes. This + * does not indicate EOF. + * + * **It is not guaranteed that the full buffer will be read in a single + * call.** + * + * ```ts + * // If the text "hello world" is piped into the script: + * const buf = new Uint8Array(100); + * const numberOfBytesRead = Deno.stdin.readSync(buf); // 11 bytes + * const text = new TextDecoder().decode(buf); // "hello world" + * ``` + * + * @category I/O + */ + readSync(p: Uint8Array): number | null; + /** Closes `stdin`, freeing the resource. + * + * ```ts + * Deno.stdin.close(); + * ``` + */ + close(): void; + /** A readable stream interface to `stdin`. */ + readonly readable: ReadableStream>; + /** + * Set TTY to be under raw mode or not. In raw mode, characters are read and + * returned as is, without being processed. All special processing of + * characters by the terminal is disabled, including echoing input + * characters. Reading from a TTY device in raw mode is faster than reading + * from a TTY device in canonical mode. + * + * ```ts + * Deno.stdin.setRaw(true, { cbreak: true }); + * ``` + * + * @category I/O + */ + setRaw(mode: boolean, options?: SetRawOptions): void; + /** + * Checks if `stdin` is a TTY (terminal). + * + * ```ts + * // This example is system and context specific + * Deno.stdin.isTerminal(); // true + * ``` + * + * @category I/O + */ + isTerminal(): boolean; + }; + /** A reference to `stdout` which can be used to write directly to `stdout`. + * It implements the Deno specific + * {@linkcode https://jsr.io/@std/io/doc/types/~/Writer | Writer}, + * {@linkcode https://jsr.io/@std/io/doc/types/~/WriterSync | WriterSync}, + * and {@linkcode https://jsr.io/@std/io/doc/types/~/Closer | Closer} interfaces as well as provides a + * {@linkcode WritableStream} interface. + * + * These are low level constructs, and the {@linkcode console} interface is a + * more straight forward way to interact with `stdout` and `stderr`. + * + * @category I/O + */ + export const stdout: { + /** Write the contents of the array buffer (`p`) to `stdout`. + * + * Resolves to the number of bytes written. + * + * **It is not guaranteed that the full buffer will be written in a single + * call.** + * + * ```ts + * const encoder = new TextEncoder(); + * const data = encoder.encode("Hello world"); + * const bytesWritten = await Deno.stdout.write(data); // 11 + * ``` + * + * @category I/O + */ + write(p: Uint8Array): Promise; + /** Synchronously write the contents of the array buffer (`p`) to `stdout`. + * + * Returns the number of bytes written. + * + * **It is not guaranteed that the full buffer will be written in a single + * call.** + * + * ```ts + * const encoder = new TextEncoder(); + * const data = encoder.encode("Hello world"); + * const bytesWritten = Deno.stdout.writeSync(data); // 11 + * ``` + */ + writeSync(p: Uint8Array): number; + /** Closes `stdout`, freeing the resource. + * + * ```ts + * Deno.stdout.close(); + * ``` + */ + close(): void; + /** A writable stream interface to `stdout`. */ + readonly writable: WritableStream>; + /** + * Checks if `stdout` is a TTY (terminal). + * + * ```ts + * // This example is system and context specific + * Deno.stdout.isTerminal(); // true + * ``` + * + * @category I/O + */ + isTerminal(): boolean; + }; + /** A reference to `stderr` which can be used to write directly to `stderr`. + * It implements the Deno specific + * {@linkcode https://jsr.io/@std/io/doc/types/~/Writer | Writer}, + * {@linkcode https://jsr.io/@std/io/doc/types/~/WriterSync | WriterSync}, + * and {@linkcode https://jsr.io/@std/io/doc/types/~/Closer | Closer} interfaces as well as provides a + * {@linkcode WritableStream} interface. + * + * These are low level constructs, and the {@linkcode console} interface is a + * more straight forward way to interact with `stdout` and `stderr`. + * + * @category I/O + */ + export const stderr: { + /** Write the contents of the array buffer (`p`) to `stderr`. + * + * Resolves to the number of bytes written. + * + * **It is not guaranteed that the full buffer will be written in a single + * call.** + * + * ```ts + * const encoder = new TextEncoder(); + * const data = encoder.encode("Hello world"); + * const bytesWritten = await Deno.stderr.write(data); // 11 + * ``` + * + * @category I/O + */ + write(p: Uint8Array): Promise; + /** Synchronously write the contents of the array buffer (`p`) to `stderr`. + * + * Returns the number of bytes written. + * + * **It is not guaranteed that the full buffer will be written in a single + * call.** + * + * ```ts + * const encoder = new TextEncoder(); + * const data = encoder.encode("Hello world"); + * const bytesWritten = Deno.stderr.writeSync(data); // 11 + * ``` + */ + writeSync(p: Uint8Array): number; + /** Closes `stderr`, freeing the resource. + * + * ```ts + * Deno.stderr.close(); + * ``` + */ + close(): void; + /** A writable stream interface to `stderr`. */ + readonly writable: WritableStream>; + /** + * Checks if `stderr` is a TTY (terminal). + * + * ```ts + * // This example is system and context specific + * Deno.stderr.isTerminal(); // true + * ``` + * + * @category I/O + */ + isTerminal(): boolean; + }; + + /** + * Options which can be set when doing {@linkcode Deno.open} and + * {@linkcode Deno.openSync}. + * + * @category File System */ + export interface OpenOptions { + /** Sets the option for read access. This option, when `true`, means that + * the file should be read-able if opened. + * + * @default {true} */ + read?: boolean; + /** Sets the option for write access. This option, when `true`, means that + * the file should be write-able if opened. If the file already exists, + * any write calls on it will overwrite its contents, by default without + * truncating it. + * + * @default {false} */ + write?: boolean; + /** Sets the option for the append mode. This option, when `true`, means + * that writes will append to a file instead of overwriting previous + * contents. + * + * Note that setting `{ write: true, append: true }` has the same effect as + * setting only `{ append: true }`. + * + * @default {false} */ + append?: boolean; + /** Sets the option for truncating a previous file. If a file is + * successfully opened with this option set it will truncate the file to `0` + * size if it already exists. The file must be opened with write access + * for truncate to work. + * + * @default {false} */ + truncate?: boolean; + /** Sets the option to allow creating a new file, if one doesn't already + * exist at the specified path. Requires write or append access to be + * used. + * + * @default {false} */ + create?: boolean; + /** If set to `true`, no file, directory, or symlink is allowed to exist at + * the target location. Requires write or append access to be used. When + * createNew is set to `true`, create and truncate are ignored. + * + * @default {false} */ + createNew?: boolean; + /** Permissions to use if creating the file (defaults to `0o666`, before + * the process's umask). + * + * Ignored on Windows. */ + mode?: number; + } + + /** + * Options which can be set when using {@linkcode Deno.readFile} or + * {@linkcode Deno.readFileSync}. + * + * @category File System */ + export interface ReadFileOptions { + /** + * An abort signal to allow cancellation of the file read operation. + * If the signal becomes aborted the readFile operation will be stopped + * and the promise returned will be rejected with an AbortError. + */ + signal?: AbortSignal; + } + + /** + * Options which can be set when using {@linkcode Deno.mkdir} and + * {@linkcode Deno.mkdirSync}. + * + * @category File System */ + export interface MkdirOptions { + /** If set to `true`, means that any intermediate directories will also be + * created (as with the shell command `mkdir -p`). + * + * Intermediate directories are created with the same permissions. + * + * When recursive is set to `true`, succeeds silently (without changing any + * permissions) if a directory already exists at the path, or if the path + * is a symlink to an existing directory. + * + * @default {false} */ + recursive?: boolean; + /** Permissions to use when creating the directory (defaults to `0o777`, + * before the process's umask). + * + * Ignored on Windows. */ + mode?: number; + } + + /** Creates a new directory with the specified path. + * + * ```ts + * await Deno.mkdir("new_dir"); + * await Deno.mkdir("nested/directories", { recursive: true }); + * await Deno.mkdir("restricted_access_dir", { mode: 0o700 }); + * ``` + * + * Defaults to throwing error if the directory already exists. + * + * Requires `allow-write` permission. + * + * @tags allow-write + * @category File System + */ + export function mkdir(path: string | URL, options?: MkdirOptions): Promise; + + /** Synchronously creates a new directory with the specified path. + * + * ```ts + * Deno.mkdirSync("new_dir"); + * Deno.mkdirSync("nested/directories", { recursive: true }); + * Deno.mkdirSync("restricted_access_dir", { mode: 0o700 }); + * ``` + * + * Defaults to throwing error if the directory already exists. + * + * Requires `allow-write` permission. + * + * @tags allow-write + * @category File System + */ + export function mkdirSync(path: string | URL, options?: MkdirOptions): void; + + /** + * Options which can be set when using {@linkcode Deno.makeTempDir}, + * {@linkcode Deno.makeTempDirSync}, {@linkcode Deno.makeTempFile}, and + * {@linkcode Deno.makeTempFileSync}. + * + * @category File System */ + export interface MakeTempOptions { + /** Directory where the temporary directory should be created (defaults to + * the env variable `TMPDIR`, or the system's default, usually `/tmp`). + * + * Note that if the passed `dir` is relative, the path returned by + * `makeTempFile()` and `makeTempDir()` will also be relative. Be mindful of + * this when changing working directory. */ + dir?: string; + /** String that should precede the random portion of the temporary + * directory's name. */ + prefix?: string; + /** String that should follow the random portion of the temporary + * directory's name. */ + suffix?: string; + } + + /** Creates a new temporary directory in the default directory for temporary + * files, unless `dir` is specified. Other optional options include + * prefixing and suffixing the directory name with `prefix` and `suffix` + * respectively. + * + * This call resolves to the full path to the newly created directory. + * + * Multiple programs calling this function simultaneously will create different + * directories. It is the caller's responsibility to remove the directory when + * no longer needed. + * + * ```ts + * const tempDirName0 = await Deno.makeTempDir(); // e.g. /tmp/2894ea76 + * const tempDirName1 = await Deno.makeTempDir({ prefix: 'my_temp' }); // e.g. /tmp/my_temp339c944d + * ``` + * + * Requires `allow-write` permission. + * + * @tags allow-write + * @category File System + */ + // TODO(ry) Doesn't check permissions. + export function makeTempDir(options?: MakeTempOptions): Promise; + + /** Synchronously creates a new temporary directory in the default directory + * for temporary files, unless `dir` is specified. Other optional options + * include prefixing and suffixing the directory name with `prefix` and + * `suffix` respectively. + * + * The full path to the newly created directory is returned. + * + * Multiple programs calling this function simultaneously will create different + * directories. It is the caller's responsibility to remove the directory when + * no longer needed. + * + * ```ts + * const tempDirName0 = Deno.makeTempDirSync(); // e.g. /tmp/2894ea76 + * const tempDirName1 = Deno.makeTempDirSync({ prefix: 'my_temp' }); // e.g. /tmp/my_temp339c944d + * ``` + * + * Requires `allow-write` permission. + * + * @tags allow-write + * @category File System + */ + // TODO(ry) Doesn't check permissions. + export function makeTempDirSync(options?: MakeTempOptions): string; + + /** Creates a new temporary file in the default directory for temporary + * files, unless `dir` is specified. + * + * Other options include prefixing and suffixing the directory name with + * `prefix` and `suffix` respectively. + * + * This call resolves to the full path to the newly created file. + * + * Multiple programs calling this function simultaneously will create + * different files. It is the caller's responsibility to remove the file when + * no longer needed. + * + * ```ts + * const tmpFileName0 = await Deno.makeTempFile(); // e.g. /tmp/419e0bf2 + * const tmpFileName1 = await Deno.makeTempFile({ prefix: 'my_temp' }); // e.g. /tmp/my_temp754d3098 + * ``` + * + * Requires `allow-write` permission. + * + * @tags allow-write + * @category File System + */ + export function makeTempFile(options?: MakeTempOptions): Promise; + + /** Synchronously creates a new temporary file in the default directory for + * temporary files, unless `dir` is specified. + * + * Other options include prefixing and suffixing the directory name with + * `prefix` and `suffix` respectively. + * + * The full path to the newly created file is returned. + * + * Multiple programs calling this function simultaneously will create + * different files. It is the caller's responsibility to remove the file when + * no longer needed. + * + * ```ts + * const tempFileName0 = Deno.makeTempFileSync(); // e.g. /tmp/419e0bf2 + * const tempFileName1 = Deno.makeTempFileSync({ prefix: 'my_temp' }); // e.g. /tmp/my_temp754d3098 + * ``` + * + * Requires `allow-write` permission. + * + * @tags allow-write + * @category File System + */ + export function makeTempFileSync(options?: MakeTempOptions): string; + + /** Changes the permission of a specific file/directory of specified path. + * Ignores the process's umask. + * + * ```ts + * await Deno.chmod("/path/to/file", 0o666); + * ``` + * + * The mode is a sequence of 3 octal numbers. The first/left-most number + * specifies the permissions for the owner. The second number specifies the + * permissions for the group. The last/right-most number specifies the + * permissions for others. For example, with a mode of 0o764, the owner (7) + * can read/write/execute, the group (6) can read/write and everyone else (4) + * can read only. + * + * | Number | Description | + * | ------ | ----------- | + * | 7 | read, write, and execute | + * | 6 | read and write | + * | 5 | read and execute | + * | 4 | read only | + * | 3 | write and execute | + * | 2 | write only | + * | 1 | execute only | + * | 0 | no permission | + * + * NOTE: This API currently throws on Windows + * + * Requires `allow-write` permission. + * + * @tags allow-write + * @category File System + */ + export function chmod(path: string | URL, mode: number): Promise; + + /** Synchronously changes the permission of a specific file/directory of + * specified path. Ignores the process's umask. + * + * ```ts + * Deno.chmodSync("/path/to/file", 0o666); + * ``` + * + * For a full description, see {@linkcode Deno.chmod}. + * + * NOTE: This API currently throws on Windows + * + * Requires `allow-write` permission. + * + * @tags allow-write + * @category File System + */ + export function chmodSync(path: string | URL, mode: number): void; + + /** Change owner of a regular file or directory. + * + * This functionality is not available on Windows. + * + * ```ts + * await Deno.chown("myFile.txt", 1000, 1002); + * ``` + * + * Requires `allow-write` permission. + * + * Throws Error (not implemented) if executed on Windows. + * + * @tags allow-write + * @category File System + * + * @param path path to the file + * @param uid user id (UID) of the new owner, or `null` for no change + * @param gid group id (GID) of the new owner, or `null` for no change + */ + export function chown(path: string | URL, uid: number | null, gid: number | null): Promise; + + /** Synchronously change owner of a regular file or directory. + * + * This functionality is not available on Windows. + * + * ```ts + * Deno.chownSync("myFile.txt", 1000, 1002); + * ``` + * + * Requires `allow-write` permission. + * + * Throws Error (not implemented) if executed on Windows. + * + * @tags allow-write + * @category File System + * + * @param path path to the file + * @param uid user id (UID) of the new owner, or `null` for no change + * @param gid group id (GID) of the new owner, or `null` for no change + */ + export function chownSync(path: string | URL, uid: number | null, gid: number | null): void; + + /** + * Options which can be set when using {@linkcode Deno.remove} and + * {@linkcode Deno.removeSync}. + * + * @category File System */ + export interface RemoveOptions { + /** If set to `true`, path will be removed even if it's a non-empty directory. + * + * @default {false} */ + recursive?: boolean; + } + + /** Removes the named file or directory. + * + * ```ts + * await Deno.remove("/path/to/empty_dir/or/file"); + * await Deno.remove("/path/to/populated_dir/or/file", { recursive: true }); + * ``` + * + * Throws error if permission denied, path not found, or path is a non-empty + * directory and the `recursive` option isn't set to `true`. + * + * Requires `allow-write` permission. + * + * @tags allow-write + * @category File System + */ + export function remove(path: string | URL, options?: RemoveOptions): Promise; + + /** Synchronously removes the named file or directory. + * + * ```ts + * Deno.removeSync("/path/to/empty_dir/or/file"); + * Deno.removeSync("/path/to/populated_dir/or/file", { recursive: true }); + * ``` + * + * Throws error if permission denied, path not found, or path is a non-empty + * directory and the `recursive` option isn't set to `true`. + * + * Requires `allow-write` permission. + * + * @tags allow-write + * @category File System + */ + export function removeSync(path: string | URL, options?: RemoveOptions): void; + + /** Synchronously renames (moves) `oldpath` to `newpath`. Paths may be files or + * directories. If `newpath` already exists and is not a directory, + * `renameSync()` replaces it. OS-specific restrictions may apply when + * `oldpath` and `newpath` are in different directories. + * + * ```ts + * Deno.renameSync("old/path", "new/path"); + * ``` + * + * On Unix-like OSes, this operation does not follow symlinks at either path. + * + * It varies between platforms when the operation throws errors, and if so what + * they are. It's always an error to rename anything to a non-empty directory. + * + * Requires `allow-read` and `allow-write` permissions. + * + * @tags allow-read, allow-write + * @category File System + */ + export function renameSync(oldpath: string | URL, newpath: string | URL): void; + + /** Renames (moves) `oldpath` to `newpath`. Paths may be files or directories. + * If `newpath` already exists and is not a directory, `rename()` replaces it. + * OS-specific restrictions may apply when `oldpath` and `newpath` are in + * different directories. + * + * ```ts + * await Deno.rename("old/path", "new/path"); + * ``` + * + * On Unix-like OSes, this operation does not follow symlinks at either path. + * + * It varies between platforms when the operation throws errors, and if so + * what they are. It's always an error to rename anything to a non-empty + * directory. + * + * Requires `allow-read` and `allow-write` permissions. + * + * @tags allow-read, allow-write + * @category File System + */ + export function rename(oldpath: string | URL, newpath: string | URL): Promise; + + /** Asynchronously reads and returns the entire contents of a file as an UTF-8 + * decoded string. Reading a directory throws an error. + * + * ```ts + * const data = await Deno.readTextFile("hello.txt"); + * console.log(data); + * ``` + * + * Requires `allow-read` permission. + * + * @tags allow-read + * @category File System + */ + export function readTextFile(path: string | URL, options?: ReadFileOptions): Promise; + + /** Synchronously reads and returns the entire contents of a file as an UTF-8 + * decoded string. Reading a directory throws an error. + * + * ```ts + * const data = Deno.readTextFileSync("hello.txt"); + * console.log(data); + * ``` + * + * Requires `allow-read` permission. + * + * @tags allow-read + * @category File System + */ + export function readTextFileSync(path: string | URL): string; + + /** Reads and resolves to the entire contents of a file as an array of bytes. + * `TextDecoder` can be used to transform the bytes to string if required. + * Rejects with an error when reading a directory. + * + * ```ts + * const decoder = new TextDecoder("utf-8"); + * const data = await Deno.readFile("hello.txt"); + * console.log(decoder.decode(data)); + * ``` + * + * Requires `allow-read` permission. + * + * @tags allow-read + * @category File System + */ + export function readFile(path: string | URL, options?: ReadFileOptions): Promise>; + + /** Synchronously reads and returns the entire contents of a file as an array + * of bytes. `TextDecoder` can be used to transform the bytes to string if + * required. Throws an error when reading a directory. + * + * ```ts + * const decoder = new TextDecoder("utf-8"); + * const data = Deno.readFileSync("hello.txt"); + * console.log(decoder.decode(data)); + * ``` + * + * Requires `allow-read` permission. + * + * @tags allow-read + * @category File System + */ + export function readFileSync(path: string | URL): Uint8Array; + + /** Provides information about a file and is returned by + * {@linkcode Deno.stat}, {@linkcode Deno.lstat}, {@linkcode Deno.statSync}, + * and {@linkcode Deno.lstatSync} or from calling `stat()` and `statSync()` + * on an {@linkcode Deno.FsFile} instance. + * + * @category File System + */ + export interface FileInfo { + /** True if this is info for a regular file. Mutually exclusive to + * `FileInfo.isDirectory` and `FileInfo.isSymlink`. */ + isFile: boolean; + /** True if this is info for a regular directory. Mutually exclusive to + * `FileInfo.isFile` and `FileInfo.isSymlink`. */ + isDirectory: boolean; + /** True if this is info for a symlink. Mutually exclusive to + * `FileInfo.isFile` and `FileInfo.isDirectory`. */ + isSymlink: boolean; + /** The size of the file, in bytes. */ + size: number; + /** The last modification time of the file. This corresponds to the `mtime` + * field from `stat` on Linux/Mac OS and `ftLastWriteTime` on Windows. This + * may not be available on all platforms. */ + mtime: Date | null; + /** The last access time of the file. This corresponds to the `atime` + * field from `stat` on Unix and `ftLastAccessTime` on Windows. This may not + * be available on all platforms. */ + atime: Date | null; + /** The creation time of the file. This corresponds to the `birthtime` + * field from `stat` on Mac/BSD and `ftCreationTime` on Windows. This may + * not be available on all platforms. */ + birthtime: Date | null; + /** The last change time of the file. This corresponds to the `ctime` + * field from `stat` on Mac/BSD and `ChangeTime` on Windows. This may + * not be available on all platforms. */ + ctime: Date | null; + /** ID of the device containing the file. */ + dev: number; + /** Inode number. + * + * _Linux/Mac OS only._ */ + ino: number | null; + /** The underlying raw `st_mode` bits that contain the standard Unix + * permissions for this file/directory. + */ + mode: number | null; + /** Number of hard links pointing to this file. + * + * _Linux/Mac OS only._ */ + nlink: number | null; + /** User ID of the owner of this file. + * + * _Linux/Mac OS only._ */ + uid: number | null; + /** Group ID of the owner of this file. + * + * _Linux/Mac OS only._ */ + gid: number | null; + /** Device ID of this file. + * + * _Linux/Mac OS only._ */ + rdev: number | null; + /** Blocksize for filesystem I/O. + * + * _Linux/Mac OS only._ */ + blksize: number | null; + /** Number of blocks allocated to the file, in 512-byte units. + * + * _Linux/Mac OS only._ */ + blocks: number | null; + /** True if this is info for a block device. + * + * _Linux/Mac OS only._ */ + isBlockDevice: boolean | null; + /** True if this is info for a char device. + * + * _Linux/Mac OS only._ */ + isCharDevice: boolean | null; + /** True if this is info for a fifo. + * + * _Linux/Mac OS only._ */ + isFifo: boolean | null; + /** True if this is info for a socket. + * + * _Linux/Mac OS only._ */ + isSocket: boolean | null; + } + + /** Resolves to the absolute normalized path, with symbolic links resolved. + * + * ```ts + * // e.g. given /home/alice/file.txt and current directory /home/alice + * await Deno.symlink("file.txt", "symlink_file.txt"); + * const realPath = await Deno.realPath("./file.txt"); + * const realSymLinkPath = await Deno.realPath("./symlink_file.txt"); + * console.log(realPath); // outputs "/home/alice/file.txt" + * console.log(realSymLinkPath); // outputs "/home/alice/file.txt" + * ``` + * + * Requires `allow-read` permission for the target path. + * + * Also requires `allow-read` permission for the `CWD` if the target path is + * relative. + * + * @tags allow-read + * @category File System + */ + export function realPath(path: string | URL): Promise; + + /** Synchronously returns absolute normalized path, with symbolic links + * resolved. + * + * ```ts + * // e.g. given /home/alice/file.txt and current directory /home/alice + * Deno.symlinkSync("file.txt", "symlink_file.txt"); + * const realPath = Deno.realPathSync("./file.txt"); + * const realSymLinkPath = Deno.realPathSync("./symlink_file.txt"); + * console.log(realPath); // outputs "/home/alice/file.txt" + * console.log(realSymLinkPath); // outputs "/home/alice/file.txt" + * ``` + * + * Requires `allow-read` permission for the target path. + * + * Also requires `allow-read` permission for the `CWD` if the target path is + * relative. + * + * @tags allow-read + * @category File System + */ + export function realPathSync(path: string | URL): string; + + /** + * Information about a directory entry returned from {@linkcode Deno.readDir} + * and {@linkcode Deno.readDirSync}. + * + * @category File System */ + export interface DirEntry { + /** The file name of the entry. It is just the entity name and does not + * include the full path. */ + name: string; + /** True if this is info for a regular file. Mutually exclusive to + * `DirEntry.isDirectory` and `DirEntry.isSymlink`. */ + isFile: boolean; + /** True if this is info for a regular directory. Mutually exclusive to + * `DirEntry.isFile` and `DirEntry.isSymlink`. */ + isDirectory: boolean; + /** True if this is info for a symlink. Mutually exclusive to + * `DirEntry.isFile` and `DirEntry.isDirectory`. */ + isSymlink: boolean; + } + + /** Reads the directory given by `path` and returns an async iterable of + * {@linkcode Deno.DirEntry}. The order of entries is not guaranteed. + * + * ```ts + * for await (const dirEntry of Deno.readDir("/")) { + * console.log(dirEntry.name); + * } + * ``` + * + * Throws error if `path` is not a directory. + * + * Requires `allow-read` permission. + * + * @tags allow-read + * @category File System + */ + export function readDir(path: string | URL): AsyncIterable; + + /** Synchronously reads the directory given by `path` and returns an iterable + * of {@linkcode Deno.DirEntry}. The order of entries is not guaranteed. + * + * ```ts + * for (const dirEntry of Deno.readDirSync("/")) { + * console.log(dirEntry.name); + * } + * ``` + * + * Throws error if `path` is not a directory. + * + * Requires `allow-read` permission. + * + * @tags allow-read + * @category File System + */ + export function readDirSync(path: string | URL): IteratorObject; + + /** Copies the contents and permissions of one file to another specified path, + * by default creating a new file if needed, else overwriting. Fails if target + * path is a directory or is unwritable. + * + * ```ts + * await Deno.copyFile("from.txt", "to.txt"); + * ``` + * + * Requires `allow-read` permission on `fromPath`. + * + * Requires `allow-write` permission on `toPath`. + * + * @tags allow-read, allow-write + * @category File System + */ + export function copyFile(fromPath: string | URL, toPath: string | URL): Promise; + + /** Synchronously copies the contents and permissions of one file to another + * specified path, by default creating a new file if needed, else overwriting. + * Fails if target path is a directory or is unwritable. + * + * ```ts + * Deno.copyFileSync("from.txt", "to.txt"); + * ``` + * + * Requires `allow-read` permission on `fromPath`. + * + * Requires `allow-write` permission on `toPath`. + * + * @tags allow-read, allow-write + * @category File System + */ + export function copyFileSync(fromPath: string | URL, toPath: string | URL): void; + + /** Resolves to the full path destination of the named symbolic link. + * + * ```ts + * await Deno.symlink("./test.txt", "./test_link.txt"); + * const target = await Deno.readLink("./test_link.txt"); // full path of ./test.txt + * ``` + * + * Throws TypeError if called with a hard link. + * + * Requires `allow-read` permission. + * + * @tags allow-read + * @category File System + */ + export function readLink(path: string | URL): Promise; + + /** Synchronously returns the full path destination of the named symbolic + * link. + * + * ```ts + * Deno.symlinkSync("./test.txt", "./test_link.txt"); + * const target = Deno.readLinkSync("./test_link.txt"); // full path of ./test.txt + * ``` + * + * Throws TypeError if called with a hard link. + * + * Requires `allow-read` permission. + * + * @tags allow-read + * @category File System + */ + export function readLinkSync(path: string | URL): string; + + /** Resolves to a {@linkcode Deno.FileInfo} for the specified `path`. If + * `path` is a symlink, information for the symlink will be returned instead + * of what it points to. + * + * ```ts + * import { assert } from "jsr:@std/assert"; + * const fileInfo = await Deno.lstat("hello.txt"); + * assert(fileInfo.isFile); + * ``` + * + * Requires `allow-read` permission. + * + * @tags allow-read + * @category File System + */ + export function lstat(path: string | URL): Promise; + + /** Synchronously returns a {@linkcode Deno.FileInfo} for the specified + * `path`. If `path` is a symlink, information for the symlink will be + * returned instead of what it points to. + * + * ```ts + * import { assert } from "jsr:@std/assert"; + * const fileInfo = Deno.lstatSync("hello.txt"); + * assert(fileInfo.isFile); + * ``` + * + * Requires `allow-read` permission. + * + * @tags allow-read + * @category File System + */ + export function lstatSync(path: string | URL): FileInfo; + + /** Resolves to a {@linkcode Deno.FileInfo} for the specified `path`. Will + * always follow symlinks. + * + * ```ts + * import { assert } from "jsr:@std/assert"; + * const fileInfo = await Deno.stat("hello.txt"); + * assert(fileInfo.isFile); + * ``` + * + * Requires `allow-read` permission. + * + * @tags allow-read + * @category File System + */ + export function stat(path: string | URL): Promise; + + /** Synchronously returns a {@linkcode Deno.FileInfo} for the specified + * `path`. Will always follow symlinks. + * + * ```ts + * import { assert } from "jsr:@std/assert"; + * const fileInfo = Deno.statSync("hello.txt"); + * assert(fileInfo.isFile); + * ``` + * + * Requires `allow-read` permission. + * + * @tags allow-read + * @category File System + */ + export function statSync(path: string | URL): FileInfo; + + /** Options for writing to a file. + * + * @category File System + */ + export interface WriteFileOptions { + /** If set to `true`, will append to a file instead of overwriting previous + * contents. + * + * @default {false} */ + append?: boolean; + /** Sets the option to allow creating a new file, if one doesn't already + * exist at the specified path. + * + * @default {true} */ + create?: boolean; + /** If set to `true`, no file, directory, or symlink is allowed to exist at + * the target location. When createNew is set to `true`, `create` is ignored. + * + * @default {false} */ + createNew?: boolean; + /** Permissions always applied to file. */ + mode?: number; + /** An abort signal to allow cancellation of the file write operation. + * + * If the signal becomes aborted the write file operation will be stopped + * and the promise returned will be rejected with an {@linkcode AbortError}. + */ + signal?: AbortSignal; + } + + /** Write `data` to the given `path`, by default creating a new file if + * needed, else overwriting. + * + * ```ts + * const encoder = new TextEncoder(); + * const data = encoder.encode("Hello world\n"); + * await Deno.writeFile("hello1.txt", data); // overwrite "hello1.txt" or create it + * await Deno.writeFile("hello2.txt", data, { create: false }); // only works if "hello2.txt" exists + * await Deno.writeFile("hello3.txt", data, { mode: 0o777 }); // set permissions on new file + * await Deno.writeFile("hello4.txt", data, { append: true }); // add data to the end of the file + * ``` + * + * Requires `allow-write` permission, and `allow-read` if `options.create` is + * `false`. + * + * @tags allow-read, allow-write + * @category File System + */ + export function writeFile( + path: string | URL, + data: Uint8Array | ReadableStream, + options?: WriteFileOptions, + ): Promise; + + /** Synchronously write `data` to the given `path`, by default creating a new + * file if needed, else overwriting. + * + * ```ts + * const encoder = new TextEncoder(); + * const data = encoder.encode("Hello world\n"); + * Deno.writeFileSync("hello1.txt", data); // overwrite "hello1.txt" or create it + * Deno.writeFileSync("hello2.txt", data, { create: false }); // only works if "hello2.txt" exists + * Deno.writeFileSync("hello3.txt", data, { mode: 0o777 }); // set permissions on new file + * Deno.writeFileSync("hello4.txt", data, { append: true }); // add data to the end of the file + * ``` + * + * Requires `allow-write` permission, and `allow-read` if `options.create` is + * `false`. + * + * @tags allow-read, allow-write + * @category File System + */ + export function writeFileSync(path: string | URL, data: Uint8Array, options?: WriteFileOptions): void; + + /** Write string `data` to the given `path`, by default creating a new file if + * needed, else overwriting. + * + * ```ts + * await Deno.writeTextFile("hello1.txt", "Hello world\n"); // overwrite "hello1.txt" or create it + * ``` + * + * Requires `allow-write` permission, and `allow-read` if `options.create` is + * `false`. + * + * @tags allow-read, allow-write + * @category File System + */ + export function writeTextFile( + path: string | URL, + data: string | ReadableStream, + options?: WriteFileOptions, + ): Promise; + + /** Synchronously write string `data` to the given `path`, by default creating + * a new file if needed, else overwriting. + * + * ```ts + * Deno.writeTextFileSync("hello1.txt", "Hello world\n"); // overwrite "hello1.txt" or create it + * ``` + * + * Requires `allow-write` permission, and `allow-read` if `options.create` is + * `false`. + * + * @tags allow-read, allow-write + * @category File System + */ + export function writeTextFileSync(path: string | URL, data: string, options?: WriteFileOptions): void; + + /** Truncates (or extends) the specified file, to reach the specified `len`. + * If `len` is not specified then the entire file contents are truncated. + * + * ### Truncate the entire file + * ```ts + * await Deno.truncate("my_file.txt"); + * ``` + * + * ### Truncate part of the file + * + * ```ts + * const file = await Deno.makeTempFile(); + * await Deno.writeTextFile(file, "Hello World"); + * await Deno.truncate(file, 7); + * const data = await Deno.readFile(file); + * console.log(new TextDecoder().decode(data)); // "Hello W" + * ``` + * + * Requires `allow-write` permission. + * + * @tags allow-write + * @category File System + */ + export function truncate(name: string, len?: number): Promise; + + /** Synchronously truncates (or extends) the specified file, to reach the + * specified `len`. If `len` is not specified then the entire file contents + * are truncated. + * + * ### Truncate the entire file + * + * ```ts + * Deno.truncateSync("my_file.txt"); + * ``` + * + * ### Truncate part of the file + * + * ```ts + * const file = Deno.makeTempFileSync(); + * Deno.writeFileSync(file, new TextEncoder().encode("Hello World")); + * Deno.truncateSync(file, 7); + * const data = Deno.readFileSync(file); + * console.log(new TextDecoder().decode(data)); + * ``` + * + * Requires `allow-write` permission. + * + * @tags allow-write + * @category File System + */ + export function truncateSync(name: string, len?: number): void; + + /** @category Runtime + * + * @deprecated This will be removed in Deno 2.0. + */ + export interface OpMetrics { + opsDispatched: number; + opsDispatchedSync: number; + opsDispatchedAsync: number; + opsDispatchedAsyncUnref: number; + opsCompleted: number; + opsCompletedSync: number; + opsCompletedAsync: number; + opsCompletedAsyncUnref: number; + bytesSentControl: number; + bytesSentData: number; + bytesReceived: number; + } + + /** + * Additional information for FsEvent objects with the "other" kind. + * + * - `"rescan"`: rescan notices indicate either a lapse in the events or a + * change in the filesystem such that events received so far can no longer + * be relied on to represent the state of the filesystem now. An + * application that simply reacts to file changes may not care about this. + * An application that keeps an in-memory representation of the filesystem + * will need to care, and will need to refresh that representation directly + * from the filesystem. + * + * @category File System + */ + export type FsEventFlag = "rescan"; + + /** + * Represents a unique file system event yielded by a + * {@linkcode Deno.FsWatcher}. + * + * @category File System */ + export interface FsEvent { + /** The kind/type of the file system event. */ + kind: "any" | "access" | "create" | "modify" | "rename" | "remove" | "other"; + /** An array of paths that are associated with the file system event. */ + paths: string[]; + /** Any additional flags associated with the event. */ + flag?: FsEventFlag; + } + + /** + * Returned by {@linkcode Deno.watchFs}. It is an async iterator yielding up + * system events. To stop watching the file system by calling `.close()` + * method. + * + * @category File System + */ + export interface FsWatcher extends AsyncIterable, Disposable { + /** Stops watching the file system and closes the watcher resource. */ + close(): void; + /** + * Stops watching the file system and closes the watcher resource. + */ + return?(value?: any): Promise>; + [Symbol.asyncIterator](): AsyncIterableIterator; + } + + /** Watch for file system events against one or more `paths`, which can be + * files or directories. These paths must exist already. One user action (e.g. + * `touch test.file`) can generate multiple file system events. Likewise, + * one user action can result in multiple file paths in one event (e.g. `mv + * old_name.txt new_name.txt`). + * + * The recursive option is `true` by default and, for directories, will watch + * the specified directory and all sub directories. + * + * Note that the exact ordering of the events can vary between operating + * systems. + * + * ```ts + * const watcher = Deno.watchFs("/"); + * for await (const event of watcher) { + * console.log(">>>> event", event); + * // { kind: "create", paths: [ "/foo.txt" ] } + * } + * ``` + * + * Call `watcher.close()` to stop watching. + * + * ```ts + * const watcher = Deno.watchFs("/"); + * + * setTimeout(() => { + * watcher.close(); + * }, 5000); + * + * for await (const event of watcher) { + * console.log(">>>> event", event); + * } + * ``` + * + * Requires `allow-read` permission. + * + * @tags allow-read + * @category File System + */ + export function watchFs(paths: string | string[], options?: { recursive: boolean }): FsWatcher; + + /** Operating signals which can be listened for or sent to sub-processes. What + * signals and what their standard behaviors are OS dependent. + * + * @category Runtime */ + export type Signal = + | "SIGABRT" + | "SIGALRM" + | "SIGBREAK" + | "SIGBUS" + | "SIGCHLD" + | "SIGCONT" + | "SIGEMT" + | "SIGFPE" + | "SIGHUP" + | "SIGILL" + | "SIGINFO" + | "SIGINT" + | "SIGIO" + | "SIGPOLL" + | "SIGUNUSED" + | "SIGKILL" + | "SIGPIPE" + | "SIGPROF" + | "SIGPWR" + | "SIGQUIT" + | "SIGSEGV" + | "SIGSTKFLT" + | "SIGSTOP" + | "SIGSYS" + | "SIGTERM" + | "SIGTRAP" + | "SIGTSTP" + | "SIGTTIN" + | "SIGTTOU" + | "SIGURG" + | "SIGUSR1" + | "SIGUSR2" + | "SIGVTALRM" + | "SIGWINCH" + | "SIGXCPU" + | "SIGXFSZ"; + + /** Registers the given function as a listener of the given signal event. + * + * ```ts + * Deno.addSignalListener( + * "SIGTERM", + * () => { + * console.log("SIGTERM!") + * } + * ); + * ``` + * + * _Note_: On Windows only `"SIGINT"` (CTRL+C) and `"SIGBREAK"` (CTRL+Break) + * are supported. + * + * @category Runtime + */ + export function addSignalListener(signal: Signal, handler: () => void): void; + + /** Removes the given signal listener that has been registered with + * {@linkcode Deno.addSignalListener}. + * + * ```ts + * const listener = () => { + * console.log("SIGTERM!") + * }; + * Deno.addSignalListener("SIGTERM", listener); + * Deno.removeSignalListener("SIGTERM", listener); + * ``` + * + * _Note_: On Windows only `"SIGINT"` (CTRL+C) and `"SIGBREAK"` (CTRL+Break) + * are supported. + * + * @category Runtime + */ + export function removeSignalListener(signal: Signal, handler: () => void): void; + + /** Create a child process. + * + * If any stdio options are not set to `"piped"`, accessing the corresponding + * field on the `Command` or its `CommandOutput` will throw a `TypeError`. + * + * If `stdin` is set to `"piped"`, the `stdin` {@linkcode WritableStream} + * needs to be closed manually. + * + * `Command` acts as a builder. Each call to {@linkcode Command.spawn} or + * {@linkcode Command.output} will spawn a new subprocess. + * + * @example Spawn a subprocess and pipe the output to a file + * + * ```ts + * const command = new Deno.Command(Deno.execPath(), { + * args: [ + * "eval", + * "console.log('Hello World')", + * ], + * stdin: "piped", + * stdout: "piped", + * }); + * const child = command.spawn(); + * + * // open a file and pipe the subprocess output to it. + * child.stdout.pipeTo( + * Deno.openSync("output", { write: true, create: true }).writable, + * ); + * + * // manually close stdin + * child.stdin.close(); + * const status = await child.status; + * ``` + * + * @example Spawn a subprocess and collect its output + * + * ```ts + * const command = new Deno.Command(Deno.execPath(), { + * args: [ + * "eval", + * "console.log('hello'); console.error('world')", + * ], + * }); + * const { code, stdout, stderr } = await command.output(); + * console.assert(code === 0); + * console.assert("hello\n" === new TextDecoder().decode(stdout)); + * console.assert("world\n" === new TextDecoder().decode(stderr)); + * ``` + * + * @example Spawn a subprocess and collect its output synchronously + * + * ```ts + * const command = new Deno.Command(Deno.execPath(), { + * args: [ + * "eval", + * "console.log('hello'); console.error('world')", + * ], + * }); + * const { code, stdout, stderr } = command.outputSync(); + * console.assert(code === 0); + * console.assert("hello\n" === new TextDecoder().decode(stdout)); + * console.assert("world\n" === new TextDecoder().decode(stderr)); + * ``` + * + * @tags allow-run + * @category Subprocess + */ + export class Command { + constructor(command: string | URL, options?: CommandOptions); + /** + * Executes the {@linkcode Deno.Command}, waiting for it to finish and + * collecting all of its output. + * + * Will throw an error if `stdin: "piped"` is set. + * + * If options `stdout` or `stderr` are not set to `"piped"`, accessing the + * corresponding field on {@linkcode Deno.CommandOutput} will throw a `TypeError`. + */ + output(): Promise; + /** + * Synchronously executes the {@linkcode Deno.Command}, waiting for it to + * finish and collecting all of its output. + * + * Will throw an error if `stdin: "piped"` is set. + * + * If options `stdout` or `stderr` are not set to `"piped"`, accessing the + * corresponding field on {@linkcode Deno.CommandOutput} will throw a `TypeError`. + */ + outputSync(): CommandOutput; + /** + * Spawns a streamable subprocess, allowing to use the other methods. + */ + spawn(): ChildProcess; + } + + /** + * The interface for handling a child process returned from + * {@linkcode Deno.Command.spawn}. + * + * @category Subprocess + */ + export class ChildProcess implements AsyncDisposable { + get stdin(): WritableStream>; + get stdout(): ReadableStream>; + get stderr(): ReadableStream>; + readonly pid: number; + /** Get the status of the child. */ + readonly status: Promise; + + /** Waits for the child to exit completely, returning all its output and + * status. */ + output(): Promise; + /** Kills the process with given {@linkcode Deno.Signal}. + * + * Defaults to `SIGTERM` if no signal is provided. + * + * @param [signo="SIGTERM"] + */ + kill(signo?: Signal): void; + + /** Ensure that the status of the child process prevents the Deno process + * from exiting. */ + ref(): void; + /** Ensure that the status of the child process does not block the Deno + * process from exiting. */ + unref(): void; + + [Symbol.asyncDispose](): Promise; + } + + /** + * Options which can be set when calling {@linkcode Deno.Command}. + * + * @category Subprocess + */ + export interface CommandOptions { + /** Arguments to pass to the process. */ + args?: string[]; + /** + * The working directory of the process. + * + * If not specified, the `cwd` of the parent process is used. + */ + cwd?: string | URL; + /** + * Clear environmental variables from parent process. + * + * Doesn't guarantee that only `env` variables are present, as the OS may + * set environmental variables for processes. + * + * @default {false} + */ + clearEnv?: boolean; + /** Environmental variables to pass to the subprocess. */ + env?: Record; + /** + * Sets the child process’s user ID. This translates to a setuid call in the + * child process. Failure in the set uid call will cause the spawn to fail. + */ + uid?: number; + /** Similar to `uid`, but sets the group ID of the child process. */ + gid?: number; + /** + * An {@linkcode AbortSignal} that allows closing the process using the + * corresponding {@linkcode AbortController} by sending the process a + * SIGTERM signal. + * + * Not supported in {@linkcode Deno.Command.outputSync}. + */ + signal?: AbortSignal; + + /** How `stdin` of the spawned process should be handled. + * + * Defaults to `"inherit"` for `output` & `outputSync`, + * and `"inherit"` for `spawn`. */ + stdin?: "piped" | "inherit" | "null"; + /** How `stdout` of the spawned process should be handled. + * + * Defaults to `"piped"` for `output` & `outputSync`, + * and `"inherit"` for `spawn`. */ + stdout?: "piped" | "inherit" | "null"; + /** How `stderr` of the spawned process should be handled. + * + * Defaults to `"piped"` for `output` & `outputSync`, + * and `"inherit"` for `spawn`. */ + stderr?: "piped" | "inherit" | "null"; + + /** Skips quoting and escaping of the arguments on windows. This option + * is ignored on non-windows platforms. + * + * @default {false} */ + windowsRawArguments?: boolean; + } + + /** + * @category Subprocess + */ + export interface CommandStatus { + /** If the child process exits with a 0 status code, `success` will be set + * to `true`, otherwise `false`. */ + success: boolean; + /** The exit code of the child process. */ + code: number; + /** The signal associated with the child process. */ + signal: Signal | null; + } + + /** + * The interface returned from calling {@linkcode Deno.Command.output} or + * {@linkcode Deno.Command.outputSync} which represents the result of spawning the + * child process. + * + * @category Subprocess + */ + export interface CommandOutput extends CommandStatus { + /** The buffered output from the child process' `stdout`. */ + readonly stdout: Uint8Array; + /** The buffered output from the child process' `stderr`. */ + readonly stderr: Uint8Array; + } + + /** Option which can be specified when performing {@linkcode Deno.inspect}. + * + * @category I/O */ + export interface InspectOptions { + /** Stylize output with ANSI colors. + * + * @default {false} */ + colors?: boolean; + /** Try to fit more than one entry of a collection on the same line. + * + * @default {true} */ + compact?: boolean; + /** Traversal depth for nested objects. + * + * @default {4} */ + depth?: number; + /** The maximum length for an inspection to take up a single line. + * + * @default {80} */ + breakLength?: number; + /** Whether or not to escape sequences. + * + * @default {true} */ + escapeSequences?: boolean; + /** The maximum number of iterable entries to print. + * + * @default {100} */ + iterableLimit?: number; + /** Show a Proxy's target and handler. + * + * @default {false} */ + showProxy?: boolean; + /** Sort Object, Set and Map entries by key. + * + * @default {false} */ + sorted?: boolean; + /** Add a trailing comma for multiline collections. + * + * @default {false} */ + trailingComma?: boolean; + /** Evaluate the result of calling getters. + * + * @default {false} */ + getters?: boolean; + /** Show an object's non-enumerable properties. + * + * @default {false} */ + showHidden?: boolean; + /** The maximum length of a string before it is truncated with an + * ellipsis. */ + strAbbreviateSize?: number; + } + + /** Converts the input into a string that has the same format as printed by + * `console.log()`. + * + * ```ts + * const obj = { + * a: 10, + * b: "hello", + * }; + * const objAsString = Deno.inspect(obj); // { a: 10, b: "hello" } + * console.log(obj); // prints same value as objAsString, e.g. { a: 10, b: "hello" } + * ``` + * + * A custom inspect functions can be registered on objects, via the symbol + * `Symbol.for("Deno.customInspect")`, to control and customize the output + * of `inspect()` or when using `console` logging: + * + * ```ts + * class A { + * x = 10; + * y = "hello"; + * [Symbol.for("Deno.customInspect")]() { + * return `x=${this.x}, y=${this.y}`; + * } + * } + * + * const inStringFormat = Deno.inspect(new A()); // "x=10, y=hello" + * console.log(inStringFormat); // prints "x=10, y=hello" + * ``` + * + * A depth can be specified by using the `depth` option: + * + * ```ts + * Deno.inspect({a: {b: {c: {d: 'hello'}}}}, {depth: 2}); // { a: { b: [Object] } } + * ``` + * + * @category I/O + */ + export function inspect(value: unknown, options?: InspectOptions): string; + + /** The name of a privileged feature which needs permission. + * + * @category Permissions + */ + export type PermissionName = "run" | "read" | "write" | "net" | "env" | "sys" | "ffi"; + + /** The current status of the permission: + * + * - `"granted"` - the permission has been granted. + * - `"denied"` - the permission has been explicitly denied. + * - `"prompt"` - the permission has not explicitly granted nor denied. + * + * @category Permissions + */ + export type PermissionState = "granted" | "denied" | "prompt"; + + /** The permission descriptor for the `allow-run` and `deny-run` permissions, which controls + * access to what sub-processes can be executed by Deno. The option `command` + * allows scoping the permission to a specific executable. + * + * **Warning, in practice, `allow-run` is effectively the same as `allow-all` + * in the sense that malicious code could execute any arbitrary code on the + * host.** + * + * @category Permissions */ + export interface RunPermissionDescriptor { + name: "run"; + /** An `allow-run` or `deny-run` permission can be scoped to a specific executable, + * which would be relative to the start-up CWD of the Deno CLI. */ + command?: string | URL; + } + + /** The permission descriptor for the `allow-read` and `deny-read` permissions, which controls + * access to reading resources from the local host. The option `path` allows + * scoping the permission to a specific path (and if the path is a directory + * any sub paths). + * + * Permission granted under `allow-read` only allows runtime code to attempt + * to read, the underlying operating system may apply additional permissions. + * + * @category Permissions */ + export interface ReadPermissionDescriptor { + name: "read"; + /** An `allow-read` or `deny-read` permission can be scoped to a specific path (and if + * the path is a directory, any sub paths). */ + path?: string | URL; + } + + /** The permission descriptor for the `allow-write` and `deny-write` permissions, which + * controls access to writing to resources from the local host. The option + * `path` allow scoping the permission to a specific path (and if the path is + * a directory any sub paths). + * + * Permission granted under `allow-write` only allows runtime code to attempt + * to write, the underlying operating system may apply additional permissions. + * + * @category Permissions */ + export interface WritePermissionDescriptor { + name: "write"; + /** An `allow-write` or `deny-write` permission can be scoped to a specific path (and if + * the path is a directory, any sub paths). */ + path?: string | URL; + } + + /** The permission descriptor for the `allow-net` and `deny-net` permissions, which controls + * access to opening network ports and connecting to remote hosts via the + * network. The option `host` allows scoping the permission for outbound + * connection to a specific host and port. + * + * @category Permissions */ + export interface NetPermissionDescriptor { + name: "net"; + /** Optional host string of the form `"[:]"`. Examples: + * + * "github.com" + * "deno.land:8080" + */ + host?: string; + } + + /** The permission descriptor for the `allow-env` and `deny-env` permissions, which controls + * access to being able to read and write to the process environment variables + * as well as access other information about the environment. The option + * `variable` allows scoping the permission to a specific environment + * variable. + * + * @category Permissions */ + export interface EnvPermissionDescriptor { + name: "env"; + /** Optional environment variable name (e.g. `PATH`). */ + variable?: string; + } + + /** The permission descriptor for the `allow-sys` and `deny-sys` permissions, which controls + * access to sensitive host system information, which malicious code might + * attempt to exploit. The option `kind` allows scoping the permission to a + * specific piece of information. + * + * @category Permissions */ + export interface SysPermissionDescriptor { + name: "sys"; + /** The specific information to scope the permission to. */ + kind?: + | "loadavg" + | "hostname" + | "systemMemoryInfo" + | "networkInterfaces" + | "osRelease" + | "osUptime" + | "uid" + | "gid" + | "username" + | "cpus" + | "homedir" + | "statfs" + | "getPriority" + | "setPriority"; + } + + /** The permission descriptor for the `allow-ffi` and `deny-ffi` permissions, which controls + * access to loading _foreign_ code and interfacing with it via the + * [Foreign Function Interface API](https://docs.deno.com/runtime/manual/runtime/ffi_api) + * available in Deno. The option `path` allows scoping the permission to a + * specific path on the host. + * + * @category Permissions */ + export interface FfiPermissionDescriptor { + name: "ffi"; + /** Optional path on the local host to scope the permission to. */ + path?: string | URL; + } + + /** Permission descriptors which define a permission and can be queried, + * requested, or revoked. + * + * View the specifics of the individual descriptors for more information about + * each permission kind. + * + * @category Permissions + */ + export type PermissionDescriptor = + | RunPermissionDescriptor + | ReadPermissionDescriptor + | WritePermissionDescriptor + | NetPermissionDescriptor + | EnvPermissionDescriptor + | SysPermissionDescriptor + | FfiPermissionDescriptor; + + /** The interface which defines what event types are supported by + * {@linkcode PermissionStatus} instances. + * + * @category Permissions */ + export interface PermissionStatusEventMap { + change: Event; + } + + /** An {@linkcode EventTarget} returned from the {@linkcode Deno.permissions} + * API which can provide updates to any state changes of the permission. + * + * @category Permissions */ + export class PermissionStatus extends EventTarget { + // deno-lint-ignore no-explicit-any + onchange: ((this: PermissionStatus, ev: Event) => any) | null; + readonly state: PermissionState; + /** + * Describes if permission is only granted partially, eg. an access + * might be granted to "/foo" directory, but denied for "/foo/bar". + * In such case this field will be set to `true` when querying for + * read permissions of "/foo" directory. + */ + readonly partial: boolean; + addEventListener( + type: K, + listener: (this: PermissionStatus, ev: PermissionStatusEventMap[K]) => any, + options?: boolean | AddEventListenerOptions, + ): void; + addEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions): void; + removeEventListener( + type: K, + listener: (this: PermissionStatus, ev: PermissionStatusEventMap[K]) => any, + options?: boolean | EventListenerOptions, + ): void; + removeEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | EventListenerOptions): void; + } + + /** + * Deno's permission management API. + * + * The class which provides the interface for the {@linkcode Deno.permissions} + * global instance and is based on the web platform + * [Permissions API](https://developer.mozilla.org/en-US/docs/Web/API/Permissions_API), + * though some proposed parts of the API which are useful in a server side + * runtime context were removed or abandoned in the web platform specification + * which is why it was chosen to locate it in the {@linkcode Deno} namespace + * instead. + * + * By default, if the `stdin`/`stdout` is TTY for the Deno CLI (meaning it can + * send and receive text), then the CLI will prompt the user to grant + * permission when an un-granted permission is requested. This behavior can + * be changed by using the `--no-prompt` command at startup. When prompting + * the CLI will request the narrowest permission possible, potentially making + * it annoying to the user. The permissions APIs allow the code author to + * request a wider set of permissions at one time in order to provide a better + * user experience. + * + * @category Permissions */ + export class Permissions { + /** Resolves to the current status of a permission. + * + * Note, if the permission is already granted, `request()` will not prompt + * the user again, therefore `query()` is only necessary if you are going + * to react differently existing permissions without wanting to modify them + * or prompt the user to modify them. + * + * ```ts + * const status = await Deno.permissions.query({ name: "read", path: "/etc" }); + * console.log(status.state); + * ``` + */ + query(desc: PermissionDescriptor): Promise; + + /** Returns the current status of a permission. + * + * Note, if the permission is already granted, `request()` will not prompt + * the user again, therefore `querySync()` is only necessary if you are going + * to react differently existing permissions without wanting to modify them + * or prompt the user to modify them. + * + * ```ts + * const status = Deno.permissions.querySync({ name: "read", path: "/etc" }); + * console.log(status.state); + * ``` + */ + querySync(desc: PermissionDescriptor): PermissionStatus; + + /** Revokes a permission, and resolves to the state of the permission. + * + * ```ts + * import { assert } from "jsr:@std/assert"; + * + * const status = await Deno.permissions.revoke({ name: "run" }); + * assert(status.state !== "granted") + * ``` + */ + revoke(desc: PermissionDescriptor): Promise; + + /** Revokes a permission, and returns the state of the permission. + * + * ```ts + * import { assert } from "jsr:@std/assert"; + * + * const status = Deno.permissions.revokeSync({ name: "run" }); + * assert(status.state !== "granted") + * ``` + */ + revokeSync(desc: PermissionDescriptor): PermissionStatus; + + /** Requests the permission, and resolves to the state of the permission. + * + * If the permission is already granted, the user will not be prompted to + * grant the permission again. + * + * ```ts + * const status = await Deno.permissions.request({ name: "env" }); + * if (status.state === "granted") { + * console.log("'env' permission is granted."); + * } else { + * console.log("'env' permission is denied."); + * } + * ``` + */ + request(desc: PermissionDescriptor): Promise; + + /** Requests the permission, and returns the state of the permission. + * + * If the permission is already granted, the user will not be prompted to + * grant the permission again. + * + * ```ts + * const status = Deno.permissions.requestSync({ name: "env" }); + * if (status.state === "granted") { + * console.log("'env' permission is granted."); + * } else { + * console.log("'env' permission is denied."); + * } + * ``` + */ + requestSync(desc: PermissionDescriptor): PermissionStatus; + } + + /** Deno's permission management API. + * + * It is a singleton instance of the {@linkcode Permissions} object and is + * based on the web platform + * [Permissions API](https://developer.mozilla.org/en-US/docs/Web/API/Permissions_API), + * though some proposed parts of the API which are useful in a server side + * runtime context were removed or abandoned in the web platform specification + * which is why it was chosen to locate it in the {@linkcode Deno} namespace + * instead. + * + * By default, if the `stdin`/`stdout` is TTY for the Deno CLI (meaning it can + * send and receive text), then the CLI will prompt the user to grant + * permission when an un-granted permission is requested. This behavior can + * be changed by using the `--no-prompt` command at startup. When prompting + * the CLI will request the narrowest permission possible, potentially making + * it annoying to the user. The permissions APIs allow the code author to + * request a wider set of permissions at one time in order to provide a better + * user experience. + * + * Requesting already granted permissions will not prompt the user and will + * return that the permission was granted. + * + * ### Querying + * + * ```ts + * const status = await Deno.permissions.query({ name: "read", path: "/etc" }); + * console.log(status.state); + * ``` + * + * ```ts + * const status = Deno.permissions.querySync({ name: "read", path: "/etc" }); + * console.log(status.state); + * ``` + * + * ### Revoking + * + * ```ts + * import { assert } from "jsr:@std/assert"; + * + * const status = await Deno.permissions.revoke({ name: "run" }); + * assert(status.state !== "granted") + * ``` + * + * ```ts + * import { assert } from "jsr:@std/assert"; + * + * const status = Deno.permissions.revokeSync({ name: "run" }); + * assert(status.state !== "granted") + * ``` + * + * ### Requesting + * + * ```ts + * const status = await Deno.permissions.request({ name: "env" }); + * if (status.state === "granted") { + * console.log("'env' permission is granted."); + * } else { + * console.log("'env' permission is denied."); + * } + * ``` + * + * ```ts + * const status = Deno.permissions.requestSync({ name: "env" }); + * if (status.state === "granted") { + * console.log("'env' permission is granted."); + * } else { + * console.log("'env' permission is denied."); + * } + * ``` + * + * @category Permissions + */ + export const permissions: Permissions; + + /** Information related to the build of the current Deno runtime. + * + * Users are discouraged from code branching based on this information, as + * assumptions about what is available in what build environment might change + * over time. Developers should specifically sniff out the features they + * intend to use. + * + * The intended use for the information is for logging and debugging purposes. + * + * @category Runtime + */ + export const build: { + /** The [LLVM](https://llvm.org/) target triple, which is the combination + * of `${arch}-${vendor}-${os}` and represent the specific build target that + * the current runtime was built for. */ + target: string; + /** Instruction set architecture that the Deno CLI was built for. */ + arch: "x86_64" | "aarch64"; + /** The operating system that the Deno CLI was built for. `"darwin"` is + * also known as OSX or MacOS. */ + os: "darwin" | "linux" | "android" | "windows" | "freebsd" | "netbsd" | "aix" | "solaris" | "illumos"; + /** The computer vendor that the Deno CLI was built for. */ + vendor: string; + /** Optional environment flags that were set for this build of Deno CLI. */ + env?: string; + }; + + /** Version information related to the current Deno CLI runtime environment. + * + * Users are discouraged from code branching based on this information, as + * assumptions about what is available in what build environment might change + * over time. Developers should specifically sniff out the features they + * intend to use. + * + * The intended use for the information is for logging and debugging purposes. + * + * @category Runtime + */ + export const version: { + /** Deno CLI's version. For example: `"1.26.0"`. */ + deno: string; + /** The V8 version used by Deno. For example: `"10.7.100.0"`. + * + * V8 is the underlying JavaScript runtime platform that Deno is built on + * top of. */ + v8: string; + /** The TypeScript version used by Deno. For example: `"4.8.3"`. + * + * A version of the TypeScript type checker and language server is built-in + * to the Deno CLI. */ + typescript: string; + }; + + /** Returns the script arguments to the program. + * + * Give the following command line invocation of Deno: + * + * ```sh + * deno run --allow-read https://examples.deno.land/command-line-arguments.ts Sushi + * ``` + * + * Then `Deno.args` will contain: + * + * ```ts + * [ "Sushi" ] + * ``` + * + * If you are looking for a structured way to parse arguments, there is + * [`parseArgs()`](https://jsr.io/@std/cli/doc/parse-args/~/parseArgs) from + * the Deno Standard Library. + * + * @category Runtime + */ + export const args: string[]; + + /** The URL of the entrypoint module entered from the command-line. It + * requires read permission to the CWD. + * + * Also see {@linkcode ImportMeta} for other related information. + * + * @tags allow-read + * @category Runtime + */ + export const mainModule: string; + + /** Options that can be used with {@linkcode symlink} and + * {@linkcode symlinkSync}. + * + * @category File System */ + export interface SymlinkOptions { + /** Specify the symbolic link type as file, directory or NTFS junction. This + * option only applies to Windows and is ignored on other operating systems. */ + type: "file" | "dir" | "junction"; + } + + /** + * Creates `newpath` as a symbolic link to `oldpath`. + * + * The `options.type` parameter can be set to `"file"`, `"dir"` or `"junction"`. + * This argument is only available on Windows and ignored on other platforms. + * + * ```ts + * await Deno.symlink("old/name", "new/name"); + * ``` + * + * Requires full `allow-read` and `allow-write` permissions. + * + * @tags allow-read, allow-write + * @category File System + */ + export function symlink(oldpath: string | URL, newpath: string | URL, options?: SymlinkOptions): Promise; + + /** + * Creates `newpath` as a symbolic link to `oldpath`. + * + * The `options.type` parameter can be set to `"file"`, `"dir"` or `"junction"`. + * This argument is only available on Windows and ignored on other platforms. + * + * ```ts + * Deno.symlinkSync("old/name", "new/name"); + * ``` + * + * Requires full `allow-read` and `allow-write` permissions. + * + * @tags allow-read, allow-write + * @category File System + */ + export function symlinkSync(oldpath: string | URL, newpath: string | URL, options?: SymlinkOptions): void; + + /** + * Synchronously changes the access (`atime`) and modification (`mtime`) times + * of a file system object referenced by `path`. Given times are either in + * seconds (UNIX epoch time) or as `Date` objects. + * + * ```ts + * Deno.utimeSync("myfile.txt", 1556495550, new Date()); + * ``` + * + * Requires `allow-write` permission. + * + * @tags allow-write + * @category File System + */ + export function utimeSync(path: string | URL, atime: number | Date, mtime: number | Date): void; + + /** + * Changes the access (`atime`) and modification (`mtime`) times of a file + * system object referenced by `path`. Given times are either in seconds + * (UNIX epoch time) or as `Date` objects. + * + * ```ts + * await Deno.utime("myfile.txt", 1556495550, new Date()); + * ``` + * + * Requires `allow-write` permission. + * + * @tags allow-write + * @category File System + */ + export function utime(path: string | URL, atime: number | Date, mtime: number | Date): Promise; + + /** Retrieve the process umask. If `mask` is provided, sets the process umask. + * This call always returns what the umask was before the call. + * + * ```ts + * console.log(Deno.umask()); // e.g. 18 (0o022) + * const prevUmaskValue = Deno.umask(0o077); // e.g. 18 (0o022) + * console.log(Deno.umask()); // e.g. 63 (0o077) + * ``` + * + * This API is under consideration to determine if permissions are required to + * call it. + * + * *Note*: This API is not implemented on Windows + * + * @category File System + */ + export function umask(mask?: number): number; + + /** The object that is returned from a {@linkcode Deno.upgradeWebSocket} + * request. + * + * @category WebSockets */ + export interface WebSocketUpgrade { + /** The response object that represents the HTTP response to the client, + * which should be used to the {@linkcode RequestEvent} `.respondWith()` for + * the upgrade to be successful. */ + response: Response; + /** The {@linkcode WebSocket} interface to communicate to the client via a + * web socket. */ + socket: WebSocket; + } + + /** Options which can be set when performing a + * {@linkcode Deno.upgradeWebSocket} upgrade of a {@linkcode Request} + * + * @category WebSockets */ + export interface UpgradeWebSocketOptions { + /** Sets the `.protocol` property on the client side web socket to the + * value provided here, which should be one of the strings specified in the + * `protocols` parameter when requesting the web socket. This is intended + * for clients and servers to specify sub-protocols to use to communicate to + * each other. */ + protocol?: string; + /** If the client does not respond to this frame with a + * `pong` within the timeout specified, the connection is deemed + * unhealthy and is closed. The `close` and `error` event will be emitted. + * + * The unit is seconds, with a default of 30. + * Set to `0` to disable timeouts. */ + idleTimeout?: number; + } + + /** + * Upgrade an incoming HTTP request to a WebSocket. + * + * Given a {@linkcode Request}, returns a pair of {@linkcode WebSocket} and + * {@linkcode Response} instances. The original request must be responded to + * with the returned response for the websocket upgrade to be successful. + * + * ```ts + * Deno.serve((req) => { + * if (req.headers.get("upgrade") !== "websocket") { + * return new Response(null, { status: 501 }); + * } + * const { socket, response } = Deno.upgradeWebSocket(req); + * socket.addEventListener("open", () => { + * console.log("a client connected!"); + * }); + * socket.addEventListener("message", (event) => { + * if (event.data === "ping") { + * socket.send("pong"); + * } + * }); + * return response; + * }); + * ``` + * + * If the request body is disturbed (read from) before the upgrade is + * completed, upgrading fails. + * + * This operation does not yet consume the request or open the websocket. This + * only happens once the returned response has been passed to `respondWith()`. + * + * @category WebSockets + */ + export function upgradeWebSocket(request: Request, options?: UpgradeWebSocketOptions): WebSocketUpgrade; + + /** Send a signal to process under given `pid`. The value and meaning of the + * `signal` to the process is operating system and process dependant. + * {@linkcode Signal} provides the most common signals. Default signal + * is `"SIGTERM"`. + * + * The term `kill` is adopted from the UNIX-like command line command `kill` + * which also signals processes. + * + * If `pid` is negative, the signal will be sent to the process group + * identified by `pid`. An error will be thrown if a negative `pid` is used on + * Windows. + * + * ```ts + * const command = new Deno.Command("sleep", { args: ["10000"] }); + * const child = command.spawn(); + * + * Deno.kill(child.pid, "SIGINT"); + * ``` + * + * Requires `allow-run` permission. + * + * @tags allow-run + * @category Subprocess + */ + export function kill(pid: number, signo?: Signal): void; + + /** The type of the resource record to resolve via DNS using + * {@linkcode Deno.resolveDns}. + * + * Only the listed types are supported currently. + * + * @category Network + */ + export type RecordType = "A" | "AAAA" | "ANAME" | "CAA" | "CNAME" | "MX" | "NAPTR" | "NS" | "PTR" | "SOA" | "SRV" | "TXT"; + + /** + * Options which can be set when using {@linkcode Deno.resolveDns}. + * + * @category Network */ + export interface ResolveDnsOptions { + /** The name server to be used for lookups. + * + * If not specified, defaults to the system configuration. For example + * `/etc/resolv.conf` on Unix-like systems. */ + nameServer?: { + /** The IP address of the name server. */ + ipAddr: string; + /** The port number the query will be sent to. + * + * @default {53} */ + port?: number; + }; + /** + * An abort signal to allow cancellation of the DNS resolution operation. + * If the signal becomes aborted the resolveDns operation will be stopped + * and the promise returned will be rejected with an AbortError. + */ + signal?: AbortSignal; + } + + /** If {@linkcode Deno.resolveDns} is called with `"CAA"` record type + * specified, it will resolve with an array of objects with this interface. + * + * @category Network + */ + export interface CaaRecord { + /** If `true`, indicates that the corresponding property tag **must** be + * understood if the semantics of the CAA record are to be correctly + * interpreted by an issuer. + * + * Issuers **must not** issue certificates for a domain if the relevant CAA + * Resource Record set contains unknown property tags that have `critical` + * set. */ + critical: boolean; + /** An string that represents the identifier of the property represented by + * the record. */ + tag: string; + /** The value associated with the tag. */ + value: string; + } + + /** If {@linkcode Deno.resolveDns} is called with `"MX"` record type + * specified, it will return an array of objects with this interface. + * + * @category Network */ + export interface MxRecord { + /** A priority value, which is a relative value compared to the other + * preferences of MX records for the domain. */ + preference: number; + /** The server that mail should be delivered to. */ + exchange: string; + } + + /** If {@linkcode Deno.resolveDns} is called with `"NAPTR"` record type + * specified, it will return an array of objects with this interface. + * + * @category Network */ + export interface NaptrRecord { + order: number; + preference: number; + flags: string; + services: string; + regexp: string; + replacement: string; + } + + /** If {@linkcode Deno.resolveDns} is called with `"SOA"` record type + * specified, it will return an array of objects with this interface. + * + * @category Network */ + export interface SoaRecord { + mname: string; + rname: string; + serial: number; + refresh: number; + retry: number; + expire: number; + minimum: number; + } + + /** If {@linkcode Deno.resolveDns} is called with `"SRV"` record type + * specified, it will return an array of objects with this interface. + * + * @category Network + */ + export interface SrvRecord { + priority: number; + weight: number; + port: number; + target: string; + } + + /** + * Performs DNS resolution against the given query, returning resolved + * records. + * + * Fails in the cases such as: + * + * - the query is in invalid format. + * - the options have an invalid parameter. For example `nameServer.port` is + * beyond the range of 16-bit unsigned integer. + * - the request timed out. + * + * ```ts + * const a = await Deno.resolveDns("example.com", "A"); + * + * const aaaa = await Deno.resolveDns("example.com", "AAAA", { + * nameServer: { ipAddr: "8.8.8.8", port: 53 }, + * }); + * ``` + * + * Requires `allow-net` permission. + * + * @tags allow-net + * @category Network + */ + export function resolveDns( + query: string, + recordType: "A" | "AAAA" | "ANAME" | "CNAME" | "NS" | "PTR", + options?: ResolveDnsOptions, + ): Promise; + + /** + * Performs DNS resolution against the given query, returning resolved + * records. + * + * Fails in the cases such as: + * + * - the query is in invalid format. + * - the options have an invalid parameter. For example `nameServer.port` is + * beyond the range of 16-bit unsigned integer. + * - the request timed out. + * + * ```ts + * const a = await Deno.resolveDns("example.com", "A"); + * + * const aaaa = await Deno.resolveDns("example.com", "AAAA", { + * nameServer: { ipAddr: "8.8.8.8", port: 53 }, + * }); + * ``` + * + * Requires `allow-net` permission. + * + * @tags allow-net + * @category Network + */ + export function resolveDns(query: string, recordType: "CAA", options?: ResolveDnsOptions): Promise; + + /** + * Performs DNS resolution against the given query, returning resolved + * records. + * + * Fails in the cases such as: + * + * - the query is in invalid format. + * - the options have an invalid parameter. For example `nameServer.port` is + * beyond the range of 16-bit unsigned integer. + * - the request timed out. + * + * ```ts + * const a = await Deno.resolveDns("example.com", "A"); + * + * const aaaa = await Deno.resolveDns("example.com", "AAAA", { + * nameServer: { ipAddr: "8.8.8.8", port: 53 }, + * }); + * ``` + * + * Requires `allow-net` permission. + * + * @tags allow-net + * @category Network + */ + export function resolveDns(query: string, recordType: "MX", options?: ResolveDnsOptions): Promise; + + /** + * Performs DNS resolution against the given query, returning resolved + * records. + * + * Fails in the cases such as: + * + * - the query is in invalid format. + * - the options have an invalid parameter. For example `nameServer.port` is + * beyond the range of 16-bit unsigned integer. + * - the request timed out. + * + * ```ts + * const a = await Deno.resolveDns("example.com", "A"); + * + * const aaaa = await Deno.resolveDns("example.com", "AAAA", { + * nameServer: { ipAddr: "8.8.8.8", port: 53 }, + * }); + * ``` + * + * Requires `allow-net` permission. + * + * @tags allow-net + * @category Network + */ + export function resolveDns(query: string, recordType: "NAPTR", options?: ResolveDnsOptions): Promise; + + /** + * Performs DNS resolution against the given query, returning resolved + * records. + * + * Fails in the cases such as: + * + * - the query is in invalid format. + * - the options have an invalid parameter. For example `nameServer.port` is + * beyond the range of 16-bit unsigned integer. + * - the request timed out. + * + * ```ts + * const a = await Deno.resolveDns("example.com", "A"); + * + * const aaaa = await Deno.resolveDns("example.com", "AAAA", { + * nameServer: { ipAddr: "8.8.8.8", port: 53 }, + * }); + * ``` + * + * Requires `allow-net` permission. + * + * @tags allow-net + * @category Network + */ + export function resolveDns(query: string, recordType: "SOA", options?: ResolveDnsOptions): Promise; + + /** + * Performs DNS resolution against the given query, returning resolved + * records. + * + * Fails in the cases such as: + * + * - the query is in invalid format. + * - the options have an invalid parameter. For example `nameServer.port` is + * beyond the range of 16-bit unsigned integer. + * - the request timed out. + * + * ```ts + * const a = await Deno.resolveDns("example.com", "A"); + * + * const aaaa = await Deno.resolveDns("example.com", "AAAA", { + * nameServer: { ipAddr: "8.8.8.8", port: 53 }, + * }); + * ``` + * + * Requires `allow-net` permission. + * + * @tags allow-net + * @category Network + */ + export function resolveDns(query: string, recordType: "SRV", options?: ResolveDnsOptions): Promise; + + /** + * Performs DNS resolution against the given query, returning resolved + * records. + * + * Fails in the cases such as: + * + * - the query is in invalid format. + * - the options have an invalid parameter. For example `nameServer.port` is + * beyond the range of 16-bit unsigned integer. + * - the request timed out. + * + * ```ts + * const a = await Deno.resolveDns("example.com", "A"); + * + * const aaaa = await Deno.resolveDns("example.com", "AAAA", { + * nameServer: { ipAddr: "8.8.8.8", port: 53 }, + * }); + * ``` + * + * Requires `allow-net` permission. + * + * @tags allow-net + * @category Network + */ + export function resolveDns(query: string, recordType: "TXT", options?: ResolveDnsOptions): Promise; + + /** + * Performs DNS resolution against the given query, returning resolved + * records. + * + * Fails in the cases such as: + * + * - the query is in invalid format. + * - the options have an invalid parameter. For example `nameServer.port` is + * beyond the range of 16-bit unsigned integer. + * - the request timed out. + * + * ```ts + * const a = await Deno.resolveDns("example.com", "A"); + * + * const aaaa = await Deno.resolveDns("example.com", "AAAA", { + * nameServer: { ipAddr: "8.8.8.8", port: 53 }, + * }); + * ``` + * + * Requires `allow-net` permission. + * + * @tags allow-net + * @category Network + */ + export function resolveDns( + query: string, + recordType: RecordType, + options?: ResolveDnsOptions, + ): Promise; + + /** + * Make the timer of the given `id` block the event loop from finishing. + * + * @category Runtime + */ + export function refTimer(id: number): void; + + /** + * Make the timer of the given `id` not block the event loop from finishing. + * + * @category Runtime + */ + export function unrefTimer(id: number): void; + + /** + * Returns the user id of the process on POSIX platforms. Returns null on Windows. + * + * ```ts + * console.log(Deno.uid()); + * ``` + * + * Requires `allow-sys` permission. + * + * @tags allow-sys + * @category Runtime + */ + export function uid(): number | null; + + /** + * Returns the group id of the process on POSIX platforms. Returns null on windows. + * + * ```ts + * console.log(Deno.gid()); + * ``` + * + * Requires `allow-sys` permission. + * + * @tags allow-sys + * @category Runtime + */ + export function gid(): number | null; + + /** Additional information for an HTTP request and its connection. + * + * @category HTTP Server + */ + export interface ServeHandlerInfo { + /** The remote address of the connection. */ + remoteAddr: Addr; + /** The completion promise */ + completed: Promise; + } + + /** A handler for HTTP requests. Consumes a request and returns a response. + * + * If a handler throws, the server calling the handler will assume the impact + * of the error is isolated to the individual request. It will catch the error + * and if necessary will close the underlying connection. + * + * @category HTTP Server + */ + export type ServeHandler = ( + request: Request, + info: ServeHandlerInfo, + ) => Response | Promise; + + /** Interface that module run with `deno serve` subcommand must conform to. + * + * To ensure your code is type-checked properly, make sure to add `satisfies Deno.ServeDefaultExport` + * to the `export default { ... }` like so: + * + * ```ts + * export default { + * fetch(req) { + * return new Response("Hello world"); + * } + * } satisfies Deno.ServeDefaultExport; + * ``` + * + * @category HTTP Server + */ + export interface ServeDefaultExport { + /** A handler for HTTP requests. Consumes a request and returns a response. + * + * If a handler throws, the server calling the handler will assume the impact + * of the error is isolated to the individual request. It will catch the error + * and if necessary will close the underlying connection. + * + * @category HTTP Server + */ + fetch: ServeHandler; + } + + /** Options which can be set when calling {@linkcode Deno.serve}. + * + * @category HTTP Server + */ + export interface ServeOptions { + /** An {@linkcode AbortSignal} to close the server and all connections. */ + signal?: AbortSignal; + + /** The handler to invoke when route handlers throw an error. */ + onError?: (error: unknown) => Response | Promise; + + /** The callback which is called when the server starts listening. */ + onListen?: (localAddr: Addr) => void; + } + + /** + * Options that can be passed to `Deno.serve` to create a server listening on + * a TCP port. + * + * @category HTTP Server + */ + export interface ServeTcpOptions extends ServeOptions { + /** The transport to use. */ + transport?: "tcp"; + + /** The port to listen on. + * + * Set to `0` to listen on any available port. + * + * @default {8000} */ + port?: number; + + /** A literal IP address or host name that can be resolved to an IP address. + * + * __Note about `0.0.0.0`__ While listening `0.0.0.0` works on all platforms, + * the browsers on Windows don't work with the address `0.0.0.0`. + * You should show the message like `server running on localhost:8080` instead of + * `server running on 0.0.0.0:8080` if your program supports Windows. + * + * @default {"0.0.0.0"} */ + hostname?: string; + + /** Sets `SO_REUSEPORT` on POSIX systems. */ + reusePort?: boolean; + } + + /** + * Options that can be passed to `Deno.serve` to create a server listening on + * a Unix domain socket. + * + * @category HTTP Server + */ + export interface ServeUnixOptions extends ServeOptions { + /** The transport to use. */ + transport?: "unix"; + + /** The unix domain socket path to listen on. */ + path: string; + } + + /** + * @category HTTP Server + */ + export interface ServeInit { + /** The handler to invoke to process each incoming request. */ + handler: ServeHandler; + } + + /** An instance of the server created using `Deno.serve()` API. + * + * @category HTTP Server + */ + export interface HttpServer extends AsyncDisposable { + /** A promise that resolves once server finishes - eg. when aborted using + * the signal passed to {@linkcode ServeOptions.signal}. + */ + finished: Promise; + + /** The local address this server is listening on. */ + addr: Addr; + + /** + * Make the server block the event loop from finishing. + * + * Note: the server blocks the event loop from finishing by default. + * This method is only meaningful after `.unref()` is called. + */ + ref(): void; + + /** Make the server not block the event loop from finishing. */ + unref(): void; + + /** Gracefully close the server. No more new connections will be accepted, + * while pending requests will be allowed to finish. + */ + shutdown(): Promise; + } + + /** Serves HTTP requests with the given handler. + * + * The below example serves with the port `8000` on hostname `"127.0.0.1"`. + * + * ```ts + * Deno.serve((_req) => new Response("Hello, world")); + * ``` + * + * @category HTTP Server + */ + export function serve(handler: ServeHandler): HttpServer; + /** Serves HTTP requests with the given option bag and handler. + * + * You can specify the socket path with `path` option. + * + * ```ts + * Deno.serve( + * { path: "path/to/socket" }, + * (_req) => new Response("Hello, world") + * ); + * ``` + * + * You can stop the server with an {@linkcode AbortSignal}. The abort signal + * needs to be passed as the `signal` option in the options bag. The server + * aborts when the abort signal is aborted. To wait for the server to close, + * await the promise returned from the `Deno.serve` API. + * + * ```ts + * const ac = new AbortController(); + * + * const server = Deno.serve( + * { signal: ac.signal, path: "path/to/socket" }, + * (_req) => new Response("Hello, world") + * ); + * server.finished.then(() => console.log("Server closed")); + * + * console.log("Closing server..."); + * ac.abort(); + * ``` + * + * By default `Deno.serve` prints the message + * `Listening on path/to/socket` on listening. If you like to + * change this behavior, you can specify a custom `onListen` callback. + * + * ```ts + * Deno.serve({ + * onListen({ path }) { + * console.log(`Server started at ${path}`); + * // ... more info specific to your server .. + * }, + * path: "path/to/socket", + * }, (_req) => new Response("Hello, world")); + * ``` + * + * @category HTTP Server + */ + export function serve(options: ServeUnixOptions, handler: ServeHandler): HttpServer; + /** Serves HTTP requests with the given option bag and handler. + * + * You can specify an object with a port and hostname option, which is the + * address to listen on. The default is port `8000` on hostname `"0.0.0.0"`. + * + * You can change the address to listen on using the `hostname` and `port` + * options. The below example serves on port `3000` and hostname `"127.0.0.1"`. + * + * ```ts + * Deno.serve( + * { port: 3000, hostname: "127.0.0.1" }, + * (_req) => new Response("Hello, world") + * ); + * ``` + * + * You can stop the server with an {@linkcode AbortSignal}. The abort signal + * needs to be passed as the `signal` option in the options bag. The server + * aborts when the abort signal is aborted. To wait for the server to close, + * await the promise returned from the `Deno.serve` API. + * + * ```ts + * const ac = new AbortController(); + * + * const server = Deno.serve( + * { signal: ac.signal }, + * (_req) => new Response("Hello, world") + * ); + * server.finished.then(() => console.log("Server closed")); + * + * console.log("Closing server..."); + * ac.abort(); + * ``` + * + * By default `Deno.serve` prints the message + * `Listening on http://:/` on listening. If you like to + * change this behavior, you can specify a custom `onListen` callback. + * + * ```ts + * Deno.serve({ + * onListen({ port, hostname }) { + * console.log(`Server started at http://${hostname}:${port}`); + * // ... more info specific to your server .. + * }, + * }, (_req) => new Response("Hello, world")); + * ``` + * + * To enable TLS you must specify the `key` and `cert` options. + * + * ```ts + * const cert = "-----BEGIN CERTIFICATE-----\n...\n-----END CERTIFICATE-----\n"; + * const key = "-----BEGIN PRIVATE KEY-----\n...\n-----END PRIVATE KEY-----\n"; + * Deno.serve({ cert, key }, (_req) => new Response("Hello, world")); + * ``` + * + * @category HTTP Server + */ + export function serve( + options: ServeTcpOptions | (ServeTcpOptions & TlsCertifiedKeyPem), + handler: ServeHandler, + ): HttpServer; + /** Serves HTTP requests with the given option bag. + * + * You can specify an object with the path option, which is the + * unix domain socket to listen on. + * + * ```ts + * const ac = new AbortController(); + * + * const server = Deno.serve({ + * path: "path/to/socket", + * handler: (_req) => new Response("Hello, world"), + * signal: ac.signal, + * onListen({ path }) { + * console.log(`Server started at ${path}`); + * }, + * }); + * server.finished.then(() => console.log("Server closed")); + * + * console.log("Closing server..."); + * ac.abort(); + * ``` + * + * @category HTTP Server + */ + export function serve(options: ServeUnixOptions & ServeInit): HttpServer; + /** Serves HTTP requests with the given option bag. + * + * You can specify an object with a port and hostname option, which is the + * address to listen on. The default is port `8000` on hostname `"0.0.0.0"`. + * + * ```ts + * const ac = new AbortController(); + * + * const server = Deno.serve({ + * port: 3000, + * hostname: "127.0.0.1", + * handler: (_req) => new Response("Hello, world"), + * signal: ac.signal, + * onListen({ port, hostname }) { + * console.log(`Server started at http://${hostname}:${port}`); + * }, + * }); + * server.finished.then(() => console.log("Server closed")); + * + * console.log("Closing server..."); + * ac.abort(); + * ``` + * + * @category HTTP Server + */ + export function serve( + options: (ServeTcpOptions | (ServeTcpOptions & TlsCertifiedKeyPem)) & ServeInit, + ): HttpServer; + + /** All plain number types for interfacing with foreign functions. + * + * @category FFI + */ + export type NativeNumberType = "u8" | "i8" | "u16" | "i16" | "u32" | "i32" | "f32" | "f64"; + + /** All BigInt number types for interfacing with foreign functions. + * + * @category FFI + */ + export type NativeBigIntType = "u64" | "i64" | "usize" | "isize"; + + /** The native boolean type for interfacing to foreign functions. + * + * @category FFI + */ + export type NativeBooleanType = "bool"; + + /** The native pointer type for interfacing to foreign functions. + * + * @category FFI + */ + export type NativePointerType = "pointer"; + + /** The native buffer type for interfacing to foreign functions. + * + * @category FFI + */ + export type NativeBufferType = "buffer"; + + /** The native function type for interfacing with foreign functions. + * + * @category FFI + */ + export type NativeFunctionType = "function"; + + /** The native void type for interfacing with foreign functions. + * + * @category FFI + */ + export type NativeVoidType = "void"; + + /** The native struct type for interfacing with foreign functions. + * + * @category FFI + */ + export interface NativeStructType { + readonly struct: readonly NativeType[]; + } + + /** + * @category FFI + */ + export const brand: unique symbol; + + /** + * @category FFI + */ + export type NativeU8Enum = "u8" & { [brand]: T }; + /** + * @category FFI + */ + export type NativeI8Enum = "i8" & { [brand]: T }; + /** + * @category FFI + */ + export type NativeU16Enum = "u16" & { [brand]: T }; + /** + * @category FFI + */ + export type NativeI16Enum = "i16" & { [brand]: T }; + /** + * @category FFI + */ + export type NativeU32Enum = "u32" & { [brand]: T }; + /** + * @category FFI + */ + export type NativeI32Enum = "i32" & { [brand]: T }; + /** + * @category FFI + */ + export type NativeTypedPointer = "pointer" & { + [brand]: T; + }; + /** + * @category FFI + */ + export type NativeTypedFunction = "function" & { + [brand]: T; + }; + + /** All supported types for interfacing with foreign functions. + * + * @category FFI + */ + export type NativeType = + | NativeNumberType + | NativeBigIntType + | NativeBooleanType + | NativePointerType + | NativeBufferType + | NativeFunctionType + | NativeStructType; + + /** @category FFI + */ + export type NativeResultType = NativeType | NativeVoidType; + + /** Type conversion for foreign symbol parameters and unsafe callback return + * types. + * + * @category FFI + */ + export type ToNativeType = T extends NativeStructType + ? BufferSource + : T extends NativeNumberType + ? T extends NativeU8Enum + ? U + : T extends NativeI8Enum + ? U + : T extends NativeU16Enum + ? U + : T extends NativeI16Enum + ? U + : T extends NativeU32Enum + ? U + : T extends NativeI32Enum + ? U + : number + : T extends NativeBigIntType + ? bigint + : T extends NativeBooleanType + ? boolean + : T extends NativePointerType + ? T extends NativeTypedPointer + ? U | null + : PointerValue + : T extends NativeFunctionType + ? T extends NativeTypedFunction + ? PointerValue | null + : PointerValue + : T extends NativeBufferType + ? BufferSource | null + : never; + + /** Type conversion for unsafe callback return types. + * + * @category FFI + */ + export type ToNativeResultType = T extends NativeStructType + ? BufferSource + : T extends NativeNumberType + ? T extends NativeU8Enum + ? U + : T extends NativeI8Enum + ? U + : T extends NativeU16Enum + ? U + : T extends NativeI16Enum + ? U + : T extends NativeU32Enum + ? U + : T extends NativeI32Enum + ? U + : number + : T extends NativeBigIntType + ? bigint + : T extends NativeBooleanType + ? boolean + : T extends NativePointerType + ? T extends NativeTypedPointer + ? U | null + : PointerValue + : T extends NativeFunctionType + ? T extends NativeTypedFunction + ? PointerObject | null + : PointerValue + : T extends NativeBufferType + ? BufferSource | null + : T extends NativeVoidType + ? void + : never; + + /** A utility type for conversion of parameter types of foreign functions. + * + * @category FFI + */ + export type ToNativeParameterTypes = + // + [T[number][]] extends [T] + ? ToNativeType[] + : [readonly T[number][]] extends [T] + ? readonly ToNativeType[] + : T extends readonly [...NativeType[]] + ? { + [K in keyof T]: ToNativeType; + } + : never; + + /** Type conversion for foreign symbol return types and unsafe callback + * parameters. + * + * @category FFI + */ + export type FromNativeType = T extends NativeStructType + ? Uint8Array + : T extends NativeNumberType + ? T extends NativeU8Enum + ? U + : T extends NativeI8Enum + ? U + : T extends NativeU16Enum + ? U + : T extends NativeI16Enum + ? U + : T extends NativeU32Enum + ? U + : T extends NativeI32Enum + ? U + : number + : T extends NativeBigIntType + ? bigint + : T extends NativeBooleanType + ? boolean + : T extends NativePointerType + ? T extends NativeTypedPointer + ? U | null + : PointerValue + : T extends NativeBufferType + ? PointerValue + : T extends NativeFunctionType + ? T extends NativeTypedFunction + ? PointerObject | null + : PointerValue + : never; + + /** Type conversion for foreign symbol return types. + * + * @category FFI + */ + export type FromNativeResultType = T extends NativeStructType + ? Uint8Array + : T extends NativeNumberType + ? T extends NativeU8Enum + ? U + : T extends NativeI8Enum + ? U + : T extends NativeU16Enum + ? U + : T extends NativeI16Enum + ? U + : T extends NativeU32Enum + ? U + : T extends NativeI32Enum + ? U + : number + : T extends NativeBigIntType + ? bigint + : T extends NativeBooleanType + ? boolean + : T extends NativePointerType + ? T extends NativeTypedPointer + ? U | null + : PointerValue + : T extends NativeBufferType + ? PointerValue + : T extends NativeFunctionType + ? T extends NativeTypedFunction + ? PointerObject | null + : PointerValue + : T extends NativeVoidType + ? void + : never; + + /** @category FFI + */ + export type FromNativeParameterTypes = + // + [T[number][]] extends [T] + ? FromNativeType[] + : [readonly T[number][]] extends [T] + ? readonly FromNativeType[] + : T extends readonly [...NativeType[]] + ? { + [K in keyof T]: FromNativeType; + } + : never; + + /** The interface for a foreign function as defined by its parameter and result + * types. + * + * @category FFI + */ + export interface ForeignFunction< + Parameters extends readonly NativeType[] = readonly NativeType[], + Result extends NativeResultType = NativeResultType, + NonBlocking extends boolean = boolean, + > { + /** Name of the symbol. + * + * Defaults to the key name in symbols object. */ + name?: string; + /** The parameters of the foreign function. */ + parameters: Parameters; + /** The result (return value) of the foreign function. */ + result: Result; + /** When `true`, function calls will run on a dedicated blocking thread and + * will return a `Promise` resolving to the `result`. */ + nonblocking?: NonBlocking; + /** When `true`, dlopen will not fail if the symbol is not found. + * Instead, the symbol will be set to `null`. + * + * @default {false} */ + optional?: boolean; + } + + /** @category FFI + */ + export interface ForeignStatic { + /** Name of the symbol, defaults to the key name in symbols object. */ + name?: string; + /** The type of the foreign static value. */ + type: Type; + /** When `true`, dlopen will not fail if the symbol is not found. + * Instead, the symbol will be set to `null`. + * + * @default {false} */ + optional?: boolean; + } + + /** A foreign library interface descriptor. + * + * @category FFI + */ + export interface ForeignLibraryInterface { + [name: string]: ForeignFunction | ForeignStatic; + } + + /** A utility type that infers a foreign symbol. + * + * @category FFI + */ + export type StaticForeignSymbol = T extends ForeignFunction + ? FromForeignFunction + : T extends ForeignStatic + ? FromNativeType + : never; + + /** @category FFI + */ + export type FromForeignFunction = T["parameters"] extends readonly [] + ? () => StaticForeignSymbolReturnType + : (...args: ToNativeParameterTypes) => StaticForeignSymbolReturnType; + + /** @category FFI + */ + export type StaticForeignSymbolReturnType = ConditionalAsync< + T["nonblocking"], + FromNativeResultType + >; + + /** @category FFI + */ + export type ConditionalAsync = IsAsync extends true ? Promise : T; + + /** A utility type that infers a foreign library interface. + * + * @category FFI + */ + export type StaticForeignLibraryInterface = { + [K in keyof T]: T[K]["optional"] extends true ? StaticForeignSymbol | null : StaticForeignSymbol; + }; + + /** A non-null pointer, represented as an object + * at runtime. The object's prototype is `null` + * and cannot be changed. The object cannot be + * assigned to either and is thus entirely read-only. + * + * To interact with memory through a pointer use the + * {@linkcode UnsafePointerView} class. To create a + * pointer from an address or the get the address of + * a pointer use the static methods of the + * {@linkcode UnsafePointer} class. + * + * @category FFI + */ + export interface PointerObject { + [brand]: T; + } + + /** Pointers are represented either with a {@linkcode PointerObject} + * object or a `null` if the pointer is null. + * + * @category FFI + */ + export type PointerValue = null | PointerObject; + + /** A collection of static functions for interacting with pointer objects. + * + * @category FFI + */ + export class UnsafePointer { + /** Create a pointer from a numeric value. This one is really dangerous! */ + static create(value: bigint): PointerValue; + /** Returns `true` if the two pointers point to the same address. */ + static equals(a: PointerValue, b: PointerValue): boolean; + /** Return the direct memory pointer to the typed array in memory. */ + static of(value: Deno.UnsafeCallback | BufferSource): PointerValue; + /** Return a new pointer offset from the original by `offset` bytes. */ + static offset(value: PointerObject, offset: number): PointerValue; + /** Get the numeric value of a pointer */ + static value(value: PointerValue): bigint; + } + + /** An unsafe pointer view to a memory location as specified by the `pointer` + * value. The `UnsafePointerView` API follows the standard built in interface + * {@linkcode DataView} for accessing the underlying types at an memory + * location (numbers, strings and raw bytes). + * + * @category FFI + */ + export class UnsafePointerView { + constructor(pointer: PointerObject); + + pointer: PointerObject; + + /** Gets a boolean at the specified byte offset from the pointer. */ + getBool(offset?: number): boolean; + /** Gets an unsigned 8-bit integer at the specified byte offset from the + * pointer. */ + getUint8(offset?: number): number; + /** Gets a signed 8-bit integer at the specified byte offset from the + * pointer. */ + getInt8(offset?: number): number; + /** Gets an unsigned 16-bit integer at the specified byte offset from the + * pointer. */ + getUint16(offset?: number): number; + /** Gets a signed 16-bit integer at the specified byte offset from the + * pointer. */ + getInt16(offset?: number): number; + /** Gets an unsigned 32-bit integer at the specified byte offset from the + * pointer. */ + getUint32(offset?: number): number; + /** Gets a signed 32-bit integer at the specified byte offset from the + * pointer. */ + getInt32(offset?: number): number; + /** Gets an unsigned 64-bit integer at the specified byte offset from the + * pointer. */ + getBigUint64(offset?: number): bigint; + /** Gets a signed 64-bit integer at the specified byte offset from the + * pointer. */ + getBigInt64(offset?: number): bigint; + /** Gets a signed 32-bit float at the specified byte offset from the + * pointer. */ + getFloat32(offset?: number): number; + /** Gets a signed 64-bit float at the specified byte offset from the + * pointer. */ + getFloat64(offset?: number): number; + /** Gets a pointer at the specified byte offset from the pointer */ + getPointer(offset?: number): PointerValue; + /** Gets a C string (`null` terminated string) at the specified byte offset + * from the pointer. */ + getCString(offset?: number): string; + /** Gets a C string (`null` terminated string) at the specified byte offset + * from the specified pointer. */ + static getCString(pointer: PointerObject, offset?: number): string; + /** Gets an `ArrayBuffer` of length `byteLength` at the specified byte + * offset from the pointer. */ + getArrayBuffer(byteLength: number, offset?: number): ArrayBuffer; + /** Gets an `ArrayBuffer` of length `byteLength` at the specified byte + * offset from the specified pointer. */ + static getArrayBuffer(pointer: PointerObject, byteLength: number, offset?: number): ArrayBuffer; + /** Copies the memory of the pointer into a typed array. + * + * Length is determined from the typed array's `byteLength`. + * + * Also takes optional byte offset from the pointer. */ + copyInto(destination: BufferSource, offset?: number): void; + /** Copies the memory of the specified pointer into a typed array. + * + * Length is determined from the typed array's `byteLength`. + * + * Also takes optional byte offset from the pointer. */ + static copyInto(pointer: PointerObject, destination: BufferSource, offset?: number): void; + } + + /** An unsafe pointer to a function, for calling functions that are not present + * as symbols. + * + * @category FFI + */ + export class UnsafeFnPointer { + /** The pointer to the function. */ + pointer: PointerObject; + /** The definition of the function. */ + definition: Fn; + + constructor(pointer: PointerObject>>, definition: Fn); + + /** Call the foreign function. */ + call: FromForeignFunction; + } + + /** Definition of a unsafe callback function. + * + * @category FFI + */ + export interface UnsafeCallbackDefinition< + Parameters extends readonly NativeType[] = readonly NativeType[], + Result extends NativeResultType = NativeResultType, + > { + /** The parameters of the callbacks. */ + parameters: Parameters; + /** The current result of the callback. */ + result: Result; + } + + /** An unsafe callback function. + * + * @category FFI + */ + export type UnsafeCallbackFunction< + Parameters extends readonly NativeType[] = readonly NativeType[], + Result extends NativeResultType = NativeResultType, + > = Parameters extends readonly [] + ? () => ToNativeResultType + : (...args: FromNativeParameterTypes) => ToNativeResultType; + + /** An unsafe function pointer for passing JavaScript functions as C function + * pointers to foreign function calls. + * + * The function pointer remains valid until the `close()` method is called. + * + * All `UnsafeCallback` are always thread safe in that they can be called from + * foreign threads without crashing. However, they do not wake up the Deno event + * loop by default. + * + * If a callback is to be called from foreign threads, use the `threadSafe()` + * static constructor or explicitly call `ref()` to have the callback wake up + * the Deno event loop when called from foreign threads. This also stops + * Deno's process from exiting while the callback still exists and is not + * unref'ed. + * + * Use `deref()` to then allow Deno's process to exit. Calling `deref()` on + * a ref'ed callback does not stop it from waking up the Deno event loop when + * called from foreign threads. + * + * @category FFI + */ + export class UnsafeCallback { + constructor(definition: Definition, callback: UnsafeCallbackFunction); + + /** The pointer to the unsafe callback. */ + readonly pointer: PointerObject; + /** The definition of the unsafe callback. */ + readonly definition: Definition; + /** The callback function. */ + readonly callback: UnsafeCallbackFunction; + + /** + * Creates an {@linkcode UnsafeCallback} and calls `ref()` once to allow it to + * wake up the Deno event loop when called from foreign threads. + * + * This also stops Deno's process from exiting while the callback still + * exists and is not unref'ed. + */ + static threadSafe( + definition: Definition, + callback: UnsafeCallbackFunction, + ): UnsafeCallback; + + /** + * Increments the callback's reference counting and returns the new + * reference count. + * + * After `ref()` has been called, the callback always wakes up the + * Deno event loop when called from foreign threads. + * + * If the callback's reference count is non-zero, it keeps Deno's + * process from exiting. + */ + ref(): number; + + /** + * Decrements the callback's reference counting and returns the new + * reference count. + * + * Calling `unref()` does not stop a callback from waking up the Deno + * event loop when called from foreign threads. + * + * If the callback's reference counter is zero, it no longer keeps + * Deno's process from exiting. + */ + unref(): number; + + /** + * Removes the C function pointer associated with this instance. + * + * Continuing to use the instance or the C function pointer after closing + * the `UnsafeCallback` will lead to errors and crashes. + * + * Calling this method sets the callback's reference counting to zero, + * stops the callback from waking up the Deno event loop when called from + * foreign threads and no longer keeps Deno's process from exiting. + */ + close(): void; + } + + /** A dynamic library resource. Use {@linkcode Deno.dlopen} to load a dynamic + * library and return this interface. + * + * @category FFI + */ + export interface DynamicLibrary { + /** All of the registered library along with functions for calling them. */ + symbols: StaticForeignLibraryInterface; + /** Removes the pointers associated with the library symbols. + * + * Continuing to use symbols that are part of the library will lead to + * errors and crashes. + * + * Calling this method will also immediately set any references to zero and + * will no longer keep Deno's process from exiting. + */ + close(): void; + } + + /** Opens an external dynamic library and registers symbols, making foreign + * functions available to be called. + * + * Requires `allow-ffi` permission. Loading foreign dynamic libraries can in + * theory bypass all of the sandbox permissions. While it is a separate + * permission users should acknowledge in practice that is effectively the + * same as running with the `allow-all` permission. + * + * @example Given a C library which exports a foreign function named `add()` + * + * ```ts + * // Determine library extension based on + * // your OS. + * let libSuffix = ""; + * switch (Deno.build.os) { + * case "windows": + * libSuffix = "dll"; + * break; + * case "darwin": + * libSuffix = "dylib"; + * break; + * default: + * libSuffix = "so"; + * break; + * } + * + * const libName = `./libadd.${libSuffix}`; + * // Open library and define exported symbols + * const dylib = Deno.dlopen( + * libName, + * { + * "add": { parameters: ["isize", "isize"], result: "isize" }, + * } as const, + * ); + * + * // Call the symbol `add` + * const result = dylib.symbols.add(35n, 34n); // 69n + * + * console.log(`Result from external addition of 35 and 34: ${result}`); + * ``` + * + * @tags allow-ffi + * @category FFI + */ + export function dlopen(filename: string | URL, symbols: S): DynamicLibrary; + + /** + * A custom `HttpClient` for use with {@linkcode fetch} function. This is + * designed to allow custom certificates or proxies to be used with `fetch()`. + * + * @example ```ts + * const caCert = await Deno.readTextFile("./ca.pem"); + * const client = Deno.createHttpClient({ caCerts: [ caCert ] }); + * const req = await fetch("https://myserver.com", { client }); + * ``` + * + * @category Fetch + */ + export class HttpClient implements Disposable { + /** Close the HTTP client. */ + close(): void; + + [Symbol.dispose](): void; + } + + /** + * The options used when creating a {@linkcode Deno.HttpClient}. + * + * @category Fetch + */ + export interface CreateHttpClientOptions { + /** A list of root certificates that will be used in addition to the + * default root certificates to verify the peer's certificate. + * + * Must be in PEM format. */ + caCerts?: string[]; + /** A HTTP proxy to use for new connections. */ + proxy?: Proxy; + /** Sets the maximum number of idle connections per host allowed in the pool. */ + poolMaxIdlePerHost?: number; + /** Set an optional timeout for idle sockets being kept-alive. + * Set to false to disable the timeout. */ + poolIdleTimeout?: number | false; + /** + * Whether HTTP/1.1 is allowed or not. + * + * @default {true} + */ + http1?: boolean; + /** Whether HTTP/2 is allowed or not. + * + * @default {true} + */ + http2?: boolean; + /** Whether setting the host header is allowed or not. + * + * @default {false} + */ + allowHost?: boolean; + } + + /** + * The definition of a proxy when specifying + * {@linkcode Deno.CreateHttpClientOptions}. + * + * @category Fetch + */ + export interface Proxy { + /** The string URL of the proxy server to use. */ + url: string; + /** The basic auth credentials to be used against the proxy server. */ + basicAuth?: BasicAuth; + } + + /** + * Basic authentication credentials to be used with a {@linkcode Deno.Proxy} + * server when specifying {@linkcode Deno.CreateHttpClientOptions}. + * + * @category Fetch + */ + export interface BasicAuth { + /** The username to be used against the proxy server. */ + username: string; + /** The password to be used against the proxy server. */ + password: string; + } + + /** Create a custom HttpClient to use with {@linkcode fetch}. This is an + * extension of the web platform Fetch API which allows Deno to use custom + * TLS CA certificates and connect via a proxy while using `fetch()`. + * + * The `cert` and `key` options can be used to specify a client certificate + * and key to use when connecting to a server that requires client + * authentication (mutual TLS or mTLS). The `cert` and `key` options must be + * provided in PEM format. + * + * @example ```ts + * const caCert = await Deno.readTextFile("./ca.pem"); + * const client = Deno.createHttpClient({ caCerts: [ caCert ] }); + * const response = await fetch("https://myserver.com", { client }); + * ``` + * + * @example ```ts + * const client = Deno.createHttpClient({ + * proxy: { url: "http://myproxy.com:8080" } + * }); + * const response = await fetch("https://myserver.com", { client }); + * ``` + * + * @example ```ts + * const key = "----BEGIN PRIVATE KEY----..."; + * const cert = "----BEGIN CERTIFICATE----..."; + * const client = Deno.createHttpClient({ key, cert }); + * const response = await fetch("https://myserver.com", { client }); + * ``` + * + * @category Fetch + */ + export function createHttpClient(options: CreateHttpClientOptions | (CreateHttpClientOptions & TlsCertifiedKeyPem)): HttpClient; + + export {}; // only export exports +} + +// Copyright 2018-2025 the Deno authors. MIT license. + +// deno-lint-ignore-file no-explicit-any + +/// +/// + +/** + * The Console interface provides methods for logging information to the console, + * as well as other utility methods for debugging and inspecting code. + * Methods include logging, debugging, and timing functionality. + * @see https://developer.mozilla.org/en-US/docs/Web/API/console + * + * @category I/O + */ + +interface Console { + /** + * Tests that an expression is true. If not, logs an error message + * @param condition The expression to test for truthiness + * @param data Additional arguments to be printed if the assertion fails + * @example + * ```ts + * console.assert(1 === 1, "This won't show"); + * console.assert(1 === 2, "This will show an error"); + * ``` + */ + assert(condition?: boolean, ...data: any[]): void; + + /** + * Clears the console if the environment allows it + * @example + * ```ts + * console.clear(); + * ``` + */ + clear(): void; + + /** + * Maintains an internal counter for a given label, incrementing it each time the method is called + * @param label The label to count. Defaults to 'default' + * @example + * ```ts + * console.count('myCounter'); + * console.count('myCounter'); // Will show: myCounter: 2 + * ``` + */ + count(label?: string): void; + + /** + * Resets the counter for a given label + * @param label The label to reset. Defaults to 'default' + * @example + * ```ts + * console.count('myCounter'); + * console.countReset('myCounter'); // Resets to 0 + * ``` + */ + countReset(label?: string): void; + + /** + * Outputs a debugging message to the console + * @param data Values to be printed to the console + * @example + * ```ts + * console.debug('Debug message', { detail: 'some data' }); + * ``` + */ + debug(...data: any[]): void; + + /** + * Displays a list of the properties of a specified object + * @param item Object to display + * @param options Formatting options + * @example + * ```ts + * console.dir({ name: 'object', value: 42 }, { depth: 1 }); + * ``` + */ + dir(item?: any, options?: any): void; + + /** + * @ignore + */ + dirxml(...data: any[]): void; + + /** + * Outputs an error message to the console. + * This method routes the output to stderr, + * unlike other console methods that route to stdout. + * @param data Values to be printed to the console + * @example + * ```ts + * console.error('Error occurred:', new Error('Something went wrong')); + * ``` + */ + error(...data: any[]): void; + + /** + * Creates a new inline group in the console, indenting subsequent console messages + * @param data Labels for the group + * @example + * ```ts + * console.group('Group 1'); + * console.log('Inside group 1'); + * console.groupEnd(); + * ``` + */ + group(...data: any[]): void; + + /** + * Creates a new inline group in the console that is initially collapsed + * @param data Labels for the group + * @example + * ```ts + * console.groupCollapsed('Details'); + * console.log('Hidden until expanded'); + * console.groupEnd(); + * ``` + */ + groupCollapsed(...data: any[]): void; + + /** + * Exits the current inline group in the console + * @example + * ```ts + * console.group('Group'); + * console.log('Grouped message'); + * console.groupEnd(); + * ``` + */ + groupEnd(): void; + + /** + * Outputs an informational message to the console + * @param data Values to be printed to the console + * @example + * ```ts + * console.info('Application started', { version: '1.0.0' }); + * ``` + */ + info(...data: any[]): void; + + /** + * Outputs a message to the console + * @param data Values to be printed to the console + * @example + * ```ts + * console.log('Hello', 'World', 123); + * ``` + */ + log(...data: any[]): void; + + /** + * Displays tabular data as a table + * @param tabularData Data to be displayed in table format + * @param properties Array of property names to be displayed + * @example + * ```ts + * console.table([ + * { name: 'John', age: 30 }, + * { name: 'Jane', age: 25 } + * ]); + * ``` + */ + table(tabularData?: any, properties?: string[]): void; + + /** + * Starts a timer you can use to track how long an operation takes + * @param label Timer label. Defaults to 'default' + * @example + * ```ts + * console.time('operation'); + * // ... some code + * console.timeEnd('operation'); + * ``` + */ + time(label?: string): void; + + /** + * Stops a timer that was previously started + * @param label Timer label to stop. Defaults to 'default' + * @example + * ```ts + * console.time('operation'); + * // ... some code + * console.timeEnd('operation'); // Prints: operation: 1234ms + * ``` + */ + timeEnd(label?: string): void; + + /** + * Logs the current value of a timer that was previously started + * @param label Timer label + * @param data Additional data to log + * @example + * ```ts + * console.time('process'); + * // ... some code + * console.timeLog('process', 'Checkpoint A'); + * ``` + */ + timeLog(label?: string, ...data: any[]): void; + + /** + * Outputs a stack trace to the console + * @param data Values to be printed to the console + * @example + * ```ts + * console.trace('Trace message'); + * ``` + */ + trace(...data: any[]): void; + + /** + * Outputs a warning message to the console + * @param data Values to be printed to the console + * @example + * ```ts + * console.warn('Deprecated feature used'); + * ``` + */ + warn(...data: any[]): void; + + /** + * Adds a marker to the DevTools Performance panel + * @param label Label for the timestamp + * @example + * ```ts + * console.timeStamp('Navigation Start'); + * ``` + */ + timeStamp(label?: string): void; + + /** + * Starts recording a performance profile + * @param label Profile label + * @example + * ```ts + * console.profile('Performance Profile'); + * // ... code to profile + * console.profileEnd('Performance Profile'); + * ``` + */ + profile(label?: string): void; + + /** + * Stops recording a performance profile + * @param label Profile label to stop + * @example + * ```ts + * console.profile('Performance Profile'); + * // ... code to profile + * console.profileEnd('Performance Profile'); + * ``` + */ + profileEnd(label?: string): void; +} + +// Copyright 2018-2025 the Deno authors. MIT license. + +// deno-lint-ignore-file no-explicit-any no-var + +/// +/// + +/** @category URL */ +interface URLSearchParamsIterator extends IteratorObject { + [Symbol.iterator](): URLSearchParamsIterator; +} + +/** @category URL */ +interface URLSearchParams { + /** Appends a specified key/value pair as a new search parameter. + * + * ```ts + * let searchParams = new URLSearchParams(); + * searchParams.append('name', 'first'); + * searchParams.append('name', 'second'); + * ``` + */ + append(name: string, value: string): void; + + /** Deletes search parameters that match a name, and optional value, + * from the list of all search parameters. + * + * ```ts + * let searchParams = new URLSearchParams([['name', 'value']]); + * searchParams.delete('name'); + * searchParams.delete('name', 'value'); + * ``` + */ + delete(name: string, value?: string): void; + + /** Returns all the values associated with a given search parameter + * as an array. + * + * ```ts + * searchParams.getAll('name'); + * ``` + */ + getAll(name: string): string[]; + + /** Returns the first value associated to the given search parameter. + * + * ```ts + * searchParams.get('name'); + * ``` + */ + get(name: string): string | null; + + /** Returns a boolean value indicating if a given parameter, + * or parameter and value pair, exists. + * + * ```ts + * searchParams.has('name'); + * searchParams.has('name', 'value'); + * ``` + */ + has(name: string, value?: string): boolean; + + /** Sets the value associated with a given search parameter to the + * given value. If there were several matching values, this method + * deletes the others. If the search parameter doesn't exist, this + * method creates it. + * + * ```ts + * searchParams.set('name', 'value'); + * ``` + */ + set(name: string, value: string): void; + + /** Sort all key/value pairs contained in this object in place and + * return undefined. The sort order is according to Unicode code + * points of the keys. + * + * ```ts + * searchParams.sort(); + * ``` + */ + sort(): void; + + /** Calls a function for each element contained in this object in + * place and return undefined. Optionally accepts an object to use + * as this when executing callback as second argument. + * + * ```ts + * const params = new URLSearchParams([["a", "b"], ["c", "d"]]); + * params.forEach((value, key, parent) => { + * console.log(value, key, parent); + * }); + * ``` + */ + forEach(callbackfn: (value: string, key: string, parent: this) => void, thisArg?: any): void; + + /** Returns an iterator allowing to go through all keys contained + * in this object. + * + * ```ts + * const params = new URLSearchParams([["a", "b"], ["c", "d"]]); + * for (const key of params.keys()) { + * console.log(key); + * } + * ``` + */ + keys(): URLSearchParamsIterator; + + /** Returns an iterator allowing to go through all values contained + * in this object. + * + * ```ts + * const params = new URLSearchParams([["a", "b"], ["c", "d"]]); + * for (const value of params.values()) { + * console.log(value); + * } + * ``` + */ + values(): URLSearchParamsIterator; + + /** Returns an iterator allowing to go through all key/value + * pairs contained in this object. + * + * ```ts + * const params = new URLSearchParams([["a", "b"], ["c", "d"]]); + * for (const [key, value] of params.entries()) { + * console.log(key, value); + * } + * ``` + */ + entries(): URLSearchParamsIterator<[string, string]>; + + /** Returns an iterator allowing to go through all key/value + * pairs contained in this object. + * + * ```ts + * const params = new URLSearchParams([["a", "b"], ["c", "d"]]); + * for (const [key, value] of params) { + * console.log(key, value); + * } + * ``` + */ + [Symbol.iterator](): URLSearchParamsIterator<[string, string]>; + + /** Returns a query string suitable for use in a URL. + * + * ```ts + * searchParams.toString(); + * ``` + */ + toString(): string; + + /** Contains the number of search parameters + * + * ```ts + * searchParams.size + * ``` + */ + readonly size: number; +} + +/** @category URL */ +declare var URLSearchParams: { + readonly prototype: URLSearchParams; + new (init?: Iterable | Record | string | URLSearchParams): URLSearchParams; +}; + +/** The URL interface represents an object providing static methods used for + * creating, parsing, and manipulating URLs. + * + * @see https://developer.mozilla.org/docs/Web/API/URL + * + * @category URL + */ +interface URL { + /** + * The hash property of the URL interface is a string that starts with a `#` and is followed by the fragment identifier of the URL. + * It returns an empty string if the URL does not contain a fragment identifier. + * + * @example + * ```ts + * const myURL = new URL('https://example.org/foo#bar'); + * console.log(myURL.hash); // Logs "#bar" + * ``` + * + * @example + * ```ts + * const myURL = new URL('https://example.org'); + * console.log(myURL.hash); // Logs "" + * ``` + * + * @see https://developer.mozilla.org/docs/Web/API/URL/hash + */ + hash: string; + + /** + * The `host` property of the URL interface is a string that includes the {@linkcode URL.hostname} and the {@linkcode URL.port} if one is specified in the URL includes by including a `:` followed by the port number. + * + * @example + * ```ts + * const myURL = new URL('https://example.org/foo'); + * console.log(myURL.host); // Logs "example.org" + * ``` + * + * @example + * ```ts + * const myURL = new URL('https://example.org:8080/foo'); + * console.log(myURL.host); // Logs "example.org:8080" + * ``` + * + * @see https://developer.mozilla.org/docs/Web/API/URL/host + */ + host: string; + + /** + * The `hostname` property of the URL interface is a string that represents the fully qualified domain name of the URL. + * + * @example + * ```ts + * const myURL = new URL('https://foo.example.org/bar'); + * console.log(myURL.hostname); // Logs "foo.example.org" + * ``` + * + * @see https://developer.mozilla.org/docs/Web/API/URL/hostname + */ + hostname: string; + + /** + * The `href` property of the URL interface is a string that represents the complete URL. + * + * @example + * ```ts + * const myURL = new URL('https://foo.example.org/bar?baz=qux#quux'); + * console.log(myURL.href); // Logs "https://foo.example.org/bar?baz=qux#quux" + * ``` + * + * @see https://developer.mozilla.org/docs/Web/API/URL/href + */ + href: string; + + /** + * The `toString()` method of the URL interface returns a string containing the complete URL. + * + * @example + * ```ts + * const myURL = new URL('https://foo.example.org/bar'); + * console.log(myURL.toString()); // Logs "https://foo.example.org/bar" + * ``` + * + * @see https://developer.mozilla.org/docs/Web/API/URL/toString + */ + toString(): string; + + /** + * The `origin` property of the URL interface is a string that represents the origin of the URL, that is the {@linkcode URL.protocol}, {@linkcode URL.host}, and {@linkcode URL.port}. + * + * @example + * ```ts + * const myURL = new URL('https://foo.example.org/bar'); + * console.log(myURL.origin); // Logs "https://foo.example.org" + * ``` + * + * @example + * ```ts + * const myURL = new URL('https://example.org:8080/foo'); + * console.log(myURL.origin); // Logs "https://example.org:8080" + * ``` + * + * @see https://developer.mozilla.org/docs/Web/API/URL/origin + */ + readonly origin: string; + + /** + * The `password` property of the URL interface is a string that represents the password specified in the URL. + * + * @example + * ```ts + * const myURL = new URL('https://someone:somepassword@example.org/baz'); + * console.log(myURL.password); // Logs "somepassword" + * ``` + * + * @see https://developer.mozilla.org/docs/Web/API/URL/password + */ + password: string; + + /** + * The `pathname` property of the URL interface is a string that represents the path of the URL. + * + * @example + * ```ts + * const myURL = new URL('https://example.org/foo/bar'); + * console.log(myURL.pathname); // Logs "/foo/bar" + * ``` + * + * @example + * ```ts + * const myURL = new URL('https://example.org'); + * console.log(myURL.pathname); // Logs "/" + * ``` + * + * @see https://developer.mozilla.org/docs/Web/API/URL/pathname + */ + pathname: string; + + /** + * The `port` property of the URL interface is a string that represents the port of the URL if an explicit port has been specified in the URL. + * + * @example + * ```ts + * const myURL = new URL('https://example.org:8080/foo'); + * console.log(myURL.port); // Logs "8080" + * ``` + * + * @example + * ```ts + * const myURL = new URL('https://example.org/foo'); + * console.log(myURL.port); // Logs "" + * ``` + * + * @see https://developer.mozilla.org/docs/Web/API/URL/port + */ + port: string; + + /** + * The `protocol` property of the URL interface is a string that represents the protocol scheme of the URL and includes a trailing `:`. + * + * @example + * ```ts + * const myURL = new URL('https://example.org/foo'); + * console.log(myURL.protocol); // Logs "https:" + * ``` + * + * @see https://developer.mozilla.org/docs/Web/API/URL/protocol + */ + protocol: string; + + /** + * The `search` property of the URL interface is a string that represents the search string, or the query string, of the URL. + * This includes the `?` character and the but excludes identifiers within the represented resource such as the {@linkcode URL.hash}. More granular control can be found using {@linkcode URL.searchParams} property. + * + * @example + * ```ts + * const myURL = new URL('https://example.org/foo?bar=baz'); + * console.log(myURL.search); // Logs "?bar=baz" + * ``` + * + * @example + * ```ts + * const myURL = new URL('https://example.org/foo?bar=baz#quux'); + * console.log(myURL.search); // Logs "?bar=baz" + * ``` + * + * @see https://developer.mozilla.org/docs/Web/API/URL/search + */ + search: string; + + /** + * The `searchParams` property of the URL interface is a {@linkcode URL.URLSearchParams} object that represents the search parameters of the URL. + * + * @example + * ```ts + * const myURL = new URL('https://example.org/foo?bar=baz'); + * const params = myURL.searchParams; + * + * console.log(params); // Logs { bar: "baz" } + * console.log(params.get('bar')); // Logs "baz" + * ``` + * + * @see https://developer.mozilla.org/docs/Web/API/URL/searchParams + */ + readonly searchParams: URLSearchParams; + + /** + * The `username` property of the URL interface is a string that represents the username of the URL. + * + * @example + * ```ts + * const myURL = new URL('https://someone:somepassword@example.org/baz'); + * console.log(myURL.username); // Logs "someone" + * ``` + * + * @see https://developer.mozilla.org/docs/Web/API/URL/username + */ + username: string; + + /** + * The `toJSON()` method of the URL interface returns a JSON representation of the URL. + * + * @example + * ```ts + * const myURL = new URL('https://example.org/foo'); + * console.log(myURL.toJSON()); // Logs "https://example.org/foo" + * ``` + * + * @see https://developer.mozilla.org/docs/Web/API/URL/toJSON + */ + toJSON(): string; +} + +/** The URL interface represents an object providing static methods used for + * creating object URLs. + * + * @category URL + */ +declare var URL: { + readonly prototype: URL; + /** + * @see https://developer.mozilla.org/docs/Web/API/URL/URL + */ + new (url: string | URL, base?: string | URL): URL; + + /** + * @see https://developer.mozilla.org/docs/Web/API/URL/parse_static + */ + parse(url: string | URL, base?: string | URL): URL | null; + + /** + * @see https://developer.mozilla.org/docs/Web/API/URL/canParse_static + */ + canParse(url: string | URL, base?: string | URL): boolean; + + /** + * @see https://developer.mozilla.org/docs/Web/API/URL/createObjectURL + */ + createObjectURL(blob: Blob): string; + + /** + * @see https://developer.mozilla.org/docs/Web/API/URL/revokeObjectURL + */ + revokeObjectURL(url: string): void; +}; + +/** @category URL */ +interface URLPatternInit { + protocol?: string; + username?: string; + password?: string; + hostname?: string; + port?: string; + pathname?: string; + search?: string; + hash?: string; + baseURL?: string; +} + +/** @category URL */ +type URLPatternInput = string | URLPatternInit; + +/** @category URL */ +interface URLPatternComponentResult { + input: string; + groups: Record; +} + +/** `URLPatternResult` is the object returned from `URLPattern.exec`. + * + * @category URL + */ +interface URLPatternResult { + /** The inputs provided when matching. */ + inputs: [URLPatternInit] | [URLPatternInit, string]; + + /** The matched result for the `protocol` matcher. */ + protocol: URLPatternComponentResult; + /** The matched result for the `username` matcher. */ + username: URLPatternComponentResult; + /** The matched result for the `password` matcher. */ + password: URLPatternComponentResult; + /** The matched result for the `hostname` matcher. */ + hostname: URLPatternComponentResult; + /** The matched result for the `port` matcher. */ + port: URLPatternComponentResult; + /** The matched result for the `pathname` matcher. */ + pathname: URLPatternComponentResult; + /** The matched result for the `search` matcher. */ + search: URLPatternComponentResult; + /** The matched result for the `hash` matcher. */ + hash: URLPatternComponentResult; +} + +/** + * Options for the {@linkcode URLPattern} constructor. + * + * @category URL + */ +interface URLPatternOptions { + /** + * Enables case-insensitive matching. + * + * @default {false} + */ + ignoreCase: boolean; +} + +/** + * The URLPattern API provides a web platform primitive for matching URLs based + * on a convenient pattern syntax. + * + * The syntax is based on path-to-regexp. Wildcards, named capture groups, + * regular groups, and group modifiers are all supported. + * + * ```ts + * // Specify the pattern as structured data. + * const pattern = new URLPattern({ pathname: "/users/:user" }); + * const match = pattern.exec("https://blog.example.com/users/joe"); + * console.log(match.pathname.groups.user); // joe + * ``` + * + * ```ts + * // Specify a fully qualified string pattern. + * const pattern = new URLPattern("https://example.com/books/:id"); + * console.log(pattern.test("https://example.com/books/123")); // true + * console.log(pattern.test("https://deno.land/books/123")); // false + * ``` + * + * ```ts + * // Specify a relative string pattern with a base URL. + * const pattern = new URLPattern("/article/:id", "https://blog.example.com"); + * console.log(pattern.test("https://blog.example.com/article")); // false + * console.log(pattern.test("https://blog.example.com/article/123")); // true + * ``` + * + * @category URL + */ +interface URLPattern { + /** + * Test if the given input matches the stored pattern. + * + * The input can either be provided as an absolute URL string with an optional base, + * relative URL string with a required base, or as individual components + * in the form of an `URLPatternInit` object. + * + * ```ts + * const pattern = new URLPattern("https://example.com/books/:id"); + * + * // Test an absolute url string. + * console.log(pattern.test("https://example.com/books/123")); // true + * + * // Test a relative url with a base. + * console.log(pattern.test("/books/123", "https://example.com")); // true + * + * // Test an object of url components. + * console.log(pattern.test({ pathname: "/books/123" })); // true + * ``` + */ + test(input: URLPatternInput, baseURL?: string): boolean; + + /** + * Match the given input against the stored pattern. + * + * The input can either be provided as an absolute URL string with an optional base, + * relative URL string with a required base, or as individual components + * in the form of an `URLPatternInit` object. + * + * ```ts + * const pattern = new URLPattern("https://example.com/books/:id"); + * + * // Match an absolute url string. + * let match = pattern.exec("https://example.com/books/123"); + * console.log(match.pathname.groups.id); // 123 + * + * // Match a relative url with a base. + * match = pattern.exec("/books/123", "https://example.com"); + * console.log(match.pathname.groups.id); // 123 + * + * // Match an object of url components. + * match = pattern.exec({ pathname: "/books/123" }); + * console.log(match.pathname.groups.id); // 123 + * ``` + */ + exec(input: URLPatternInput, baseURL?: string): URLPatternResult | null; + + /** The pattern string for the `protocol`. */ + readonly protocol: string; + /** The pattern string for the `username`. */ + readonly username: string; + /** The pattern string for the `password`. */ + readonly password: string; + /** The pattern string for the `hostname`. */ + readonly hostname: string; + /** The pattern string for the `port`. */ + readonly port: string; + /** The pattern string for the `pathname`. */ + readonly pathname: string; + /** The pattern string for the `search`. */ + readonly search: string; + /** The pattern string for the `hash`. */ + readonly hash: string; + + /** Whether or not any of the specified groups use regexp groups. */ + readonly hasRegExpGroups: boolean; +} + +/** + * The URLPattern API provides a web platform primitive for matching URLs based + * on a convenient pattern syntax. + * + * The syntax is based on path-to-regexp. Wildcards, named capture groups, + * regular groups, and group modifiers are all supported. + * + * ```ts + * // Specify the pattern as structured data. + * const pattern = new URLPattern({ pathname: "/users/:user" }); + * const match = pattern.exec("https://blog.example.com/users/joe"); + * console.log(match.pathname.groups.user); // joe + * ``` + * + * ```ts + * // Specify a fully qualified string pattern. + * const pattern = new URLPattern("https://example.com/books/:id"); + * console.log(pattern.test("https://example.com/books/123")); // true + * console.log(pattern.test("https://deno.land/books/123")); // false + * ``` + * + * ```ts + * // Specify a relative string pattern with a base URL. + * const pattern = new URLPattern("/article/:id", "https://blog.example.com"); + * console.log(pattern.test("https://blog.example.com/article")); // false + * console.log(pattern.test("https://blog.example.com/article/123")); // true + * ``` + * + * @category URL + */ +declare var URLPattern: { + readonly prototype: URLPattern; + new (input: URLPatternInput, baseURL: string, options?: URLPatternOptions): URLPattern; + new (input?: URLPatternInput, options?: URLPatternOptions): URLPattern; +}; + +// Copyright 2018-2025 the Deno authors. MIT license. + +// deno-lint-ignore-file no-explicit-any no-var + +/// +/// + +/** @category Platform */ +interface DOMException extends Error { + readonly name: string; + readonly message: string; + /** @deprecated */ + readonly code: number; + readonly INDEX_SIZE_ERR: 1; + readonly DOMSTRING_SIZE_ERR: 2; + readonly HIERARCHY_REQUEST_ERR: 3; + readonly WRONG_DOCUMENT_ERR: 4; + readonly INVALID_CHARACTER_ERR: 5; + readonly NO_DATA_ALLOWED_ERR: 6; + readonly NO_MODIFICATION_ALLOWED_ERR: 7; + readonly NOT_FOUND_ERR: 8; + readonly NOT_SUPPORTED_ERR: 9; + readonly INUSE_ATTRIBUTE_ERR: 10; + readonly INVALID_STATE_ERR: 11; + readonly SYNTAX_ERR: 12; + readonly INVALID_MODIFICATION_ERR: 13; + readonly NAMESPACE_ERR: 14; + readonly INVALID_ACCESS_ERR: 15; + readonly VALIDATION_ERR: 16; + readonly TYPE_MISMATCH_ERR: 17; + readonly SECURITY_ERR: 18; + readonly NETWORK_ERR: 19; + readonly ABORT_ERR: 20; + readonly URL_MISMATCH_ERR: 21; + readonly QUOTA_EXCEEDED_ERR: 22; + readonly TIMEOUT_ERR: 23; + readonly INVALID_NODE_TYPE_ERR: 24; + readonly DATA_CLONE_ERR: 25; +} + +/** @category Platform */ +declare var DOMException: { + readonly prototype: DOMException; + new (message?: string, name?: string): DOMException; + readonly INDEX_SIZE_ERR: 1; + readonly DOMSTRING_SIZE_ERR: 2; + readonly HIERARCHY_REQUEST_ERR: 3; + readonly WRONG_DOCUMENT_ERR: 4; + readonly INVALID_CHARACTER_ERR: 5; + readonly NO_DATA_ALLOWED_ERR: 6; + readonly NO_MODIFICATION_ALLOWED_ERR: 7; + readonly NOT_FOUND_ERR: 8; + readonly NOT_SUPPORTED_ERR: 9; + readonly INUSE_ATTRIBUTE_ERR: 10; + readonly INVALID_STATE_ERR: 11; + readonly SYNTAX_ERR: 12; + readonly INVALID_MODIFICATION_ERR: 13; + readonly NAMESPACE_ERR: 14; + readonly INVALID_ACCESS_ERR: 15; + readonly VALIDATION_ERR: 16; + readonly TYPE_MISMATCH_ERR: 17; + readonly SECURITY_ERR: 18; + readonly NETWORK_ERR: 19; + readonly ABORT_ERR: 20; + readonly URL_MISMATCH_ERR: 21; + readonly QUOTA_EXCEEDED_ERR: 22; + readonly TIMEOUT_ERR: 23; + readonly INVALID_NODE_TYPE_ERR: 24; + readonly DATA_CLONE_ERR: 25; +}; + +/** @category Events */ +interface EventInit { + bubbles?: boolean; + cancelable?: boolean; + composed?: boolean; +} + +/** An event which takes place in the DOM. + * + * @category Events + */ +interface Event { + /** Returns true or false depending on how event was initialized. True if + * event goes through its target's ancestors in reverse tree order, and + * false otherwise. */ + readonly bubbles: boolean; + /** @deprecated */ + cancelBubble: boolean; + /** Returns true or false depending on how event was initialized. Its return + * value does not always carry meaning, but true can indicate that part of the + * operation during which event was dispatched, can be canceled by invoking + * the preventDefault() method. */ + readonly cancelable: boolean; + /** Returns true or false depending on how event was initialized. True if + * event invokes listeners past a ShadowRoot node that is the root of its + * target, and false otherwise. */ + readonly composed: boolean; + /** Returns the object whose event listener's callback is currently being + * invoked. */ + readonly currentTarget: EventTarget | null; + /** Returns true if preventDefault() was invoked successfully to indicate + * cancellation, and false otherwise. */ + readonly defaultPrevented: boolean; + /** Returns the event's phase, which is one of NONE, CAPTURING_PHASE, + * AT_TARGET, and BUBBLING_PHASE. */ + readonly eventPhase: number; + /** Returns true if event was dispatched by the user agent, and false + * otherwise. */ + readonly isTrusted: boolean; + /** @deprecated */ + returnValue: boolean; + /** @deprecated */ + readonly srcElement: EventTarget | null; + /** Returns the object to which event is dispatched (its target). */ + readonly target: EventTarget | null; + /** Returns the event's timestamp as the number of milliseconds measured + * relative to the time origin. */ + readonly timeStamp: number; + /** Returns the type of event, e.g. "click", "hashchange", or "submit". */ + readonly type: string; + /** Returns the invocation target objects of event's path (objects on which + * listeners will be invoked), except for any nodes in shadow trees of which + * the shadow root's mode is "closed" that are not reachable from event's + * currentTarget. */ + composedPath(): EventTarget[]; + /** @deprecated */ + initEvent(type: string, bubbles?: boolean, cancelable?: boolean): void; + /** If invoked when the cancelable attribute value is true, and while + * executing a listener for the event with passive set to false, signals to + * the operation that caused event to be dispatched that it needs to be + * canceled. */ + preventDefault(): void; + /** Invoking this method prevents event from reaching any registered event + * listeners after the current one finishes running and, when dispatched in a + * tree, also prevents event from reaching any other objects. */ + stopImmediatePropagation(): void; + /** When dispatched in a tree, invoking this method prevents event from + * reaching any objects other than the current object. */ + stopPropagation(): void; + readonly NONE: 0; + readonly CAPTURING_PHASE: 1; + readonly AT_TARGET: 2; + readonly BUBBLING_PHASE: 3; +} + +/** An event which takes place in the DOM. + * + * @category Events + */ +declare var Event: { + readonly prototype: Event; + new (type: string, eventInitDict?: EventInit): Event; + readonly NONE: 0; + readonly CAPTURING_PHASE: 1; + readonly AT_TARGET: 2; + readonly BUBBLING_PHASE: 3; +}; + +/** + * EventTarget is a DOM interface implemented by objects that can receive events + * and may have listeners for them. + * + * @category Events + */ +interface EventTarget { + /** Appends an event listener for events whose type attribute value is type. + * The callback argument sets the callback that will be invoked when the event + * is dispatched. + * + * The options argument sets listener-specific options. For compatibility this + * can be a boolean, in which case the method behaves exactly as if the value + * was specified as options's capture. + * + * When set to true, options's capture prevents callback from being invoked + * when the event's eventPhase attribute value is BUBBLING_PHASE. When false + * (or not present), callback will not be invoked when event's eventPhase + * attribute value is CAPTURING_PHASE. Either way, callback will be invoked if + * event's eventPhase attribute value is AT_TARGET. + * + * When set to true, options's passive indicates that the callback will not + * cancel the event by invoking preventDefault(). This is used to enable + * performance optimizations described in § 2.8 Observing event listeners. + * + * When set to true, options's once indicates that the callback will only be + * invoked once after which the event listener will be removed. + * + * The event listener is appended to target's event listener list and is not + * appended if it has the same type, callback, and capture. */ + addEventListener( + type: string, + listener: EventListenerOrEventListenerObject | null, + options?: boolean | AddEventListenerOptions, + ): void; + /** Dispatches a synthetic event to event target and returns true if either + * event's cancelable attribute value is false or its preventDefault() method + * was not invoked, and false otherwise. */ + dispatchEvent(event: Event): boolean; + /** Removes the event listener in target's event listener list with the same + * type, callback, and options. */ + removeEventListener( + type: string, + callback: EventListenerOrEventListenerObject | null, + options?: EventListenerOptions | boolean, + ): void; +} + +/** + * EventTarget is a DOM interface implemented by objects that can receive events + * and may have listeners for them. + * + * @category Events + */ +declare var EventTarget: { + readonly prototype: EventTarget; + new (): EventTarget; +}; + +/** @category Events */ +interface EventListener { + (evt: Event): void; +} + +/** @category Events */ +interface EventListenerObject { + handleEvent(evt: Event): void; +} + +/** @category Events */ +type EventListenerOrEventListenerObject = EventListener | EventListenerObject; + +/** @category Events */ +interface AddEventListenerOptions extends EventListenerOptions { + once?: boolean; + passive?: boolean; + signal?: AbortSignal; +} + +/** @category Events */ +interface EventListenerOptions { + capture?: boolean; +} + +/** @category Events */ +interface ProgressEventInit extends EventInit { + lengthComputable?: boolean; + loaded?: number; + total?: number; +} + +/** Events measuring progress of an underlying process, like an HTTP request + * (for an XMLHttpRequest, or the loading of the underlying resource of an + * ,