feat: Add a lot of stuff

This commit is contained in:
Wanjohi
2025-10-25 22:11:33 +03:00
parent a3ee9aadd9
commit c5d0242d50
85 changed files with 6718 additions and 1377 deletions

7
alchemy.run.ts Normal file
View File

@@ -0,0 +1,7 @@
import alchemy from "alchemy";
const app = await alchemy("nestri");
// await import("./infra/api");
await app.finalize();

3282
bun.lock

File diff suppressed because it is too large Load Diff

View File

@@ -1,51 +1,51 @@
{ {
"name": "@nestri/core", "name": "@nestri/cloud-core",
"version": "0.0.0", "version": "0.0.0",
"sideEffects": false, "sideEffects": false,
"type": "module", "type": "module",
"scripts": { "scripts": {
"db:dev": "drizzle-kit", "db:dev": "drizzle-kit",
"typecheck": "tsc --noEmit", "typecheck": "tsc --noEmit",
"db": "sst shell drizzle-kit", "db": "sst shell drizzle-kit",
"db:exec": "sst shell ../scripts/src/psql.sh", "db:exec": "sst shell ../scripts/src/psql.sh",
"db:reset": "sst shell ../scripts/src/db-reset.sh" "db:reset": "sst shell ../scripts/src/db-reset.sh"
}, },
"exports": { "exports": {
"./*": "./src/*.ts" "./*": "./src/*.ts"
}, },
"devDependencies": { "devDependencies": {
"@tsconfig/node20": "^20.1.4", "@tsconfig/node20": "^20.1.4",
"@types/pngjs": "^6.0.5", "@types/pngjs": "^6.0.5",
"@types/sanitize-html": "^2.16.0", "@types/sanitize-html": "^2.16.0",
"@types/xml2js": "^0.4.14", "@types/xml2js": "^0.4.14",
"aws-iot-device-sdk-v2": "^1.21.1", "aws-iot-device-sdk-v2": "^1.21.1",
"aws4fetch": "^1.0.20", "aws4fetch": "^1.0.20",
"mqtt": "^5.10.3", "mqtt": "^5.10.3",
"remeda": "^2.21.2", "remeda": "^2.21.2",
"ulid": "^2.3.0", "ulid": "^2.3.0",
"zod": "^3.24.1", "zod": "^3.24.1",
"zod-openapi": "^4.2.2" "zod-openapi": "^4.2.2"
}, },
"dependencies": { "dependencies": {
"@aws-sdk/client-iot-data-plane": "^3.758.0", "@aws-sdk/client-iot-data-plane": "^3.758.0",
"@aws-sdk/client-sesv2": "^3.753.0", "@aws-sdk/client-sesv2": "^3.753.0",
"@neondatabase/serverless": "^1.0.1", "@neondatabase/serverless": "^1.0.1",
"@openauthjs/openauth": "*", "@openauthjs/openauth": "catalog:",
"@openauthjs/openevent": "^0.0.27", "@openauthjs/openevent": "^0.0.27",
"@polar-sh/sdk": "^0.26.1", "@polar-sh/sdk": "^0.26.1",
"drizzle-kit": "^0.30.5", "drizzle-kit": "^0.30.5",
"drizzle-orm": "^0.40.0", "drizzle-orm": "^0.40.0",
"drizzle-zod": "^0.7.1", "drizzle-zod": "^0.7.1",
"fast-average-color": "^9.5.0", "fast-average-color": "^9.5.0",
"lru-cache": "^11.1.0", "lru-cache": "^11.1.0",
"p-limit": "^6.2.0", "p-limit": "^6.2.0",
"pixelmatch": "^7.1.0", "pixelmatch": "^7.1.0",
"pngjs": "^7.0.0", "pngjs": "^7.0.0",
"postgres": "^3.4.5", "postgres": "^3.4.5",
"sanitize-html": "^2.16.0", "sanitize-html": "^2.16.0",
"sharp": "^0.34.1", "sharp": "^0.34.1",
"steam-session": "*", "steam-session": "*",
"ws": "^8.18.3", "ws": "^8.18.3",
"xml2js": "^0.6.2" "xml2js": "^0.6.2"
} }
} }

View File

@@ -0,0 +1,97 @@
import { z } from "zod/v4";
import { fn } from "../utils/fn";
import { Examples } from "../examples.js";
import { createID } from "../utils/id.js";
import { Polar } from "../polar/index.js";
import { Database } from "../drizzle/index.js";
import { teamTable } from "../team/team.sql.js";
import { and, asc, eq, isNull } from "drizzle-orm";
import { VisibleError, ErrorCodes } from "../error.js";
export namespace Team {
export const Info = z
.object({
id: z.string().meta({
description: "The unique identifier of the team",
example: Examples.Team.id,
}),
email: z.email().meta({
description: "The contact email address for the team",
example: Examples.Team.email,
}),
polarCustomerID: z.string().nullable().meta({
description: "Associated Polar.sh customer identifier",
example: Examples.Team.polarCustomerID,
}),
})
.meta({
ref: "Team",
description: "Represents a team entity",
example: Examples.Team,
});
export type Info = z.infer<typeof Info>;
export class TeamExistsError extends VisibleError {
constructor(email: string) {
super(
"already_exists",
ErrorCodes.Validation.ALREADY_EXISTS,
`A team with this email ${email} already exists`,
);
}
}
export const create = fn(
Info.omit({ polarCustomerID: true }).partial({ id: true }),
async (input) => {
const userID = createID("team");
const customerID = await Polar.fromUserEmail(input.email).then(
(polarUser) => polarUser?.id || null,
);
const id = input.id ?? userID;
await Database.transaction(async (tx) => {
const result = await tx
.insert(teamTable)
.values({
id,
email: input.email,
polarCustomerID: customerID,
})
.onConflictDoNothing({
target: [teamTable.email],
});
if (result.count === 0) {
throw new TeamExistsError(input.email);
}
});
return id;
},
);
export const fromEmail = fn(Info.shape.email, async (email) =>
Database.transaction(async (tx) =>
tx
.select()
.from(teamTable)
.where(and(eq(teamTable.email, email), isNull(teamTable.timeDeleted)))
.orderBy(asc(teamTable.timeCreated))
.execute()
.then((rows) => rows.map(serialize).at(0)),
),
);
export function serialize(
input: typeof teamTable.$inferSelect,
): z.infer<typeof Info> {
return {
id: input.id,
email: input.email,
polarCustomerID: input.polarCustomerID,
};
}
}

View File

@@ -0,0 +1,9 @@
import { ulid, timestamps } from "@/drizzle/types.js";
import { pgTable, varchar } from "drizzle-orm/pg-core";
export const teamTable = pgTable("team", {
...timestamps,
id: ulid("id").primaryKey().notNull(),
email: varchar("email", { length: 255 }).unique().notNull(),
polarCustomerID: varchar("polar_customer_id", { length: 255 }),
});

View File

@@ -1,27 +1,30 @@
import { ZodSchema, z } from "zod"; import { ZodType, z } from "zod/v4";
export function fn< export function fn<
Arg1 extends ZodSchema, Arg1 extends ZodType,
Callback extends (arg1: z.output<Arg1>) => any, Callback extends (arg1: z.output<Arg1>) => any,
>(arg1: Arg1, cb: Callback) { >(arg1: Arg1, cb: Callback) {
const result = function (input: z.input<typeof arg1>): ReturnType<Callback> { const result = function (input: z.input<typeof arg1>): ReturnType<Callback> {
const parsed = arg1.parse(input); const parsed = arg1.parse(input);
return cb.apply(cb, [parsed as any]); return cb.apply(cb, [parsed as any]);
}; };
result.schema = arg1; result.schema = arg1;
return result; return result;
} }
export function doubleFn< export function doubleFn<
Arg1 extends ZodSchema, Arg1 extends ZodType,
Arg2 extends ZodSchema, Arg2 extends ZodType,
Callback extends (arg1: z.output<Arg1>, arg2: z.output<Arg2>) => any, Callback extends (arg1: z.output<Arg1>, arg2: z.output<Arg2>) => any,
>(arg1: Arg1, arg2: Arg2, cb: Callback) { >(arg1: Arg1, arg2: Arg2, cb: Callback) {
const result = function (input: z.input<typeof arg1>, input2: z.input<typeof arg2>): ReturnType<Callback> { const result = function (
const parsed = arg1.parse(input); input: z.input<typeof arg1>,
const parsed2 = arg2.parse(input2); input2: z.input<typeof arg2>,
return cb.apply(cb, [parsed as any, parsed2 as any]); ): ReturnType<Callback> {
}; const parsed = arg1.parse(input);
result.schema = arg1; const parsed2 = arg2.parse(input2);
return result; return cb.apply(cb, [parsed as any, parsed2 as any]);
};
result.schema = arg1;
return result;
} }

View File

@@ -1,8 +1,11 @@
{ {
"extends": "@tsconfig/node20/tsconfig.json", "extends": "@tsconfig/node20/tsconfig.json",
"compilerOptions": { "compilerOptions": {
"strict": true, "strict": true,
"module": "esnext", "module": "esnext",
"moduleResolution": "bundler", "moduleResolution": "bundler",
"paths": {
"@/*": ["./src/*"]
} }
} }
}

View File

@@ -1,5 +1,5 @@
{ {
"name": "@nestri/functions", "name": "@nestri/cloud-functions",
"module": "index.ts", "module": "index.ts",
"type": "module", "type": "module",
"private": true, "private": true,
@@ -22,6 +22,7 @@
"@aws-sdk/client-s3": "^3.806.0", "@aws-sdk/client-s3": "^3.806.0",
"@aws-sdk/client-sqs": "^3.806.0", "@aws-sdk/client-sqs": "^3.806.0",
"@nestri/core": "workspace:", "@nestri/core": "workspace:",
"@nestri/cloud-core": "workspace:",
"actor-core": "^0.8.0", "actor-core": "^0.8.0",
"hono": "^4.7.8", "hono": "^4.7.8",
"hono-openapi": "^0.4.8", "hono-openapi": "^0.4.8",

View File

@@ -3,9 +3,10 @@ import { Resource } from "sst";
import { logger } from "hono/logger"; import { logger } from "hono/logger";
import { subjects } from "../subjects"; import { subjects } from "../subjects";
import { handleDiscord } from "./utils"; import { handleDiscord } from "./utils";
import { Team } from "@nestri/cloud-core/team/index";
import { DiscordAdapter } from "./adapters"; import { DiscordAdapter } from "./adapters";
import { issuer } from "@openauthjs/openauth"; import { issuer } from "@openauthjs/openauth";
import { User } from "@nestri/core/user/index"; import { User } from "@nestri/cloud-core/user/index";
import { patchLogger } from "../utils/patch-logger"; import { patchLogger } from "../utils/patch-logger";
import type { KVNamespace } from "@cloudflare/workers-types"; import type { KVNamespace } from "@cloudflare/workers-types";
import { CloudflareStorage } from "@openauthjs/openauth/storage/cloudflare"; import { CloudflareStorage } from "@openauthjs/openauth/storage/cloudflare";
@@ -63,6 +64,7 @@ export default {
if (user) { if (user) {
try { try {
await Team.fromEmail(email);
const matching = await User.fromEmail(user.primary.email); const matching = await User.fromEmail(user.primary.email);
//Sign Up //Sign Up

View File

@@ -1,21 +1,20 @@
{ {
"name": "nestri", "name": "nestri",
"devDependencies": { "devDependencies": {
"@cloudflare/workers-types": "4.20240821.1", "@cloudflare/workers-types": "catalog:",
"@pulumi/pulumi": "^3.134.0", "@tsconfig/node22": "catalog:",
"@tsconfig/node22": "^22.0.1", "@types/bun": "catalog:",
"@types/aws-lambda": "8.10.147", "@types/node": "catalog:",
"prettier": "^3.2.5", "prettier": "^3.2.5",
"typescript": "^5.4.5" "typescript": "^5.4.5"
}, },
"engines": { "engines": {
"node": ">=18" "node": ">=18"
}, },
"packageManager": "bun@1.2.4", "packageManager": "bun@1.3.0",
"private": true, "private": true,
"scripts": { "scripts": {
"format": "prettier --write \"**/*.{ts,tsx,md}\"", "format": "prettier --write \"**/*.{ts,tsx,md}\""
"sso": "aws sso login --sso-session=nestri --no-browser --use-device-code"
}, },
"trustedDependencies": [ "trustedDependencies": [
"core-js-pure", "core-js-pure",
@@ -31,10 +30,15 @@
], ],
"catalog": { "catalog": {
"@openauthjs/openauth": "0.0.0-20250322224806", "@openauthjs/openauth": "0.0.0-20250322224806",
"steam-session": "1.9.3" "@cloudflare/workers-types": "4.20240821.1",
"@types/bun": "1.3.0",
"@types/node": "22.13.9",
"@tsconfig/node22": "22.0.2",
"steam-session": "1.9.3",
"zod": "^4.1.12"
} }
}, },
"dependencies": { "dependencies": {
"sst": "^3.17.13" "alchemy": "^0.75.1"
} }
} }

34
packages/.gitignore vendored Normal file
View File

@@ -0,0 +1,34 @@
# dependencies (bun install)
node_modules
# output
out
dist
*.tgz
# code coverage
coverage
*.lcov
# logs
logs
_.log
report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
# dotenv environment variable files
.env
.env.development.local
.env.test.local
.env.production.local
.env.local
# caches
.eslintcache
.cache
*.tsbuildinfo
# IntelliJ based IDEs
.idea
# Finder (MacOS) folder config
.DS_Store

15
packages/README.md Normal file
View File

@@ -0,0 +1,15 @@
# packages
To install dependencies:
```bash
bun install
```
To run:
```bash
bun run index.ts
```
This project was created using `bun init` in bun v1.2.23. [Bun](https://bun.com) is a fast all-in-one JavaScript runtime.

29
packages/bun.lock Normal file
View File

@@ -0,0 +1,29 @@
{
"lockfileVersion": 1,
"workspaces": {
"": {
"name": "packages",
"devDependencies": {
"@types/bun": "latest",
},
"peerDependencies": {
"typescript": "^5",
},
},
},
"packages": {
"@types/bun": ["@types/bun@1.3.0", "", { "dependencies": { "bun-types": "1.3.0" } }, "sha512-+lAGCYjXjip2qY375xX/scJeVRmZ5cY0wyHYyCYxNcdEXrQ4AOe3gACgd4iQ8ksOslJtW4VNxBJ8llUwc3a6AA=="],
"@types/node": ["@types/node@24.9.1", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-QoiaXANRkSXK6p0Duvt56W208du4P9Uye9hWLWgGMDTEoKPhuenzNcC4vGUmrNkiOKTlIrBoyNQYNpSwfEZXSg=="],
"@types/react": ["@types/react@19.2.2", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA=="],
"bun-types": ["bun-types@1.3.0", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-u8X0thhx+yJ0KmkxuEo9HAtdfgCBaM/aI9K90VQcQioAmkVp3SG3FkwWGibUFz3WdXAdcsqOcbU40lK7tbHdkQ=="],
"csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="],
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
"undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="],
}
}

34
packages/core/.gitignore vendored Normal file
View File

@@ -0,0 +1,34 @@
# dependencies (bun install)
node_modules
# output
out
dist
*.tgz
# code coverage
coverage
*.lcov
# logs
logs
_.log
report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
# dotenv environment variable files
.env
.env.development.local
.env.test.local
.env.production.local
.env.local
# caches
.eslintcache
.cache
*.tsbuildinfo
# IntelliJ based IDEs
.idea
# Finder (MacOS) folder config
.DS_Store

15
packages/core/README.md Normal file
View File

@@ -0,0 +1,15 @@
# core
To install dependencies:
```bash
bun install
```
To run:
```bash
bun run index.ts
```
This project was created using `bun init` in bun v1.2.23. [Bun](https://bun.com) is a fast all-in-one JavaScript runtime.

View File

@@ -0,0 +1,29 @@
{
"name": "@nestri/core",
"type": "module",
"version": "0.0.0",
"sideEffects": false,
"devDependencies": {
"@types/bun": "catalog:",
"@types/node": "catalog:",
"drizzle-kit": "^0.31.5"
},
"files": [
"./tsconfig.base.json"
],
"exports": {
"./*": "./src/*.ts",
"./tsconfig.base": "./tsconfig.base.json"
},
"peerDependencies": {
"typescript": "^5"
},
"dependencies": {
"drizzle-orm": "^0.44.6",
"ioredis": "^5.8.2",
"postgres": "^3.4.7",
"ulid": "^3.0.1",
"zod": "catalog:",
"zod-openapi": "^5.4.3"
}
}

View File

@@ -0,0 +1,89 @@
import { Log } from "./utils/log.js";
import { Context } from "./context.js";
import { ErrorCodes, VisibleError } from "./error.js";
export namespace Actor {
export interface Member {
type: "member";
properties: {
memberID: string;
};
}
export interface Team {
type: "team";
properties: {
teamID: string;
email: string;
};
}
export interface System {
type: "system";
properties: {
teamID: string;
memberID: string;
};
}
export interface Token {
type: "token";
properties: {
teamID: string;
tokenID: string;
};
}
export interface Public {
type: "public";
properties: {};
}
export type Info = Team | Member | Public | Token | System;
export const ctx = Context.create<Info>();
export function teamID() {
const actor = ctx.use();
if ("teamID" in actor.properties) return actor.properties.teamID;
throw new VisibleError(
"authentication",
ErrorCodes.Authentication.UNAUTHORIZED,
`You don't have permission to access this resource.`,
);
}
export function use() {
try {
return ctx.use();
} catch {
return { type: "public", properties: {} } as Public;
}
}
export function assert<T extends Info["type"]>(type: T) {
const actor = use();
if (actor.type !== type)
throw new VisibleError(
"authentication",
ErrorCodes.Authentication.UNAUTHORIZED,
`Actor is not "${type}"`,
);
return actor as Extract<Info, { type: T }>;
}
export function provide<
T extends Info["type"],
Next extends (...args: any) => any,
>(type: T, properties: Extract<Info, { type: T }>["properties"], fn: Next) {
return ctx.provide({ type, properties } as any, () =>
Log.provide(
{
actor: type,
...properties,
},
fn,
),
);
}
}

View File

@@ -0,0 +1,27 @@
import { teamTable } from "../team/team.sql.js";
import { pgTable, varchar } from "drizzle-orm/pg-core";
import { id, timestamps, ulid } from "../drizzle/types.js";
export const apiAppTable = pgTable("api_app", {
...id,
...timestamps,
name: varchar("name", { length: 255 }).notNull(),
secret: varchar("secret", { length: 255 }).notNull(),
redirectURI: varchar("redirect", { length: 255 }).notNull(),
teamID: ulid("team_id")
.references(() => teamTable.id, {
onDelete: "cascade",
})
.notNull(),
});
export const apiPersonalTokenTable = pgTable("api_personal_token", {
...id,
...timestamps,
token: varchar("token", { length: 255 }).notNull(),
teamID: ulid("team_id")
.references(() => teamTable.id, {
onDelete: "cascade",
})
.notNull(),
});

View File

@@ -0,0 +1,31 @@
import { z } from "zod";
import { Examples } from "../examples.js";
export namespace App {
export const Info = z
.object({
id: z.string().meta({
description: "The unique identifier of the App",
example: Examples.App.id,
}),
name: z.string().meta({
description: "The name of the App",
example: Examples.App.name,
}),
redirectURI: z.url().meta({
description: "Redirect endpoint URL of the App",
example: Examples.App.redirectURI,
}),
secret: z.string().meta({
description: "Auth2.0 client secret key of the App (obfuscated)",
example: Examples.App.secret,
}),
})
.meta({
ref: "App",
description: "Represents an App for OAuth2.0 authentication",
example: Examples.App,
});
export type Info = z.infer<typeof Info>;
}

View File

@@ -0,0 +1,7 @@
import { App as ApiApp } from "./app.js";
import { Personal as ApiPersonal } from "./personal.js";
export namespace Api {
export const App = ApiApp;
export const Personal = ApiPersonal;
}

View File

@@ -0,0 +1,45 @@
import { z } from "zod/v4";
import { eq } from "drizzle-orm";
import { fn } from "../utils/fn.js";
import { Examples } from "../examples.js";
import { Database } from "../drizzle/index.js";
import { apiPersonalTokenTable } from "./api.sql.js";
export namespace Personal {
export const Info = z
.object({
id: z.string().meta({
description: "The unique identifier of the personal access token",
example: Examples.Token.id,
}),
created: z.string().meta({
description: "The creation timestamp of the personal access token",
example: Examples.Token.created,
}),
token: z.string().meta({
description:
"A personal access token used to access the Nestri API. This is used to do things on behalf of the user.",
example: Examples.Token.token,
}),
})
.meta({
ref: "PersonalToken",
description: "Represents a personal access token for API authentication",
example: Examples.Token,
});
export type Info = z.infer<typeof Info>;
export const fromToken = fn(Info.shape.token, async (token: string) =>
Database.transaction(async (tx) => {
return tx
.select({
id: apiPersonalTokenTable.id,
teamID: apiPersonalTokenTable.teamID,
})
.from(apiPersonalTokenTable)
.where(eq(apiPersonalTokenTable.token, token))
.then((rows) => rows.at(0));
}),
);
}

View File

@@ -0,0 +1,21 @@
import { AsyncLocalStorage } from "node:async_hooks";
export namespace Context {
export class NotFound extends Error {}
export function create<T>() {
const storage = new AsyncLocalStorage<T>();
return {
use() {
const result = storage.getStore();
if (!result) {
throw new Error("No context available");
}
return result;
},
provide<R>(value: T, fn: () => R) {
return storage.run<R>(value, fn);
},
};
}
}

View File

@@ -0,0 +1,90 @@
import postgres from "postgres";
import { Context } from "../context.js";
import { ExtractTablesWithRelations } from "drizzle-orm";
import { PgTransaction, PgTransactionConfig } from "drizzle-orm/pg-core";
import { PostgresJsQueryResultHKT, drizzle } from "drizzle-orm/postgres-js";
export namespace Database {
const { DATABASE_URL } = process.env;
if (!DATABASE_URL) {
throw new Error("DATABASE_URL is not set");
}
const sql = postgres(DATABASE_URL, {
max: 20,
idle_timeout: 60,
});
const client = drizzle(sql);
export type Transaction = PgTransaction<
PostgresJsQueryResultHKT,
Record<string, never>,
ExtractTablesWithRelations<Record<string, never>>
>;
export type TxOrDb = Transaction | typeof client;
const TransactionContext = Context.create<{
tx: TxOrDb;
effects: (() => void | Promise<void>)[];
}>();
export async function use<T>(callback: (trx: TxOrDb) => Promise<T>) {
try {
const { tx } = TransactionContext.use();
return tx.transaction(callback);
} catch (err) {
if (err instanceof Context.NotFound) {
const effects: (() => void | Promise<void>)[] = [];
const result = await TransactionContext.provide(
{
effects,
tx: client,
},
() => callback(client),
);
await Promise.all(effects.map((x) => x()));
return result;
}
throw err;
}
}
export async function fn<Input, T>(
callback: (input: Input, trx: TxOrDb) => Promise<T>,
) {
return (input: Input) => use(async (tx) => callback(input, tx));
}
export async function effect(effect: () => any | Promise<any>) {
try {
const { effects } = TransactionContext.use();
effects.push(effect);
} catch {
await effect();
}
}
export async function transaction<T>(
callback: (tx: TxOrDb) => Promise<T>,
config?: PgTransactionConfig,
) {
try {
const { tx } = TransactionContext.use();
return callback(tx);
} catch (err) {
if (err instanceof Context.NotFound) {
const effects: (() => void | Promise<void>)[] = [];
const result = await client.transaction(async (tx) => {
return TransactionContext.provide({ tx, effects }, () =>
callback(tx),
);
}, config);
await Promise.all(effects.map((x) => x()));
return result;
}
throw err;
}
}
}

View File

@@ -0,0 +1,39 @@
import { char, timestamp as rawTs } from "drizzle-orm/pg-core";
export const ulid = (name: string) => char(name, { length: 26 + 5 });
export const id = {
get id() {
return ulid("id").primaryKey().notNull();
},
};
export const teamID = {
get id() {
return ulid("id").notNull();
},
get teamID() {
return ulid("team_id").notNull();
},
};
export const memberID = {
get id() {
return ulid("id").notNull();
},
get userID() {
return ulid("member_id").notNull();
},
};
export const utc = (name: string) =>
rawTs(name, {
withTimezone: true,
// mode: "date"
});
export const timestamps = {
timeCreated: utc("time_created").notNull().defaultNow(),
timeUpdated: utc("time_updated").notNull().defaultNow(),
timeDeleted: utc("time_deleted"),
};

145
packages/core/src/error.ts Normal file
View File

@@ -0,0 +1,145 @@
import { z } from "zod/v4";
/**
* Standard error response schema used for OpenAPI documentation
*/
export const ErrorResponse = z
.object({
type: z
.enum([
"validation",
"authentication",
"forbidden",
"not_found",
"already_exists",
"rate_limit",
"internal",
])
.meta({
description: "The error type category",
examples: ["validation", "authentication"],
}),
code: z.string().meta({
description: "Machine-readable error code identifier",
examples: ["invalid_parameter", "missing_required_field", "unauthorized"],
}),
message: z.string().meta({
description: "Human-readable error message",
examples: ["The request was invalid", "Authentication required"],
}),
param: z
.string()
.optional()
.meta({
description: "The parameter that caused the error (if applicable)",
examples: ["email", "user_id", "team_id"],
}),
details: z.any().optional().meta({
description: "Additional error context information",
}),
})
.meta({ ref: "ErrorResponse" });
export type ErrorResponseType = z.infer<typeof ErrorResponse>;
/**
* Standardized error codes for the API
*/
export const ErrorCodes = {
// Validation errors (400)
Validation: {
MISopenapiSING_REQUIRED_FIELD: "missing_required_field",
ALREADY_EXISTS: "resource_already_exists",
TEAM_ALREADY_EXISTS: "team_already_exists",
INVALID_PARAMETER: "invalid_parameter",
INVALID_FORMAT: "invalid_format",
INVALID_STATE: "invalid_state",
IN_USE: "resource_in_use",
},
// Authentication errors (401)
Authentication: {
UNAUTHORIZED: "unauthorized",
INVALID_TOKEN: "invalid_token",
EXPIRED_TOKEN: "expired_token",
INVALID_CREDENTIALS: "invalid_credentials",
},
// Permission errors (403)
Permission: {
FORBIDDEN: "forbidden",
INSUFFICIENT_PERMISSIONS: "insufficient_permissions",
ACCOUNT_RESTRICTED: "account_restricted",
},
// Resource not found errors (404)
NotFound: {
RESOURCE_NOT_FOUND: "resource_not_found",
},
// Rate limit errors (429)
RateLimit: {
TOO_MANY_REQUESTS: "too_many_requests",
QUOTA_EXCEEDED: "quota_exceeded",
},
// Server errors (500)
Server: {
INTERNAL_ERROR: "internal_error",
SERVICE_UNAVAILABLE: "service_unavailable",
DEPENDENCY_FAILURE: "dependency_failure",
},
};
/**
* Standard error that will be exposed to clients through API responses
*/
export class VisibleError extends Error {
constructor(
public type: ErrorResponseType["type"],
public code: string,
public message: string,
public param?: string,
public details?: any,
) {
super(message);
}
/**
* Convert this error to an HTTP status code
*/
public statusCode(): number {
switch (this.type) {
case "validation":
return 400;
case "authentication":
return 401;
case "forbidden":
return 403;
case "not_found":
return 404;
case "already_exists":
return 409;
case "rate_limit":
return 429;
case "internal":
return 500;
}
}
/**
* Convert this error to a standard response object
*/
public toResponse(): ErrorResponseType {
const response: ErrorResponseType = {
type: this.type,
code: this.code,
message: this.message,
};
if (this.param) response.param = this.param;
if (this.details) response.details = this.details;
return response;
}
}

View File

@@ -0,0 +1,138 @@
import { Prettify } from "../utils/types.js";
export namespace event {
export type Definition = {
type: string;
$input: any;
$output: any;
$metadata: any;
$payload: any;
create: (...args: any[]) => Promise<any>;
};
export function builder<
Metadata extends
| ((type: string, properties: any) => any)
| Parameters<Validator>[0],
Validator extends (schema: any) => (input: any) => any,
>(input: { validator: Validator; metadata?: Metadata }) {
const validator = input.validator;
const fn = function event<
Type extends string,
Schema extends Parameters<Validator>[0],
>(type: Type, schema: Schema) {
type MetadataOutput = Metadata extends (
type: string,
properties: any,
) => any
? ReturnType<Metadata>
: // @ts-expect-error
inferParser<Metadata>["out"];
type Payload = Prettify<{
type: Type;
properties: Parsed["out"];
metadata: MetadataOutput;
}>;
type Parsed = inferParser<Schema>;
type Create = Metadata extends (type: string, properties: any) => any
? (properties: Parsed["in"]) => Promise<Payload>
: (
properties: Parsed["in"],
// @ts-expect-error
metadata: inferParser<Metadata>["in"],
) => Promise<Payload>;
const validate = validator(schema);
async function create(properties: any, metadata?: any) {
metadata = input.metadata
? typeof input.metadata === "function"
? input.metadata(type, properties)
: input.metadata(metadata)
: {};
properties = validate(properties);
return {
type,
properties,
metadata,
};
}
return {
create: create as Create,
type,
$input: {} as Parsed["in"],
$output: {} as Parsed["out"],
$payload: {} as Payload,
$metadata: {} as MetadataOutput,
} satisfies Definition;
};
fn.coerce = <Events extends Definition>(
_events: Events | Events[],
raw: any,
): {
[K in Events["type"]]: Extract<Events, { type: K }>["$payload"];
}[Events["type"]] => {
return raw;
};
return fn;
}
// Taken from tRPC
type ParserZodEsque<TInput, TParsedInput> = {
_input: TInput;
_output: TParsedInput;
};
type ParserValibotEsque<TInput, TParsedInput> = {
_types?: {
input: TInput;
output: TParsedInput;
};
};
type ParserMyZodEsque<TInput> = {
parse: (input: any) => TInput;
};
type ParserSuperstructEsque<TInput> = {
create: (input: unknown) => TInput;
};
type ParserCustomValidatorEsque<TInput> = (
input: unknown,
) => Promise<TInput> | TInput;
type ParserYupEsque<TInput> = {
validateSync: (input: unknown) => TInput;
};
type ParserScaleEsque<TInput> = {
assert(value: unknown): asserts value is TInput;
};
export type ParserWithoutInput<TInput> =
| ParserCustomValidatorEsque<TInput>
| ParserMyZodEsque<TInput>
| ParserScaleEsque<TInput>
| ParserSuperstructEsque<TInput>
| ParserYupEsque<TInput>;
export type ParserWithInputOutput<TInput, TParsedInput> =
| ParserZodEsque<TInput, TParsedInput>
| ParserValibotEsque<TInput, TParsedInput>;
export type Parser =
| ParserWithInputOutput<any, any>
| ParserWithoutInput<any>;
export type inferParser<TParser extends Parser> =
TParser extends ParserWithInputOutput<infer $TIn, infer $TOut>
? {
in: $TIn;
out: $TOut;
}
: TParser extends ParserWithoutInput<infer $InOut>
? {
in: $InOut;
out: $InOut;
}
: never;
}

View File

@@ -0,0 +1,12 @@
import { event } from "./event.js";
import { Actor } from "../actor.js";
import { ZodValidator } from "./validator.js";
export const createEvent = event.builder({
validator: ZodValidator,
metadata() {
return {
actor: Actor.use(),
};
},
});

View File

@@ -0,0 +1,9 @@
import { type ZodType, z } from "zod/v4";
export function ZodValidator<Schema extends ZodType>(
schema: Schema,
): (input: z.input<Schema>) => z.output<Schema> {
return (input) => {
return schema.parse(input);
};
}

View File

@@ -0,0 +1,32 @@
import { prefixes } from "./utils/id.js";
export namespace Examples {
export const Id = (prefix: keyof typeof prefixes) =>
`${prefixes[prefix]}_XXXXXXXXXXXXXXXXXXXXXXXXX`;
export const Team = {
id: Id("team"),
email: "email@example.com",
polarCustomerID: "0bfcb712-df13-4454-81a8-fbee66eddca4",
};
export const Member = {
id: Id("member"),
name: "John Doe",
role: "adult" as const, // Role on the team, adult or child
teamID: Team.id, // FK to Teams
};
export const Token = {
id: Id("apiPersonal"),
token: "nst_test_******XXXX",
created: new Date("2024-06-29 19:36:19.000"),
};
export const App = {
id: Id("apiApp"),
secret: "sec_******XXXX",
name: "Example App",
redirectURI: "https://web.example.com/v1/callback",
};
}

View File

@@ -0,0 +1,32 @@
import { z } from "zod/v4";
import { Examples } from "../examples.js";
import { MemberRole } from "./member.sql.js";
export namespace Member {
export const Info = z
.object({
id: z.string().meta({
description: "The unique identifier of the member",
example: Examples.Member.id,
}),
name: z.string().meta({
description: "The name of the member",
example: Examples.Member.name,
}),
teamID: z.string().meta({
description: "The unique identifier of the team this member belongs to",
example: Examples.Member.teamID,
}),
role: z.enum(MemberRole.enumValues).meta({
description: "The role of the member within the team",
example: Examples.Member.role,
}),
})
.meta({
ref: "Member",
description: "Represents a member of a team",
example: Examples.Member,
});
export type Info = z.infer<typeof Info>;
}

View File

@@ -0,0 +1,15 @@
import { teamTable } from "../team/team.sql.js";
import { pgEnum, pgTable } from "drizzle-orm/pg-core";
import { id, ulid, timestamps } from "../drizzle/types.js";
export const MemberRole = pgEnum("role", ["adult", "child"]);
export const memberTable = pgTable("members", {
...id,
...timestamps,
name: ulid("name").notNull(),
teamID: ulid("team_id").references(() => teamTable.id, {
onDelete: "cascade",
}),
role: MemberRole("role").notNull(),
});

View File

@@ -0,0 +1,20 @@
import { z } from "zod/v4";
import { fn } from "../utils/fn.js";
import { Polar as PolarSdk } from "@polar-sh/sdk";
import { type Customer } from "@polar-sh/sdk/models/components/customer.js";
export namespace Polar {
export const client = new PolarSdk({
accessToken: process.env.POLAR_API_KEY || "",
server: process.env.APP_STAGE !== "production" ? "sandbox" : "production",
});
export const fromUserEmail = fn(z.email(), async (email) => {
const customers = await client.customers.list({ email });
if (customers.result.items.length === 0) {
return await client.customers.create({ email });
}
return customers.result.items[0] as Customer;
});
}

View File

@@ -0,0 +1,47 @@
import { Log } from "../utils/log.js";
import { Redis, type RedisOptions } from "ioredis";
export {
Redis,
Cluster,
type Callback,
type RedisOptions,
type Result,
type RedisCommander,
} from "ioredis";
const defaultOptions: Partial<RedisOptions> = {
retryStrategy: (times: number) => {
const delay = Math.min(times * 50, 1000);
return delay;
},
maxRetriesPerRequest: 20,
};
const logger = Log.create({ namespace: "redis" });
export namespace RedisClient {
export function create(
options: RedisOptions,
handlers?: { onError?: (err: Error) => void },
) {
const client = new Redis({
...defaultOptions,
...options,
});
client.on("error", (error) => {
if (handlers?.onError) {
handlers.onError(error);
} else {
logger.error(
new Error(
`Redis client error:${error} at keyPrefix:${options.keyPrefix}`,
),
);
}
});
return client;
}
}

View File

@@ -0,0 +1,755 @@
import {
Client,
type Redis,
type Callback,
type RedisOptions,
type Result,
} from "./client.js";
import { z } from "zod/v4";
import { ulid } from "ulid";
import { Log } from "../utils/log.js";
export interface MessageCatalogSchema {
[key: string]: z.ZodFirstPartySchemaTypes; //| z.ZodDiscriminatedUnion<any, any>;
}
export type MessageCatalogKey<TMessageCatalog extends MessageCatalogSchema> =
keyof TMessageCatalog;
export type MessageCatalogValue<
TMessageCatalog extends MessageCatalogSchema,
TKey extends MessageCatalogKey<TMessageCatalog>,
> = z.infer<TMessageCatalog[TKey]>;
export type AnyMessageCatalog = MessageCatalogSchema;
export type QueueItem<TMessageCatalog extends MessageCatalogSchema> = {
id: string;
job: MessageCatalogKey<TMessageCatalog>;
item: MessageCatalogValue<
TMessageCatalog,
MessageCatalogKey<TMessageCatalog>
>;
visibilityTimeoutMs: number;
attempt: number;
timestamp: Date;
deduplicationKey?: string;
};
export type AnyQueueItem = {
id: string;
job: string;
item: any;
visibilityTimeoutMs: number;
attempt: number;
timestamp: Date;
deduplicationKey?: string;
};
export class SimpleQueue<TMessageCatalog extends MessageCatalogSchema> {
name: string;
private redis: Redis;
private schema: TMessageCatalog;
private logger: ReturnType<typeof Log.create>;
constructor({
name,
schema,
redisOptions,
}: {
name: string;
schema: TMessageCatalog;
redisOptions: RedisOptions;
}) {
this.name = name;
this.logger = Log.create({ namespace: "SimpleQueue" });
this.redis = Client.create(
{
...redisOptions,
keyPrefix: `${redisOptions.keyPrefix ?? ""}{queue:${name}:}`,
retryStrategy(times) {
const delay = Math.min(times * 50, 1000);
return delay;
},
maxRetriesPerRequest: 20,
},
{
onError: (error) => {
this.logger.error(`RedisWorker queue redis client error:`, {
error,
keyPrefix: redisOptions.keyPrefix,
});
},
},
);
this.#registerCommands();
this.schema = schema;
}
async cancel(cancellationKey: string): Promise<void> {
await this.redis.set(
`cancellationKey:${cancellationKey}`,
"1",
"EX",
60 * 60 * 24,
); // 1 day
}
async enqueue({
id,
job,
item,
attempt,
availableAt,
visibilityTimeoutMs,
cancellationKey,
}: {
id?: string;
job: MessageCatalogKey<TMessageCatalog>;
item: MessageCatalogValue<
TMessageCatalog,
MessageCatalogKey<TMessageCatalog>
>;
attempt?: number;
availableAt?: Date;
visibilityTimeoutMs: number;
cancellationKey?: string;
}): Promise<void> {
try {
const score = availableAt ? availableAt.getTime() : Date.now();
const deduplicationKey = ulid();
const serializedItem = JSON.stringify({
job,
item,
visibilityTimeoutMs,
attempt,
deduplicationKey,
});
const result = cancellationKey
? await this.redis.enqueueItemWithCancellationKey(
`queue`,
`items`,
`cancellationKey:${cancellationKey}`,
id ?? ulid(),
score,
serializedItem,
)
: await this.redis.enqueueItem(
`queue`,
`items`,
id ?? ulid(),
score,
serializedItem,
);
if (result !== 1) {
throw new Error("Enqueue operation failed");
}
} catch (e) {
this.logger.error(`SimpleQueue ${this.name}.enqueue(): error enqueuing`, {
queue: this.name,
error: e,
id,
item,
});
throw e;
}
}
async enqueueOnce({
id,
job,
item,
attempt,
availableAt,
visibilityTimeoutMs,
}: {
id: string;
job: MessageCatalogKey<TMessageCatalog>;
item: MessageCatalogValue<
TMessageCatalog,
MessageCatalogKey<TMessageCatalog>
>;
attempt?: number;
availableAt?: Date;
visibilityTimeoutMs: number;
}): Promise<boolean> {
if (!id) {
throw new Error("enqueueOnce requires an id");
}
try {
const score = availableAt ? availableAt.getTime() : Date.now();
const deduplicationKey = ulid();
const serializedItem = JSON.stringify({
job,
item,
visibilityTimeoutMs,
attempt,
deduplicationKey,
});
const result = await this.redis.enqueueItemOnce(
`queue`,
`items`,
id,
score,
serializedItem,
);
// 1 if inserted, 0 if already exists
return result === 1;
} catch (e) {
this.logger.error(
`SimpleQueue ${this.name}.enqueueOnce(): error enqueuing`,
{
queue: this.name,
error: e,
id,
item,
},
);
throw e;
}
}
async dequeue(count: number = 1): Promise<Array<QueueItem<TMessageCatalog>>> {
const now = Date.now();
try {
const results = await this.redis.dequeueItems(
`queue`,
`items`,
now,
count,
);
if (!results || results.length === 0) {
return [];
}
const dequeuedItems: Array<QueueItem<TMessageCatalog>> = [];
for (const [id, serializedItem, score] of results) {
const parsedItem = JSON.parse(serializedItem) as any;
if (typeof parsedItem.job !== "string") {
this.logger.error(`Invalid item in queue`, {
queue: this.name,
id,
item: parsedItem,
});
continue;
}
const timestamp = new Date(Number(score));
const schema = this.schema[parsedItem.job];
if (!schema) {
this.logger.error(`Invalid item in queue, schema not found`, {
queue: this.name,
id,
item: parsedItem,
job: parsedItem.job,
timestamp,
availableJobs: Object.keys(this.schema),
});
continue;
}
const validatedItem = schema.safeParse(parsedItem.item);
if (!validatedItem.success) {
this.logger.error("Invalid item in queue", {
queue: this.name,
id,
item: parsedItem,
errors: validatedItem.error,
attempt: parsedItem.attempt,
timestamp,
});
continue;
}
const visibilityTimeoutMs = parsedItem.visibilityTimeoutMs as number;
dequeuedItems.push({
id,
job: parsedItem.job,
item: validatedItem.data,
visibilityTimeoutMs,
attempt: parsedItem.attempt ?? 0,
timestamp,
deduplicationKey: parsedItem.deduplicationKey,
});
}
return dequeuedItems;
} catch (e) {
this.logger.error(`SimpleQueue ${this.name}.dequeue(): error dequeuing`, {
queue: this.name,
error: e,
count,
});
throw e;
}
}
async ack(id: string, deduplicationKey?: string): Promise<void> {
try {
const result = await this.redis.ackItem(
`queue`,
`items`,
id,
deduplicationKey ?? "",
);
if (result !== 1) {
this.logger.debug(
`SimpleQueue ${this.name}.ack(): ack operation returned ${result}. This means it was not removed from the queue.`,
{
queue: this.name,
id,
deduplicationKey,
result,
},
);
}
} catch (e) {
this.logger.error(
`SimpleQueue ${this.name}.ack(): error acknowledging item`,
{
queue: this.name,
error: e,
id,
deduplicationKey,
},
);
throw e;
}
}
async reschedule(id: string, availableAt: Date): Promise<void> {
await this.redis.zadd(`queue`, "XX", availableAt.getTime(), id);
}
async size({
includeFuture = false,
}: { includeFuture?: boolean } = {}): Promise<number> {
try {
if (includeFuture) {
// If includeFuture is true, return the total count of all items
return await this.redis.zcard(`queue`);
} else {
// If includeFuture is false, return the count of items available now
const now = Date.now();
return await this.redis.zcount(`queue`, "-inf", now);
}
} catch (e) {
this.logger.error(
`SimpleQueue ${this.name}.size(): error getting queue size`,
{
queue: this.name,
error: e,
includeFuture,
},
);
throw e;
}
}
async getJob(id: string): Promise<QueueItem<TMessageCatalog> | null> {
const result = await this.redis.getJob(`queue`, `items`, id);
if (!result) {
return null;
}
const [_, score, serializedItem] = result;
const item = JSON.parse(serializedItem) as QueueItem<TMessageCatalog>;
return {
id,
job: item.job,
item: item.item,
visibilityTimeoutMs: item.visibilityTimeoutMs,
attempt: item.attempt ?? 0,
timestamp: new Date(Number(score)),
deduplicationKey: item.deduplicationKey ?? undefined,
};
}
async moveToDeadLetterQueue(id: string, errorMessage: string): Promise<void> {
try {
this.logger.debug(
`SimpleQueue ${this.name}.moveToDeadLetterQueue(): moving item to DLQ`,
{
queue: this.name,
id,
errorMessage,
},
);
const result = await this.redis.moveToDeadLetterQueue(
`queue`,
`items`,
`dlq`,
`dlq:items`,
id,
errorMessage,
);
if (result !== 1) {
throw new Error("Move to Dead Letter Queue operation failed");
}
} catch (e) {
this.logger.error(
`SimpleQueue ${this.name}.moveToDeadLetterQueue(): error moving item to DLQ`,
{
queue: this.name,
error: e,
id,
errorMessage,
},
);
throw e;
}
}
async sizeOfDeadLetterQueue(): Promise<number> {
try {
return await this.redis.zcard(`dlq`);
} catch (e) {
this.logger.error(
`SimpleQueue ${this.name}.dlqSize(): error getting DLQ size`,
{
queue: this.name,
error: e,
},
);
throw e;
}
}
async redriveFromDeadLetterQueue(id: string): Promise<void> {
try {
const result = await this.redis.redriveFromDeadLetterQueue(
`queue`,
`items`,
`dlq`,
`dlq:items`,
id,
);
if (result !== 1) {
throw new Error("Redrive from Dead Letter Queue operation failed");
}
} catch (e) {
this.logger.error(
`SimpleQueue ${this.name}.redriveFromDeadLetterQueue(): error redriving item from DLQ`,
{
queue: this.name,
error: e,
id,
},
);
throw e;
}
}
async close(): Promise<void> {
await this.redis.quit();
}
#registerCommands() {
this.redis.defineCommand("enqueueItem", {
numberOfKeys: 2,
lua: `
local queue = KEYS[1]
local items = KEYS[2]
local id = ARGV[1]
local score = ARGV[2]
local serializedItem = ARGV[3]
redis.call('ZADD', queue, score, id)
redis.call('HSET', items, id, serializedItem)
return 1
`,
});
this.redis.defineCommand("enqueueItemWithCancellationKey", {
numberOfKeys: 3,
lua: `
local queue = KEYS[1]
local items = KEYS[2]
local cancellationKey = KEYS[3]
local id = ARGV[1]
local score = ARGV[2]
local serializedItem = ARGV[3]
-- if the cancellation key exists, return 1
if redis.call('EXISTS', cancellationKey) == 1 then
return 1
end
redis.call('ZADD', queue, score, id)
redis.call('HSET', items, id, serializedItem)
return 1
`,
});
this.redis.defineCommand("dequeueItems", {
numberOfKeys: 2,
lua: `
local queue = KEYS[1]
local items = KEYS[2]
local now = tonumber(ARGV[1])
local count = tonumber(ARGV[2])
local result = redis.call('ZRANGEBYSCORE', queue, '-inf', now, 'WITHSCORES', 'LIMIT', 0, count)
if #result == 0 then
return {}
end
local dequeued = {}
for i = 1, #result, 2 do
local id = result[i]
local score = tonumber(result[i + 1])
if score > now then
break
end
local serializedItem = redis.call('HGET', items, id)
if serializedItem then
local item = cjson.decode(serializedItem)
local visibilityTimeoutMs = tonumber(item.visibilityTimeoutMs)
local invisibleUntil = now + visibilityTimeoutMs
redis.call('ZADD', queue, invisibleUntil, id)
table.insert(dequeued, {id, serializedItem, score})
else
-- Remove the orphaned queue entry if no corresponding item exists
redis.call('ZREM', queue, id)
end
end
return dequeued
`,
});
this.redis.defineCommand("getJob", {
numberOfKeys: 2,
lua: `
local queue = KEYS[1]
local items = KEYS[2]
local jobId = ARGV[1]
local serializedItem = redis.call('HGET', items, jobId)
if serializedItem == false then
return nil
end
-- get the score from the queue sorted set
local score = redis.call('ZSCORE', queue, jobId)
return { jobId, score, serializedItem }
`,
});
this.redis.defineCommand("ackItem", {
numberOfKeys: 2,
lua: `
local queueKey = KEYS[1]
local itemsKey = KEYS[2]
local id = ARGV[1]
local deduplicationKey = ARGV[2]
-- Get the item from the hash
local item = redis.call('HGET', itemsKey, id)
if not item then
return -1
end
-- Only check deduplicationKey if a non-empty one was passed in
if deduplicationKey and deduplicationKey ~= "" then
local success, parsed = pcall(cjson.decode, item)
if success then
if parsed.deduplicationKey and parsed.deduplicationKey ~= deduplicationKey then
return 0
end
end
end
-- Remove from sorted set and hash
redis.call('ZREM', queueKey, id)
redis.call('HDEL', itemsKey, id)
return 1
`,
});
this.redis.defineCommand("moveToDeadLetterQueue", {
numberOfKeys: 4,
lua: `
local queue = KEYS[1]
local items = KEYS[2]
local dlq = KEYS[3]
local dlqItems = KEYS[4]
local id = ARGV[1]
local errorMessage = ARGV[2]
local item = redis.call('HGET', items, id)
if not item then
return 0
end
local parsedItem = cjson.decode(item)
parsedItem.errorMessage = errorMessage
local time = redis.call('TIME')
local now = tonumber(time[1]) * 1000 + math.floor(tonumber(time[2]) / 1000)
redis.call('ZREM', queue, id)
redis.call('HDEL', items, id)
redis.call('ZADD', dlq, now, id)
redis.call('HSET', dlqItems, id, cjson.encode(parsedItem))
return 1
`,
});
this.redis.defineCommand("redriveFromDeadLetterQueue", {
numberOfKeys: 4,
lua: `
local queue = KEYS[1]
local items = KEYS[2]
local dlq = KEYS[3]
local dlqItems = KEYS[4]
local id = ARGV[1]
local item = redis.call('HGET', dlqItems, id)
if not item then
return 0
end
local parsedItem = cjson.decode(item)
parsedItem.errorMessage = nil
local time = redis.call('TIME')
local now = tonumber(time[1]) * 1000 + math.floor(tonumber(time[2]) / 1000)
redis.call('ZREM', dlq, id)
redis.call('HDEL', dlqItems, id)
redis.call('ZADD', queue, now, id)
redis.call('HSET', items, id, cjson.encode(parsedItem))
return 1
`,
});
this.redis.defineCommand("enqueueItemOnce", {
numberOfKeys: 2,
lua: `
local queue = KEYS[1]
local items = KEYS[2]
local id = ARGV[1]
local score = ARGV[2]
local serializedItem = ARGV[3]
-- Only add if not exists
local added = redis.call('HSETNX', items, id, serializedItem)
if added == 1 then
redis.call('ZADD', queue, 'NX', score, id)
return 1
else
return 0
end
`,
});
}
}
declare module "ioredis" {
interface RedisCommander<Context> {
enqueueItem(
//keys
queue: string,
items: string,
//args
id: string,
score: number,
serializedItem: string,
callback?: Callback<number>,
): Result<number, Context>;
enqueueItemWithCancellationKey(
//keys
queue: string,
items: string,
cancellationKey: string,
//args
id: string,
score: number,
serializedItem: string,
callback?: Callback<number>,
): Result<number, Context>;
dequeueItems(
//keys
queue: string,
items: string,
//args
now: number,
count: number,
callback?: Callback<Array<[string, string, string]>>,
): Result<Array<[string, string, string]>, Context>;
ackItem(
queue: string,
items: string,
id: string,
deduplicationKey: string,
callback?: Callback<number>,
): Result<number, Context>;
redriveFromDeadLetterQueue(
queue: string,
items: string,
dlq: string,
dlqItems: string,
id: string,
callback?: Callback<number>,
): Result<number, Context>;
moveToDeadLetterQueue(
queue: string,
items: string,
dlq: string,
dlqItems: string,
id: string,
errorMessage: string,
callback?: Callback<number>,
): Result<number, Context>;
enqueueItemOnce(
queue: string,
items: string,
id: string,
score: number,
serializedItem: string,
callback?: Callback<number>,
): Result<number, Context>;
getJob(
queue: string,
items: string,
id: string,
callback?: Callback<[string, string, string] | null>,
): Result<[string, string, string] | null, Context>;
}
}

View File

@@ -0,0 +1,3 @@
export * from "./json.js";
export * from "./types.js";
export * from "./schema.js";

View File

@@ -0,0 +1,118 @@
import { z } from "zod/v4";
const LiteralSchema = z.union([z.string(), z.number(), z.boolean(), z.null()]);
type Literal = z.infer<typeof LiteralSchema>;
export type DeserializedJson =
| Literal
| { [key: string]: DeserializedJson }
| DeserializedJson[];
export const DeserializedJsonSchema: z.ZodType<DeserializedJson> = z.lazy(() =>
z.union([
LiteralSchema,
z.array(DeserializedJsonSchema),
z.record(z.string(), DeserializedJsonSchema),
]),
);
const SerializableSchema = z.union([
z.string(),
z.number(),
z.boolean(),
z.null(),
z.date(),
z.undefined(),
z.symbol(),
]);
type Serializable = z.infer<typeof SerializableSchema>;
export type SerializableJson =
| Serializable
| { [key: string]: SerializableJson }
| SerializableJson[];
export const SerializableJsonSchema: z.ZodType<SerializableJson> = z.lazy(() =>
z.union([
SerializableSchema,
z.array(SerializableJsonSchema),
z.record(z.string(), SerializableJsonSchema),
]),
);
/**
* JSON Schema type definition - compatible with JSON Schema Draft 7
* Based on the JSONSchema7 type from @types/json-schema but defined inline to avoid import issues
*/
export interface JSONSchema {
$id?: string;
$ref?: string;
$schema?: string;
$comment?: string;
type?: JSONSchemaType | JSONSchemaType[];
enum?: any[];
const?: any;
// Number/Integer validations
multipleOf?: number;
maximum?: number;
exclusiveMaximum?: number;
minimum?: number;
exclusiveMinimum?: number;
// String validations
maxLength?: number;
minLength?: number;
pattern?: string;
format?: string;
// Array validations
items?: JSONSchema | JSONSchema[];
additionalItems?: JSONSchema | boolean;
maxItems?: number;
minItems?: number;
uniqueItems?: boolean;
contains?: JSONSchema;
// Object validations
maxProperties?: number;
minProperties?: number;
required?: string[];
properties?: Record<string, JSONSchema>;
patternProperties?: Record<string, JSONSchema>;
additionalProperties?: JSONSchema | boolean;
dependencies?: Record<string, JSONSchema | string[]>;
propertyNames?: JSONSchema;
// Conditional schemas
if?: JSONSchema;
then?: JSONSchema;
else?: JSONSchema;
// Boolean logic
allOf?: JSONSchema[];
anyOf?: JSONSchema[];
oneOf?: JSONSchema[];
not?: JSONSchema;
// Metadata
title?: string;
description?: string;
default?: any;
readOnly?: boolean;
writeOnly?: boolean;
examples?: any[];
// Additional properties for extensibility
[key: string]: any;
}
export type JSONSchemaType =
| "string"
| "number"
| "integer"
| "boolean"
| "object"
| "array"
| "null";

View File

@@ -0,0 +1,152 @@
export type SchemaZodEsque<TInput, TParsedInput> = {
_input: TInput;
_output: TParsedInput;
};
export function isSchemaZodEsque<TInput, TParsedInput>(
schema: Schema,
): schema is SchemaZodEsque<TInput, TParsedInput> {
return (
typeof schema === "object" &&
"_def" in schema &&
"parse" in schema &&
"parseAsync" in schema &&
"safeParse" in schema
);
}
export type SchemaValibotEsque<TInput, TParsedInput> = {
schema: {
_types?: {
input: TInput;
output: TParsedInput;
};
};
};
export function isSchemaValibotEsque<TInput, TParsedInput>(
schema: Schema,
): schema is SchemaValibotEsque<TInput, TParsedInput> {
return typeof schema === "object" && "_types" in schema;
}
export type SchemaArkTypeEsque<TInput, TParsedInput> = {
inferIn: TInput;
infer: TParsedInput;
};
export function isSchemaArkTypeEsque<TInput, TParsedInput>(
schema: Schema,
): schema is SchemaArkTypeEsque<TInput, TParsedInput> {
return (
typeof schema === "object" && "_inferIn" in schema && "_infer" in schema
);
}
export type SchemaMyZodEsque<TInput> = {
parse: (input: any) => TInput;
};
export type SchemaSuperstructEsque<TInput> = {
create: (input: unknown) => TInput;
};
export type SchemaCustomValidatorEsque<TInput> = (
input: unknown,
) => Promise<TInput> | TInput;
export type SchemaYupEsque<TInput> = {
validateSync: (input: unknown) => TInput;
};
export type SchemaScaleEsque<TInput> = {
assert(value: unknown): asserts value is TInput;
};
export type SchemaWithoutInput<TInput> =
| SchemaCustomValidatorEsque<TInput>
| SchemaMyZodEsque<TInput>
| SchemaScaleEsque<TInput>
| SchemaSuperstructEsque<TInput>
| SchemaYupEsque<TInput>;
export type SchemaWithInputOutput<TInput, TParsedInput> =
| SchemaZodEsque<TInput, TParsedInput>
| SchemaValibotEsque<TInput, TParsedInput>
| SchemaArkTypeEsque<TInput, TParsedInput>;
export type Schema = SchemaWithInputOutput<any, any> | SchemaWithoutInput<any>;
export type inferSchema<TSchema extends Schema> =
TSchema extends SchemaWithInputOutput<infer $TIn, infer $TOut>
? {
in: $TIn;
out: $TOut;
}
: TSchema extends SchemaWithoutInput<infer $InOut>
? {
in: $InOut;
out: $InOut;
}
: never;
export type inferSchemaIn<
TSchema extends Schema | undefined,
TDefault = unknown,
> = TSchema extends Schema ? inferSchema<TSchema>["in"] : TDefault;
export type inferSchemaOut<
TSchema extends Schema | undefined,
TDefault = unknown,
> = TSchema extends Schema ? inferSchema<TSchema>["out"] : TDefault;
export type SchemaParseFn<TType> = (value: unknown) => Promise<TType> | TType;
export type AnySchemaParseFn = SchemaParseFn<any>;
export function getSchemaParseFn<TType>(
procedureParser: Schema,
): SchemaParseFn<TType> {
const parser = procedureParser as any;
if (typeof parser === "function" && typeof parser.assert === "function") {
// ParserArkTypeEsque - arktype schemas shouldn't be called as a function because they return a union type instead of throwing
return parser.assert.bind(parser);
}
if (typeof parser === "function") {
// ParserValibotEsque (>= v0.31.0)
// ParserCustomValidatorEsque
return parser;
}
if (typeof parser.parseAsync === "function") {
// ParserZodEsque
return parser.parseAsync.bind(parser);
}
if (typeof parser.parse === "function") {
// ParserZodEsque
// ParserValibotEsque (< v0.13.0)
return parser.parse.bind(parser);
}
if (typeof parser.validateSync === "function") {
// ParserYupEsque
return parser.validateSync.bind(parser);
}
if (typeof parser.create === "function") {
// ParserSuperstructEsque
return parser.create.bind(parser);
}
if (typeof parser.assert === "function") {
// ParserScaleEsque
return (value) => {
parser.assert(value);
return value as TType;
};
}
throw new Error("Could not find a validator fn");
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,100 @@
import { z } from "zod/v4";
import { fn } from "../utils/fn.js";
import { Examples } from "../examples.js";
import { teamTable } from "./team.sql.js";
import { createID } from "../utils/id.js";
import { Polar } from "../polar/index.js";
import { Database } from "../drizzle/index.js";
import { VisibleError, ErrorCodes } from "../error.js";
import { and, asc, eq, isNull, sql } from "drizzle-orm";
export namespace Team {
export const Info = z
.object({
id: z.string().meta({
description: "The unique identifier of the team",
example: Examples.Team.id,
}),
email: z.email().meta({
description: "The contact email address for the team",
example: Examples.Team.email,
}),
polarCustomerID: z.string().nullable().meta({
description: "Associated Polar.sh customer identifier",
example: Examples.Team.polarCustomerID,
}),
})
.meta({
ref: "Team",
description: "Represents a team entity",
example: Examples.Team,
});
export type Info = z.infer<typeof Info>;
export class TeamExistsError extends VisibleError {
constructor(email: string) {
super(
"already_exists",
ErrorCodes.Validation.ALREADY_EXISTS,
`A team with this email ${email} already exists`,
);
}
}
export const create = fn(
Info.omit({ polarCustomerID: true }).partial({ id: true }),
async (input) => {
const userID = createID("team");
const polarCustomerID = await Polar.fromUserEmail(input.email).then(
(polarUser) => polarUser?.id || null,
);
const id = input.id ?? userID;
await Database.transaction((tx) => {
return tx
.insert(teamTable)
.values({
id,
email: input.email,
polarCustomerID,
})
.onConflictDoUpdate({
target: teamTable.email,
set: {
timeDeleted: null,
id: sql.raw(`excluded.${teamTable.id.name}`),
polarCustomerID: sql.raw(
`excluded.${teamTable.polarCustomerID.name}`,
),
},
});
});
return id;
},
);
export const fromEmail = fn(Info.shape.email, async (email) =>
Database.transaction(async (tx) =>
tx
.select()
.from(teamTable)
.where(and(eq(teamTable.email, email), isNull(teamTable.timeDeleted)))
.orderBy(asc(teamTable.timeCreated))
.execute()
.then((rows) => rows.map(serialize).at(0)),
),
);
export function serialize(
input: typeof teamTable.$inferSelect,
): z.infer<typeof Info> {
return {
id: input.id,
email: input.email,
polarCustomerID: input.polarCustomerID,
};
}
}

View File

@@ -0,0 +1,9 @@
import { id, timestamps } from "../drizzle/types.js";
import { pgTable, varchar } from "drizzle-orm/pg-core";
export const teamTable = pgTable("team", {
...id,
...timestamps,
email: varchar("email", { length: 255 }).unique().notNull(),
polarCustomerID: varchar("polar_customer_id", { length: 255 }),
});

View File

@@ -0,0 +1,30 @@
import { type ZodType, z } from "zod/v4";
export function fn<
Arg1 extends ZodType,
Callback extends (arg1: z.output<Arg1>) => any,
>(arg1: Arg1, cb: Callback) {
const result = function (input: z.input<typeof arg1>): ReturnType<Callback> {
const parsed = arg1.parse(input);
return cb.apply(cb, [parsed as any]);
};
result.schema = arg1;
return result;
}
export function doubleFn<
Arg1 extends ZodType,
Arg2 extends ZodType,
Callback extends (arg1: z.output<Arg1>, arg2: z.output<Arg2>) => any,
>(arg1: Arg1, arg2: Arg2, cb: Callback) {
const result = function (
input: z.input<typeof arg1>,
input2: z.input<typeof arg2>,
): ReturnType<Callback> {
const parsed = arg1.parse(input);
const parsed2 = arg2.parse(input2);
return cb.apply(cb, [parsed as any, parsed2 as any]);
};
result.schema = arg1;
return result;
}

View File

@@ -0,0 +1,23 @@
import { ulid } from "ulid";
export const prefixes = {
apiApp: "app",
apiSecret: "sec",
apiPersonal: "pat",
team: "team",
member: "mebr",
} as const;
/**
* Generates a unique identifier by concatenating a predefined prefix with a ULID.
*
* Given a key from the predefined prefixes mapping (e.g.,"team", "member", "steam"),
* this function retrieves the corresponding prefix and combines it with a ULID using an underscore
* as a separator. The resulting identifier is formatted as "prefix_ulid".
*
* @param prefix - A key from the prefixes mapping.
* @returns A unique identifier string.
*/
export function createID(prefix: keyof typeof prefixes): string {
return [prefixes[prefix], ulid()].join("_");
}

View File

@@ -0,0 +1,2 @@
export * from "./log.js";
export * from "./types.js";

View File

@@ -0,0 +1,76 @@
import { Context } from "../context.js";
export namespace Log {
const ctx = Context.create<{
tags: Record<string, any>;
}>();
export function create(tags?: Record<string, any>) {
tags = tags || {};
const result = {
info(msg: string, extra?: Record<string, any>) {
const prefix = Object.entries({
...use().tags,
...tags,
...extra,
})
.map(([key, value]) => `${key}=${value}`)
.join(" ");
console.log(prefix, msg);
return result;
},
warn(msg: string, extra?: Record<string, any>) {
const prefix = Object.entries({
...use().tags,
...tags,
...extra,
})
.map(([key, value]) => `${key}=${value}`)
.join(" ");
console.warn(prefix, msg);
return result;
},
error(error: Error) {
const prefix = Object.entries({
...use().tags,
...tags,
})
.map(([key, value]) => `${key}=${value}`)
.join(" ");
console.error(prefix, error);
return result;
},
tag(key: string, value: string) {
tags[key] = value;
return result;
},
clone() {
return Log.create({ ...tags });
},
};
return result;
}
export function provide<R>(tags: Record<string, any>, cb: () => R) {
const existing = use();
return ctx.provide(
{
tags: {
...existing.tags,
...tags,
},
},
cb,
);
}
function use() {
try {
return ctx.use();
} catch (e) {
return { tags: {} };
}
}
}

View File

@@ -0,0 +1,16 @@
export type RequireKeys<T extends object, K extends keyof T> = Required<
Pick<T, K>
> &
Omit<T, K> extends infer O
? { [P in keyof O]: O[P] }
: never;
export type Prettify<T> = {
[K in keyof T]: T[K];
} & {};
declare const __brand: unique symbol;
type Brand<B> = { [__brand]: B };
type Branded<T, B> = T & Brand<B>;
export type IdempotencyKey = Branded<string, "IdempotencyKey">;

View File

@@ -0,0 +1,35 @@
{
"compilerOptions": {
"target": "es2022",
"lib": ["ES2022", "DOM", "DOM.Iterable", "DOM.AsyncIterable"],
"module": "NodeNext",
"moduleResolution": "NodeNext",
"moduleDetection": "force",
"verbatimModuleSyntax": false,
"jsx": "react",
"strict": true,
"alwaysStrict": true,
"strictPropertyInitialization": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"noUnusedLocals": false,
"noUnusedParameters": false,
"noImplicitAny": true,
"noImplicitReturns": true,
"noImplicitThis": true,
"noFallthroughCasesInSwitch": true,
"resolveJsonModule": true,
"removeComments": false,
"esModuleInterop": true,
"emitDecoratorMetadata": false,
"experimentalDecorators": false,
"downlevelIteration": true,
"isolatedModules": true,
"noUncheckedIndexedAccess": true,
"pretty": true
}
}

View File

@@ -0,0 +1,8 @@
{
"extends": "./tsconfig.base.json",
"references": [
{
"path": "./tsconfig.src.json"
}
]
}

View File

@@ -0,0 +1,9 @@
{
"extends": "./tsconfig.json",
"include": ["./src/**/*.ts"],
"compilerOptions": {
"isolatedDeclarations": false,
"composite": true,
"sourceMap": true
}
}

34
packages/function/.gitignore vendored Normal file
View File

@@ -0,0 +1,34 @@
# dependencies (bun install)
node_modules
# output
out
dist
*.tgz
# code coverage
coverage
*.lcov
# logs
logs
_.log
report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
# dotenv environment variable files
.env
.env.development.local
.env.test.local
.env.production.local
.env.local
# caches
.eslintcache
.cache
*.tsbuildinfo
# IntelliJ based IDEs
.idea
# Finder (MacOS) folder config
.DS_Store

View File

@@ -0,0 +1,15 @@
# function
To install dependencies:
```bash
bun install
```
To run:
```bash
bun run index.ts
```
This project was created using `bun init` in bun v1.2.23. [Bun](https://bun.com) is a fast all-in-one JavaScript runtime.

View File

@@ -0,0 +1,20 @@
{
"name": "@nestri/function",
"type": "module",
"private": true,
"devDependencies": {
"@cloudflare/workers-types": "catalog:",
"@types/bun": "catalog:",
"@tsconfig/node22": "catalog:"
},
"peerDependencies": {
"typescript": "^5",
"@nestri/core": "workspace:^"
},
"dependencies": {
"@openauthjs/openauth": "catalog:",
"hono": "^4.10.1",
"hono-openapi": "^1.1.0",
"zod": "catalog:"
}
}

View File

@@ -0,0 +1,69 @@
import { subjects } from "../subjects";
import { MiddlewareHandler } from "hono";
import { Actor } from "@nestri/core/actor";
import { Api } from "@nestri/core/api/index";
import { Log } from "@nestri/core/utils/log";
import { createClient } from "@openauthjs/openauth/client";
import { VisibleError, ErrorCodes } from "@nestri/core/error";
const client = createClient({
clientID: "api",
issuer: process.env.AUTH_URL,
subjects,
});
const log = Log.create({ namespace: "api" });
const auth: MiddlewareHandler = async (c, next) => {
const authHeader =
c.req.query("authorization") ?? c.req.header("authorization");
if (authHeader) {
const match = authHeader.match(/^Bearer (.+)$/);
if (!match || !match[1]) {
throw new VisibleError(
"authentication",
ErrorCodes.Authentication.UNAUTHORIZED,
"Bearer token not found or improperly formatted",
);
}
const bearerToken = match[1];
if (bearerToken?.startsWith("nst_")) {
const token = await Api.Personal.fromToken(bearerToken);
if (!token)
throw new VisibleError(
"authentication",
ErrorCodes.Authentication.INVALID_TOKEN,
"Invalid personal access token",
);
return Actor.provide(
"token",
{
teamID: token.teamID,
tokenID: token.id,
},
next,
);
}
const result = await client.verify(bearerToken!);
if (result.err)
throw new VisibleError(
"authentication",
ErrorCodes.Authentication.INVALID_TOKEN,
"Invalid bearer token",
);
if (result.subject.type === "team") {
return Actor.provide(
"team",
{
email: result.subject.properties.email,
teamID: result.subject.properties.teamID,
},
next,
);
}
}
return Actor.provide("public", {}, next);
};

View File

@@ -0,0 +1,76 @@
import { z } from "zod/v4";
import { subjects } from "../subjects.js";
import {redis} from 'bun';
import { issuer } from "@openauthjs/openauth";
import { RedisStorage } from "./src/storage.js";
import { Team } from "@nestri/core/team/index";
import { CodeProvider } from "@openauthjs/openauth/provider/code";
//!TODO: Add an auth storage adapter, maybe redis or postgres
const storage = RedisStorage({
redis: RedisClient.create(),
});
const app = issuer({
subjects,
providers: {
email: CodeProvider({
async request(_req, state, form, error) {
console.log(state);
const params = new URLSearchParams();
if (error) {
params.set("error", error.type);
}
if (state.type === "start") {
return Response.redirect(
process.env.AUTH_FRONTEND_URL + "/auth/email?" + params.toString(),
302,
);
}
if (state.type === "code") {
return Response.redirect(
process.env.AUTH_FRONTEND_URL + "/auth/code?" + params.toString(),
302,
);
}
return new Response("ok");
},
async sendCode(claims, code) {
const email = z.email().parse(claims.email);
console.log(`Sending code ${code} to email ${email}`);
},
}),
},
success: async (ctx, res) => {
let email = res.claims.email;
if (!email) throw new Error("No email found");
let teamID = await Team.fromEmail(email).then((x) => x?.id);
if (!teamID) {
console.log("creating account for", email);
teamID = await Team.create({
email: email!,
});
}
return ctx.subject("team", email!, {
teamID: teamID!,
email: email,
});
},
async allow(input) {
const url = new URL(input.redirectURI);
return (
url.hostname.endsWith("localhost") ||
url.hostname.endsWith("nestri.io") ||
url.hostname.endsWith("console.nestri.io")
);
},
});
export default {
port: 3000,
fetch: app.fetch,
};

View File

@@ -0,0 +1,126 @@
/**
* Configure OpenAuth to use Redis DB as a
* storage adapter.
*
* ```ts
* import { RedisStorage } from "./src/storage.js"
* import { Redis } from "@nestri/core/redis/client"
*
* const storage = RedisStorage({
* redis: Redis.Client.create()
* })
*
*
* export default issuer({
* storage,
* // ...
* })
* ```
*
* @packageDocumentation
*/
import {
joinKey,
StorageAdapter,
} from "@openauthjs/openauth/storage/storage";
import { Cluster, Redis } from "@nestri/core/redis/client";
const separator = String.fromCharCode(0x1f);
/**
* Configure the Redis connection that's created.
*/
export interface RedisStorageOptions {
redis: Cluster | Redis;
keyPrefix?: string;
defaultTTL?: number;
}
/**
* Creates a Cloudflare KV store.
* @param options - The config for the adapter.
*/
export function RedisStorage(options: RedisStorageOptions): StorageAdapter {
const { redis } = options;
const prefix = "storage";
function createKey(key: string[]): string {
return `${prefix}:${joinKey(key)}`;
}
function parseKey(redisKey: string): string[] {
const withoutPrefix = redisKey.slice(prefix.length + 1);
return withoutPrefix.split(separator);
}
return {
async get(key: string[]) {
const redisKey = createKey(key);
const value = await redis.get(redisKey);
if (!value) return;
return JSON.parse(value) as Record<string, any>;
},
async set(key: string[], value: any, expiry?: Date) {
const redisKey = createKey(key);
const ttlSeconds = expiry
? Math.max(Math.floor((expiry.getTime() - Date.now()) / 1000), 1)
: undefined;
if (ttlSeconds) {
// SET key value EX ttlSeconds
await options.redis.set(
redisKey,
JSON.stringify(value),
"EX",
ttlSeconds,
);
} else {
// No expiry → simple SET
await options.redis.set(redisKey, JSON.stringify(value));
}
},
async remove(key: string[]) {
const redisKey = createKey(key);
await options.redis.del(redisKey);
},
async *scan(prefix: string[]) {
const scanPattern = `${createKey(prefix)}${
prefix.length ? separator : ""
}*`;
let cursor = "0";
do {
// SCAN cursor MATCH pattern COUNT 100
const [nextCursor, keys] = await redis.scan(
cursor,
"MATCH",
scanPattern,
"COUNT",
100,
);
cursor = nextCursor;
if (keys.length === 0) {
continue;
}
const values = await redis.mget(...keys);
for (let i = 0; i < keys.length; i++) {
const key = keys[i];
if (!key) {
continue;
}
const value = values[i];
if (value !== null) {
const parsedValue =
typeof value === "string" ? JSON.parse(value) : value;
yield [parseKey(key), parsedValue];
}
}
} while (cursor !== "0");
},
};
}

View File

@@ -0,0 +1,9 @@
import { z } from "zod/v4";
import { createSubjects } from "@openauthjs/openauth/subject";
export const subjects = createSubjects({
team: z.object({
email: z.email(),
teamID: z.string(),
}),
});

View File

@@ -0,0 +1,30 @@
import { format } from "node:util";
/**
* Overrides the default Node.js console logging methods with a custom logger.
*
* This function patches console.log, console.warn, console.error, console.trace, and console.debug so that each logs
* messages prefixed with a log level. The messages are formatted using Node.js formatting conventions, with newline
* characters replaced by carriage returns, and are written directly to standard output.
*
* @example
* patchLogger();
* console.info("Server started on port %d", 3000);
*/
export function patchLogger() {
const log =
(level: "INFO" | "WARN" | "TRACE" | "DEBUG" | "ERROR") =>
(msg: string, ...rest: any[]) => {
let formattedMessage = format(msg, ...rest);
// Split by newlines, prefix each line with the level, and join back
const lines = formattedMessage.split("\n");
const prefixedLines = lines.map((line) => `${level}\t${line}`);
const output = prefixedLines.join("\n");
process.stdout.write(output + "\n");
};
console.log = log("INFO");
console.warn = log("WARN");
console.error = log("ERROR");
console.trace = log("TRACE");
console.debug = log("DEBUG");
}

View File

@@ -0,0 +1,12 @@
{
"extends": "@tsconfig/node22/tsconfig.json",
"compilerOptions": {
"module": "esnext",
"jsx": "react-jsx",
"moduleResolution": "bundler",
"noUncheckedIndexedAccess": true,
"paths": {
"@/*": ["./src/*"]
}
}
}

1
packages/index.ts Normal file
View File

@@ -0,0 +1 @@
console.log("Hello via Bun!");

12
packages/package.json Normal file
View File

@@ -0,0 +1,12 @@
{
"name": "packages",
"module": "index.ts",
"type": "module",
"private": true,
"devDependencies": {
"@types/bun": "latest"
},
"peerDependencies": {
"typescript": "^5"
}
}

34
packages/sdk/.gitignore vendored Normal file
View File

@@ -0,0 +1,34 @@
# dependencies (bun install)
node_modules
# output
out
dist
*.tgz
# code coverage
coverage
*.lcov
# logs
logs
_.log
report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
# dotenv environment variable files
.env
.env.development.local
.env.test.local
.env.production.local
.env.local
# caches
.eslintcache
.cache
*.tsbuildinfo
# IntelliJ based IDEs
.idea
# Finder (MacOS) folder config
.DS_Store

15
packages/sdk/README.md Normal file
View File

@@ -0,0 +1,15 @@
# sdk
To install dependencies:
```bash
bun install
```
To run:
```bash
bun run index.ts
```
This project was created using `bun init` in bun v1.2.23. [Bun](https://bun.com) is a fast all-in-one JavaScript runtime.

View File

@@ -0,0 +1,22 @@
import { session } from "../src/index.js";
const myTestSession = session({
id: "test-session",
description: "A game test session",
resolution: "1080x1920",
videoBitDepth: 8,
machine: "gpu-medium",
videoCodec: "h264",
framerate: 60,
audioBitrate: 128,
gpuCardPath: "/dev/dri/renderD128",
run: async (payload) => {
console.log("Running test session with payload:", payload);
return { message: "Test session completed" };
},
});
myTestSession.start().then((result) => {
console.log("Session result:", result);
});

18
packages/sdk/package.json Normal file
View File

@@ -0,0 +1,18 @@
{
"name": "@nestri/sdk",
"module": "src/index.ts",
"type": "module",
"private": true,
"devDependencies": {
"@hey-api/openapi-ts": "^0.86.2",
"@types/bun": "catalog:",
"@types/node": "catalog:"
},
"exports": {
".": "./src/index.ts"
},
"peerDependencies": {
"typescript": "^5",
"@nestri/core": "workspace:^"
}
}

217
packages/sdk/src/index.ts Normal file
View File

@@ -0,0 +1,217 @@
import {
type Session,
type RunTypes,
type InitOutput,
type StartOptions,
type inferSchemaIn,
type SchemaParseFn,
type SessionSchema,
type SessionOptions,
type SessionWithSchema,
type SessionWithSchemaOptions,
type RunHandleFromTypes,
type AnyRunTypes,
type SessionOptionsWithSchema,
getSchemaParseFn,
SessionRunPromise,
} from "@nestri/core/session/index";
// Overload: when payloadSchema is provided, payload type should be any
export function createRun<
TIdentifier extends string,
TOutput = unknown,
TInitOutput extends InitOutput = any,
>(
params: SessionOptionsWithSchema<TIdentifier, TOutput, TInitOutput>,
): Session<TIdentifier, any, TOutput>;
// Overload: normal case without payloadSchema
export function createRun<
TIdentifier extends string,
TInput = void,
TOutput = unknown,
TInitOutput extends InitOutput = any,
>(
params: SessionOptions<TIdentifier, TInput, TOutput, TInitOutput>,
): Session<TIdentifier, TInput, TOutput>;
export function createRun<
TIdentifier extends string,
TInput = void,
TOutput = unknown,
TInitOutput extends InitOutput = any,
>(
params:
| SessionOptions<TIdentifier, TInput, TOutput, TInitOutput>
| SessionOptionsWithSchema<TIdentifier, TOutput, TInitOutput>,
): Session<TIdentifier, TInput, TOutput> | Session<TIdentifier, any, TOutput> {
const session: Session<TIdentifier, TInput, TOutput> = {
id: params.id,
description: params.description,
jsonSchema: params.jsonSchema,
start: async (payload, options) => {
return await run_internal<RunTypes<TIdentifier, TInput, TOutput>>(
"run()",
params.id,
payload,
undefined,
{
queue: params.queue?.name,
...options,
},
);
},
startAndWait: (payload, options) => {
return new SessionRunPromise<TIdentifier, TOutput>((resolve, reject) => {
triggerAndWait_internal<TIdentifier, TInput, TOutput>(
"triggerAndWait()",
params.id,
payload,
undefined,
{
queue: params.queue?.name,
...options,
},
)
.then((result) => {
resolve(result);
})
.catch((error) => {
reject(error);
});
}, params.id);
},
};
registerTaskLifecycleHooks(params.id, params);
resourceCatalog.registerTaskMetadata({
id: params.id,
description: params.description,
queue: params.queue,
retry: params.retry
? { ...defaultRetryOptions, ...params.retry }
: undefined,
machine:
typeof params.machine === "string"
? { preset: params.machine }
: params.machine,
maxDuration: params.maxDuration,
payloadSchema: params.jsonSchema,
fns: {
run: params.run,
},
});
const queue = params.queue;
if (queue && typeof queue.name === "string") {
resourceCatalog.registerQueueMetadata({
name: queue.name,
concurrencyLimit: queue.concurrencyLimit,
});
}
// @ts-expect-error
session[Symbol.for("nestri/session")] = true;
return session;
}
export function createSchemaTask<
TIdentifier extends string,
TSchema extends SessionSchema | undefined = undefined,
TOutput = unknown,
TInitOutput extends InitOutput = any,
>(
params: SessionWithSchemaOptions<TIdentifier, TSchema, TOutput, TInitOutput>,
): SessionWithSchema<TIdentifier, TSchema, TOutput> {
const parsePayload = params.schema
? getSchemaParseFn<inferSchemaIn<TSchema>>(params.schema)
: undefined;
const session: SessionWithSchema<TIdentifier, TSchema, TOutput> = {
id: params.id,
description: params.description,
schema: params.schema,
start: async (payload, options, requestOptions) => {
return await run_internal<
RunTypes<TIdentifier, inferSchemaIn<TSchema>, TOutput>
>(
"run()",
params.id,
payload,
parsePayload,
{
queue: params.queue?.name,
...options,
},
requestOptions,
);
},
startAndWait: (payload, options) => {
return new SessionRunPromise<TIdentifier, TOutput>((resolve, reject) => {
triggerAndWait_internal<TIdentifier, inferSchemaIn<TSchema>, TOutput>(
"triggerAndWait()",
params.id,
payload,
parsePayload,
{
queue: params.queue?.name,
...options,
},
)
.then((result) => {
resolve(result);
})
.catch((error) => {
reject(error);
});
}, params.id);
},
};
registerTaskLifecycleHooks(params.id, params);
resourceCatalog.registerTaskMetadata({
id: params.id,
description: params.description,
queue: params.queue,
retry: params.retry
? { ...defaultRetryOptions, ...params.retry }
: undefined,
machine: { preset: params.machine },
maxDuration: params.maxDuration,
fns: {
run: params.run,
parsePayload,
},
schema: params.schema,
});
const queue = params.queue;
if (queue && typeof queue.name === "string") {
resourceCatalog.registerQueueMetadata({
name: queue.name,
concurrencyLimit: queue.concurrencyLimit,
});
}
// @ts-expect-error
session[Symbol.for("nestri/session")] = true;
return session;
}
export const session = createRun;
export const schemaSession = createSchemaTask;
async function run_internal<TRunTypes extends AnyRunTypes>(
name: string,
id: TRunTypes["sessionIdentifier"],
payload: TRunTypes["payload"],
parsePayload?: SchemaParseFn<TRunTypes["payload"]>,
options?: StartOptions,
requestOptions?: any, //TriggerApiRequestOptions,
): Promise<RunHandleFromTypes<TRunTypes>> {}

View File

@@ -0,0 +1,42 @@
#!/usr/bin/env bun
//TODO: Fix this to import JSON
const dir = new URL("..", import.meta.url).pathname;
process.chdir(dir);
import { $ } from "bun";
import path from "path";
import { createClient } from "@hey-api/openapi-ts";
await $`bun dev generate > ${dir}/openapi.json`.cwd(
path.resolve(dir, "../../opencode"),
);
await createClient({
input: "./openapi.json",
output: {
path: "./src/gen",
tsConfigPath: path.join(dir, "tsconfig.json"),
},
plugins: [
{
name: "@hey-api/typescript",
exportFromIndex: false,
},
{
name: "@hey-api/sdk",
instance: "OpencodeClient",
exportFromIndex: false,
auth: false,
},
{
name: "@hey-api/client-fetch",
exportFromIndex: false,
baseUrl: "http://localhost:4096",
},
],
});
await $`bun prettier --write src/gen`;
await $`rm -rf dist`;
await $`bun tsc`;

View File

@@ -0,0 +1,9 @@
{
"extends": "@nestri/core/tsconfig.base",
"compilerOptions": {
"composite": true,
"sourceMap": true,
"stripInternal": true,
"isolatedDeclarations": false
}
}

View File

@@ -0,0 +1,10 @@
{
"extends": "@nestri/core/tsconfig.base",
"compilerOptions": {
"isolatedDeclarations": false,
"composite": true,
"sourceMap": true,
"stripInternal": true
},
"include": ["src/globals.d.ts", "./src/**/*.ts", "tsup.config.ts"]
}

34
packages/studio/.gitignore vendored Normal file
View File

@@ -0,0 +1,34 @@
# dependencies (bun install)
node_modules
# output
out
dist
*.tgz
# code coverage
coverage
*.lcov
# logs
logs
_.log
report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
# dotenv environment variable files
.env
.env.development.local
.env.test.local
.env.production.local
.env.local
# caches
.eslintcache
.cache
*.tsbuildinfo
# IntelliJ based IDEs
.idea
# Finder (MacOS) folder config
.DS_Store

15
packages/studio/README.md Normal file
View File

@@ -0,0 +1,15 @@
# studio
To install dependencies:
```bash
bun install
```
To run:
```bash
bun run index.ts
```
This project was created using `bun init` in bun v1.2.23. [Bun](https://bun.com) is a fast all-in-one JavaScript runtime.

1
packages/studio/index.ts Normal file
View File

@@ -0,0 +1 @@
console.log("Hello via Bun!");

View File

@@ -0,0 +1,15 @@
{
"name": "@nestri/studio",
"module": "src/index.ts",
"type": "module",
"private": true,
"devDependencies": {
"@types/bun": "latest"
},
"exports": {
".": "./src/index.ts"
},
"peerDependencies": {
"typescript": "^5"
}
}

View File

@@ -0,0 +1,10 @@
{
"extends": "@nestri/core/tsconfig.base.json",
"compilerOptions": {
"isolatedDeclarations": false,
"composite": true,
"sourceMap": true,
"stripInternal": true
},
"include": ["src/globals.d.ts", "./src/**/*.ts", "tsup.config.ts"]
}

29
packages/tsconfig.json Normal file
View File

@@ -0,0 +1,29 @@
{
"compilerOptions": {
// Environment setup & latest features
"lib": ["ESNext"],
"target": "ESNext",
"module": "Preserve",
"moduleDetection": "force",
"jsx": "react-jsx",
"allowJs": true,
// Bundler mode
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"noEmit": true,
// Best practices
"strict": true,
"skipLibCheck": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedIndexedAccess": true,
"noImplicitOverride": true,
// Some stricter flags (disabled by default)
"noUnusedLocals": false,
"noUnusedParameters": false,
"noPropertyAccessFromIndexSignature": false
}
}

View File

@@ -1 +0,0 @@
export default new Map();

View File

@@ -1 +0,0 @@
export default new Map();

View File

@@ -1 +0,0 @@
[["Map",1,2],"meta::meta",["Map",3,4,5,6],"astro-version","5.11.2","astro-config-digest","{\"root\":{},\"srcDir\":{},\"publicDir\":{},\"outDir\":{},\"cacheDir\":{},\"compressHTML\":true,\"base\":\"/\",\"trailingSlash\":\"ignore\",\"output\":\"static\",\"scopedStyleStrategy\":\"attribute\",\"build\":{\"format\":\"directory\",\"client\":{},\"server\":{},\"assets\":\"_astro\",\"serverEntry\":\"entry.mjs\",\"redirects\":true,\"inlineStylesheets\":\"auto\",\"concurrency\":1},\"server\":{\"open\":false,\"host\":false,\"port\":4321,\"streaming\":true,\"allowedHosts\":[]},\"redirects\":{},\"image\":{\"endpoint\":{\"route\":\"/_image\"},\"service\":{\"entrypoint\":\"astro/assets/services/sharp\",\"config\":{}},\"domains\":[],\"remotePatterns\":[],\"responsiveStyles\":false},\"devToolbar\":{\"enabled\":true},\"markdown\":{\"syntaxHighlight\":{\"type\":\"shiki\",\"excludeLangs\":[\"math\"]},\"shikiConfig\":{\"langs\":[],\"langAlias\":{},\"theme\":\"github-dark\",\"themes\":{},\"wrap\":false,\"transformers\":[]},\"remarkPlugins\":[],\"rehypePlugins\":[],\"remarkRehype\":{},\"gfm\":true,\"smartypants\":true},\"security\":{\"checkOrigin\":true},\"env\":{\"schema\":{},\"validateSecrets\":false},\"experimental\":{\"clientPrerender\":false,\"contentIntellisense\":false,\"headingIdCompat\":false,\"preserveScriptOrder\":false,\"liveContentCollections\":false,\"csp\":false},\"legacy\":{\"collections\":false}}"]

View File

@@ -1,5 +0,0 @@
{
"_variables": {
"lastUpdateCheck": 1752741260450
}
}

View File

@@ -1 +0,0 @@
/// <reference types="astro/client" />

View File

@@ -1,65 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="410"
height="50"
viewBox="0 0 108.47917 13.229167"
version="1.1"
id="svg1"
xml:space="preserve"
inkscape:export-filename="logo-black.png"
inkscape:export-xdpi="96"
inkscape:export-ydpi="96"
inkscape:version="1.4.2 (f4327f4, 2025-05-13)"
sodipodi:docname="logo.black.svg"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"><sodipodi:namedview
id="namedview1"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:document-units="mm"
inkscape:zoom="1.9477379"
inkscape:cx="243.35923"
inkscape:cy="61.353224"
inkscape:window-width="1920"
inkscape:window-height="1046"
inkscape:window-x="-11"
inkscape:window-y="-11"
inkscape:window-maximized="1"
inkscape:current-layer="layer1" /><defs
id="defs1"><linearGradient
gradientUnits="userSpaceOnUse"
id="paint1"
x1="317.5"
x2="314.00699"
y1="-51.5"
y2="126"
gradientTransform="matrix(0.06483182,0,0,0.06483182,7.793483,1.7716596)"><stop
stop-color="white"
id="stop1" /><stop
offset="1"
stop-opacity="0"
id="stop2" /></linearGradient></defs><g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"><path
fill="url(#paint1)"
pathLength="1"
stroke="url(#paint1)"
d="m 66.688022,7.9135153 h -8.573986 q -3.000755,0 -3.000755,-2.572126 V 3.1978936 a 3.1280616,3.1280616 0 0 1 0.175245,-1.0930831 q 0.549966,-1.4792191 2.82551,-1.4792191 h 12.432349 v 2.5809064 h -11.58352 q -0.333456,0 -0.40229,0.2702417 a 0.64443619,0.64443619 0 0 0 -0.01774,0.1583869 v 1.2776343 q 0,0.4286286 0.420026,0.4286286 h 8.58259 q 3.000928,0 3.000928,2.572126 v 2.1434957 a 3.1280616,3.1280616 0 0 1 -0.175243,1.09326 q -0.549967,1.479042 -2.825685,1.479042 h -12.43217 v -2.580731 h 11.574741 a 0.61721885,0.61721885 0 0 0 0.169801,-0.02125 q 0.259003,-0.07428 0.259003,-0.4075567 v -1.277454 a 0.61721885,0.61721885 0 0 0 -0.02125,-0.169627 q -0.07428,-0.259178 -0.407558,-0.259178 z M 53.398413,10.065616 v 2.563697 H 40.966062 q -3.000929,0 -3.000929,-2.572302 V 3.1978936 a 3.1280616,3.1280616 0 0 1 0.175419,-1.0930831 q 0.549966,-1.4792191 2.82551,-1.4792191 h 9.43142 q 3.000931,0 3.000931,2.5723022 v 3.0009297 a 3.1303444,3.1303444 0 0 1 -0.176122,1.0955408 q -0.513266,1.3780752 -2.52331,1.4774622 a 6.4805345,6.4805345 0 0 1 -0.301499,0.0077 h -9.002615 l 0.0085,0.857433 q 0,0.4286287 0.4202,0.4372327 z M 24.246719,3.1978936 v 9.4314194 h -3.42956 V 0.6255914 h 12.432352 q 3.000929,0 3.000929,2.5723022 V 12.629313 H 32.820705 V 3.6265222 q 0,-0.4286286 -0.420024,-0.4286286 z m 75.451365,9.4314194 -3.34387,-3.8583647 h -5.23012 V 12.629313 H 87.694359 V 0.6255914 H 100.1267 a 4.3200052,4.3200052 0 0 1 1.18755,0.1489057 q 1.33173,0.3813933 1.68537,1.7406798 a 4.4211483,4.4211483 0 0 1 0.12801,1.1113453 v 2.5723011 a 3.1988267,3.1988267 0 0 1 -0.15154,1.0263568 q -0.33942,1.0033542 -1.43794,1.3564762 a 4.0817218,4.0817218 0 0 1 -0.91417,0.172259 l 3.36108,3.8753977 z M 72.261256,0.5999546 85.979669,0.617163 v 2.5807306 h -5.13583 v 9.4314194 h -3.438161 l -0.0086,-9.4314194 -5.13582,0.00858 z m 32.581244,0.025641 h 3.42956 V 12.629316 H 104.8425 Z M 91.124094,3.2065009 v 2.9923255 h 8.15377 q 0.33365,0 0.4023,-0.2704175 a 0.64443619,0.64443619 0 0 0 0.0179,-0.1583869 V 3.6351296 q 0,-0.4286286 -0.42021,-0.4286286 z m -49.729227,2.9837213 8.145181,0.017211 q 0.343115,-0.00688 0.411597,-0.2772657 a 0.61704326,0.61704326 0 0 0 0.01721,-0.1515397 V 3.6265286 q 0,-0.3334559 -0.270419,-0.4022896 A 0.64443619,0.64443619 0 0 0 49.54005,3.206499 l -7.716551,-0.00858 q -0.42863,0 -0.42863,0.4202002 z"
id="path1"
style="font-size:12px;fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:0.0438989mm;stroke-linecap:round;stroke-opacity:1" /><path
d="M 1.50309,1.4982293 H 15.924554 V 2.2921471 H 1.50309 Z m 0,4.7193972 H 15.924554 V 7.0115422 H 1.50309 Z m 0,4.7193935 h 14.421464 v 0.793917 H 1.50309 Z"
id="path1-8"
style="font-size:12px;fill:#ff4f01;fill-opacity:1;fill-rule:evenodd;stroke:#ff4f01;stroke-width:2.75789;stroke-linecap:round;stroke-dasharray:none;stroke-opacity:1"
inkscape:export-filename="logo.png"
inkscape:export-xdpi="96"
inkscape:export-ydpi="96" /></g></svg>

Before

Width:  |  Height:  |  Size: 4.7 KiB

View File

@@ -1,65 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="410"
height="50"
viewBox="0 0 108.47917 13.229167"
version="1.1"
id="svg1"
xml:space="preserve"
inkscape:export-filename="logo-black.png"
inkscape:export-xdpi="96"
inkscape:export-ydpi="96"
inkscape:version="1.4.2 (f4327f4, 2025-05-13)"
sodipodi:docname="logo.white.svg"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"><sodipodi:namedview
id="namedview1"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:document-units="mm"
inkscape:zoom="1.9477379"
inkscape:cx="243.35923"
inkscape:cy="61.353224"
inkscape:window-width="1920"
inkscape:window-height="1046"
inkscape:window-x="-11"
inkscape:window-y="-11"
inkscape:window-maximized="1"
inkscape:current-layer="layer1" /><defs
id="defs1"><linearGradient
gradientUnits="userSpaceOnUse"
id="paint1"
x1="317.5"
x2="314.00699"
y1="-51.5"
y2="126"
gradientTransform="matrix(0.06483182,0,0,0.06483182,7.793483,1.7716596)"><stop
stop-color="white"
id="stop1" /><stop
offset="1"
stop-opacity="0"
id="stop2" /></linearGradient></defs><g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"><path
fill="url(#paint1)"
pathLength="1"
stroke="url(#paint1)"
d="m 66.688022,7.9135153 h -8.573986 q -3.000755,0 -3.000755,-2.572126 V 3.1978936 a 3.1280616,3.1280616 0 0 1 0.175245,-1.0930831 q 0.549966,-1.4792191 2.82551,-1.4792191 h 12.432349 v 2.5809064 h -11.58352 q -0.333456,0 -0.40229,0.2702417 a 0.64443619,0.64443619 0 0 0 -0.01774,0.1583869 v 1.2776343 q 0,0.4286286 0.420026,0.4286286 h 8.58259 q 3.000928,0 3.000928,2.572126 v 2.1434957 a 3.1280616,3.1280616 0 0 1 -0.175243,1.09326 q -0.549967,1.479042 -2.825685,1.479042 h -12.43217 v -2.580731 h 11.574741 a 0.61721885,0.61721885 0 0 0 0.169801,-0.02125 q 0.259003,-0.07428 0.259003,-0.4075567 v -1.277454 a 0.61721885,0.61721885 0 0 0 -0.02125,-0.169627 q -0.07428,-0.259178 -0.407558,-0.259178 z M 53.398413,10.065616 v 2.563697 H 40.966062 q -3.000929,0 -3.000929,-2.572302 V 3.1978936 a 3.1280616,3.1280616 0 0 1 0.175419,-1.0930831 q 0.549966,-1.4792191 2.82551,-1.4792191 h 9.43142 q 3.000931,0 3.000931,2.5723022 v 3.0009297 a 3.1303444,3.1303444 0 0 1 -0.176122,1.0955408 q -0.513266,1.3780752 -2.52331,1.4774622 a 6.4805345,6.4805345 0 0 1 -0.301499,0.0077 h -9.002615 l 0.0085,0.857433 q 0,0.4286287 0.4202,0.4372327 z M 24.246719,3.1978936 v 9.4314194 h -3.42956 V 0.6255914 h 12.432352 q 3.000929,0 3.000929,2.5723022 V 12.629313 H 32.820705 V 3.6265222 q 0,-0.4286286 -0.420024,-0.4286286 z m 75.451365,9.4314194 -3.34387,-3.8583647 h -5.23012 V 12.629313 H 87.694359 V 0.6255914 H 100.1267 a 4.3200052,4.3200052 0 0 1 1.18755,0.1489057 q 1.33173,0.3813933 1.68537,1.7406798 a 4.4211483,4.4211483 0 0 1 0.12801,1.1113453 v 2.5723011 a 3.1988267,3.1988267 0 0 1 -0.15154,1.0263568 q -0.33942,1.0033542 -1.43794,1.3564762 a 4.0817218,4.0817218 0 0 1 -0.91417,0.172259 l 3.36108,3.8753977 z M 72.261256,0.5999546 85.979669,0.617163 v 2.5807306 h -5.13583 v 9.4314194 h -3.438161 l -0.0086,-9.4314194 -5.13582,0.00858 z m 32.581244,0.025641 h 3.42956 V 12.629316 H 104.8425 Z M 91.124094,3.2065009 v 2.9923255 h 8.15377 q 0.33365,0 0.4023,-0.2704175 a 0.64443619,0.64443619 0 0 0 0.0179,-0.1583869 V 3.6351296 q 0,-0.4286286 -0.42021,-0.4286286 z m -49.729227,2.9837213 8.145181,0.017211 q 0.343115,-0.00688 0.411597,-0.2772657 a 0.61704326,0.61704326 0 0 0 0.01721,-0.1515397 V 3.6265286 q 0,-0.3334559 -0.270419,-0.4022896 A 0.64443619,0.64443619 0 0 0 49.54005,3.206499 l -7.716551,-0.00858 q -0.42863,0 -0.42863,0.4202002 z"
id="path1"
style="font-size:12px;fill:#ffffff;fill-opacity:1;fill-rule:evenodd;stroke:#ffffff;stroke-width:0.0438989mm;stroke-linecap:round;stroke-opacity:1" /><path
d="M 1.50309,1.4982293 H 15.924554 V 2.2921471 H 1.50309 Z m 0,4.7193972 H 15.924554 V 7.0115422 H 1.50309 Z m 0,4.7193935 h 14.421464 v 0.793917 H 1.50309 Z"
id="path1-8"
style="font-size:12px;fill:#ff4f01;fill-opacity:1;fill-rule:evenodd;stroke:#ff4f01;stroke-width:2.75789;stroke-linecap:round;stroke-dasharray:none;stroke-opacity:1"
inkscape:export-filename="logo.png"
inkscape:export-xdpi="96"
inkscape:export-ydpi="96" /></g></svg>

Before

Width:  |  Height:  |  Size: 4.7 KiB

View File

@@ -27,7 +27,7 @@
"@macaron-css/solid": "1.5.3", "@macaron-css/solid": "1.5.3",
"@modular-forms/solid": "^0.25.1", "@modular-forms/solid": "^0.25.1",
"@nestri/core": "*", "@nestri/core": "*",
"@nestri/functions": "*", "@nestri/cloud-functions": "*",
"@nestri/zero": "*", "@nestri/zero": "*",
"@openauthjs/openauth": "*", "@openauthjs/openauth": "*",
"@openauthjs/solid": "0.0.0-20250311201457", "@openauthjs/solid": "0.0.0-20250311201457",

View File

@@ -1,36 +1,35 @@
import { hc } from "hono/client"; import { hc } from "hono/client";
import { useSteam } from "./context"; import { useSteam } from "./context";
import { useOpenAuth } from "@openauthjs/solid"; import { useOpenAuth } from "@openauthjs/solid";
import { type app } from "@nestri/functions/api/index"; import { type app } from "@nestri/cloud-functions/api/index";
import { createInitializedContext } from "@nestri/www/common/context"; import { createInitializedContext } from "@nestri/www/common/context";
export const { use: useApi, provider: ApiProvider } = createInitializedContext( export const { use: useApi, provider: ApiProvider } = createInitializedContext(
"ApiContext", "ApiContext",
() => { () => {
const steam = useSteam(); const steam = useSteam();
const auth = useOpenAuth(); const auth = useOpenAuth();
const client = hc<typeof app>(import.meta.env.VITE_API_URL, { const client = hc<typeof app>(import.meta.env.VITE_API_URL, {
async fetch(...args: Parameters<typeof fetch>): Promise<Response> { async fetch(...args: Parameters<typeof fetch>): Promise<Response> {
const [input, init] = args; const [input, init] = args;
const request = const request =
input instanceof Request ? input : new Request(input, init); input instanceof Request ? input : new Request(input, init);
const headers = new Headers(request.headers); const headers = new Headers(request.headers);
headers.set("authorization", `Bearer ${await auth.access()}`); headers.set("authorization", `Bearer ${await auth.access()}`);
headers.set("x-nestri-steam", steam().id); headers.set("x-nestri-steam", steam().id);
return fetch( return fetch(
new Request(request, { new Request(request, {
...init, ...init,
headers, headers,
}), }),
); );
}, },
}); });
return { return {
client, client,
ready: true, ready: true,
}; };
}, },
); );

47
sst-env.d.ts vendored
View File

@@ -1,47 +0,0 @@
/* This file is auto-generated by SST. Do not edit. */
/* tslint:disable */
/* eslint-disable */
/* deno-fmt-ignore-file */
import "sst"
declare module "sst" {
export interface Resource {
"DISCORD_CLIENT_ID": {
"type": "sst.sst.Secret"
"value": string
}
"DISCORD_CLIENT_SECRET": {
"type": "sst.sst.Secret"
"value": string
}
"Database": {
"host": string
"name": string
"password": string
"type": "sst.sst.Linkable"
"user": string
}
"POLAR_API_KEY": {
"type": "sst.sst.Secret"
"value": string
}
"Urls": {
"api": string
"auth": string
"openapi": string
"site": string
"type": "sst.sst.Linkable"
}
}
}
// cloudflare
import * as cloudflare from "@cloudflare/workers-types";
declare module "sst" {
export interface Resource {
"Auth": cloudflare.Service
"AuthStorage": cloudflare.KVNamespace
}
}
import "sst"
export {}

View File

@@ -1,26 +0,0 @@
/// <reference path="./.sst/platform/config.d.ts" />
export default $config({
app(input) {
return {
name: "nestri",
removal: input?.stage === "production" ? "retain" : "remove",
protect: ["production"].includes(input?.stage),
home: "cloudflare",
providers: {
cloudflare: "6.6.0",
random: "4.17.0",
command: "1.0.2",
neon: "0.9.0",
},
};
},
async run() {
const fs = await import("fs");
const outputs = {};
for (const value of fs.readdirSync("./cloud/infra/")) {
const result = await import("./cloud/infra/" + value);
if (result.outputs) Object.assign(outputs, result.outputs);
}
return outputs;
},
});