mirror of
https://github.com/nestriness/nestri.git
synced 2026-08-01 17:34:15 +03:00
feat: Add a lot of stuff
This commit is contained in:
7
alchemy.run.ts
Normal file
7
alchemy.run.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import alchemy from "alchemy";
|
||||||
|
|
||||||
|
const app = await alchemy("nestri");
|
||||||
|
|
||||||
|
// await import("./infra/api");
|
||||||
|
|
||||||
|
await app.finalize();
|
||||||
@@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
97
cloud/packages/core/src/team/index.ts
Normal file
97
cloud/packages/core/src/team/index.ts
Normal 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,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
9
cloud/packages/core/src/team/team.sql.ts
Normal file
9
cloud/packages/core/src/team/team.sql.ts
Normal 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 }),
|
||||||
|
});
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
|||||||
@@ -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/*"]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
22
package.json
22
package.json
@@ -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
34
packages/.gitignore
vendored
Normal 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
15
packages/README.md
Normal 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
29
packages/bun.lock
Normal 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
34
packages/core/.gitignore
vendored
Normal 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
15
packages/core/README.md
Normal 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.
|
||||||
29
packages/core/package.json
Normal file
29
packages/core/package.json
Normal 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"
|
||||||
|
}
|
||||||
|
}
|
||||||
89
packages/core/src/actor.ts
Normal file
89
packages/core/src/actor.ts
Normal 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,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
27
packages/core/src/api/api.sql.ts
Normal file
27
packages/core/src/api/api.sql.ts
Normal 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(),
|
||||||
|
});
|
||||||
31
packages/core/src/api/app.ts
Normal file
31
packages/core/src/api/app.ts
Normal 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>;
|
||||||
|
}
|
||||||
7
packages/core/src/api/index.ts
Normal file
7
packages/core/src/api/index.ts
Normal 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;
|
||||||
|
}
|
||||||
45
packages/core/src/api/personal.ts
Normal file
45
packages/core/src/api/personal.ts
Normal 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));
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
21
packages/core/src/context.ts
Normal file
21
packages/core/src/context.ts
Normal 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);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
90
packages/core/src/drizzle/index.ts
Normal file
90
packages/core/src/drizzle/index.ts
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
39
packages/core/src/drizzle/types.ts
Normal file
39
packages/core/src/drizzle/types.ts
Normal 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
145
packages/core/src/error.ts
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
138
packages/core/src/event/event.ts
Normal file
138
packages/core/src/event/event.ts
Normal 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;
|
||||||
|
}
|
||||||
12
packages/core/src/event/index.ts
Normal file
12
packages/core/src/event/index.ts
Normal 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(),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
9
packages/core/src/event/validator.ts
Normal file
9
packages/core/src/event/validator.ts
Normal 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);
|
||||||
|
};
|
||||||
|
}
|
||||||
32
packages/core/src/examples.ts
Normal file
32
packages/core/src/examples.ts
Normal 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",
|
||||||
|
};
|
||||||
|
}
|
||||||
32
packages/core/src/member/index.ts
Normal file
32
packages/core/src/member/index.ts
Normal 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>;
|
||||||
|
}
|
||||||
15
packages/core/src/member/member.sql.ts
Normal file
15
packages/core/src/member/member.sql.ts
Normal 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(),
|
||||||
|
});
|
||||||
20
packages/core/src/polar/index.ts
Normal file
20
packages/core/src/polar/index.ts
Normal 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;
|
||||||
|
});
|
||||||
|
}
|
||||||
47
packages/core/src/redis/client.ts
Normal file
47
packages/core/src/redis/client.ts
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
755
packages/core/src/redis/queue.ts
Normal file
755
packages/core/src/redis/queue.ts
Normal 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>;
|
||||||
|
}
|
||||||
|
}
|
||||||
3
packages/core/src/session/index.ts
Normal file
3
packages/core/src/session/index.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
export * from "./json.js";
|
||||||
|
export * from "./types.js";
|
||||||
|
export * from "./schema.js";
|
||||||
118
packages/core/src/session/json.ts
Normal file
118
packages/core/src/session/json.ts
Normal 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";
|
||||||
152
packages/core/src/session/schema.ts
Normal file
152
packages/core/src/session/schema.ts
Normal 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");
|
||||||
|
}
|
||||||
1076
packages/core/src/session/types.ts
Normal file
1076
packages/core/src/session/types.ts
Normal file
File diff suppressed because it is too large
Load Diff
100
packages/core/src/team/index.ts
Normal file
100
packages/core/src/team/index.ts
Normal 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,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
9
packages/core/src/team/team.sql.ts
Normal file
9
packages/core/src/team/team.sql.ts
Normal 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 }),
|
||||||
|
});
|
||||||
30
packages/core/src/utils/fn.ts
Normal file
30
packages/core/src/utils/fn.ts
Normal 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;
|
||||||
|
}
|
||||||
23
packages/core/src/utils/id.ts
Normal file
23
packages/core/src/utils/id.ts
Normal 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("_");
|
||||||
|
}
|
||||||
2
packages/core/src/utils/index.ts
Normal file
2
packages/core/src/utils/index.ts
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
export * from "./log.js";
|
||||||
|
export * from "./types.js";
|
||||||
76
packages/core/src/utils/log.ts
Normal file
76
packages/core/src/utils/log.ts
Normal 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: {} };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
16
packages/core/src/utils/types.ts
Normal file
16
packages/core/src/utils/types.ts
Normal 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">;
|
||||||
35
packages/core/tsconfig.base.json
Normal file
35
packages/core/tsconfig.base.json
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
8
packages/core/tsconfig.json
Normal file
8
packages/core/tsconfig.json
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"extends": "./tsconfig.base.json",
|
||||||
|
"references": [
|
||||||
|
{
|
||||||
|
"path": "./tsconfig.src.json"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
9
packages/core/tsconfig.src.json
Normal file
9
packages/core/tsconfig.src.json
Normal 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
34
packages/function/.gitignore
vendored
Normal 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/function/README.md
Normal file
15
packages/function/README.md
Normal 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.
|
||||||
20
packages/function/package.json
Normal file
20
packages/function/package.json
Normal 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:"
|
||||||
|
}
|
||||||
|
}
|
||||||
69
packages/function/src/api/index.ts
Normal file
69
packages/function/src/api/index.ts
Normal 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);
|
||||||
|
};
|
||||||
76
packages/function/src/auth/index.ts
Normal file
76
packages/function/src/auth/index.ts
Normal 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,
|
||||||
|
};
|
||||||
126
packages/function/src/auth/src/storage.ts
Normal file
126
packages/function/src/auth/src/storage.ts
Normal 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");
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
9
packages/function/src/subjects.ts
Normal file
9
packages/function/src/subjects.ts
Normal 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(),
|
||||||
|
}),
|
||||||
|
});
|
||||||
30
packages/function/src/utils/logger.ts
Normal file
30
packages/function/src/utils/logger.ts
Normal 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");
|
||||||
|
}
|
||||||
12
packages/function/tsconfig.json
Normal file
12
packages/function/tsconfig.json
Normal 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
1
packages/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
console.log("Hello via Bun!");
|
||||||
12
packages/package.json
Normal file
12
packages/package.json
Normal 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
34
packages/sdk/.gitignore
vendored
Normal 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
15
packages/sdk/README.md
Normal 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.
|
||||||
22
packages/sdk/examples/test.ts
Normal file
22
packages/sdk/examples/test.ts
Normal 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
18
packages/sdk/package.json
Normal 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
217
packages/sdk/src/index.ts
Normal 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>> {}
|
||||||
42
packages/sdk/src/script/build.ts
Normal file
42
packages/sdk/src/script/build.ts
Normal 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`;
|
||||||
9
packages/sdk/tsconfig.build.json
Normal file
9
packages/sdk/tsconfig.build.json
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"extends": "@nestri/core/tsconfig.base",
|
||||||
|
"compilerOptions": {
|
||||||
|
"composite": true,
|
||||||
|
"sourceMap": true,
|
||||||
|
"stripInternal": true,
|
||||||
|
"isolatedDeclarations": false
|
||||||
|
}
|
||||||
|
}
|
||||||
10
packages/sdk/tsconfig.json
Normal file
10
packages/sdk/tsconfig.json
Normal 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
34
packages/studio/.gitignore
vendored
Normal 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
15
packages/studio/README.md
Normal 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
1
packages/studio/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
console.log("Hello via Bun!");
|
||||||
15
packages/studio/package.json
Normal file
15
packages/studio/package.json
Normal 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"
|
||||||
|
}
|
||||||
|
}
|
||||||
10
packages/studio/tsconfig.json
Normal file
10
packages/studio/tsconfig.json
Normal 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
29
packages/tsconfig.json
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1 +0,0 @@
|
|||||||
export default new Map();
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
export default new Map();
|
|
||||||
@@ -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}}"]
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
{
|
|
||||||
"_variables": {
|
|
||||||
"lastUpdateCheck": 1752741260450
|
|
||||||
}
|
|
||||||
}
|
|
||||||
1
packages/web/.astro/types.d.ts
vendored
1
packages/web/.astro/types.d.ts
vendored
@@ -1 +0,0 @@
|
|||||||
/// <reference types="astro/client" />
|
|
||||||
@@ -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 |
@@ -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 |
@@ -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",
|
||||||
@@ -47,4 +47,4 @@
|
|||||||
"valibot": "^1.0.0-rc.3",
|
"valibot": "^1.0.0-rc.3",
|
||||||
"zod": "^3.24.2"
|
"zod": "^3.24.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
47
sst-env.d.ts
vendored
@@ -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 {}
|
|
||||||
@@ -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;
|
|
||||||
},
|
|
||||||
});
|
|
||||||
Reference in New Issue
Block a user