This commit is contained in:
Harivansh Rathi 2026-04-01 03:45:34 +00:00
parent 4f174ec3a8
commit db1dea097f
81 changed files with 6263 additions and 545 deletions

View file

@ -12,6 +12,7 @@ Use it to keep the four product parts aligned:
## What belongs here
- OpenAPI source documents
- shared TypeScript types
- route constants
- JSON schemas for payloads we want to validate outside TypeScript
@ -28,6 +29,8 @@ Use it to keep the four product parts aligned:
- current runtime scaffold for health and version
- [`src/foundation.ts`](/home/rathi/Documents/GitHub/betterNAS/packages/contracts/src/foundation.ts)
- first product-level entities and route constants for node, mount, and cloud flows
- [`openapi/`](/home/rathi/Documents/GitHub/betterNAS/packages/contracts/openapi)
- language-neutral source documents for future SDK generation
- [`schemas/`](/home/rathi/Documents/GitHub/betterNAS/packages/contracts/schemas)
- JSON schema mirrors for the first shared entities

View file

@ -0,0 +1,307 @@
openapi: 3.1.0
info:
title: betterNAS Foundation API
version: 0.1.0
summary: Foundation contract for node registration, export metadata, and profile issuance.
servers:
- url: http://localhost:8081
paths:
/health:
get:
operationId: getControlPlaneHealth
responses:
"200":
description: Control-plane health
/version:
get:
operationId: getControlPlaneVersion
responses:
"200":
description: Control-plane version
/api/v1/nodes/register:
post:
operationId: registerNode
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/NodeRegistrationRequest"
responses:
"200":
description: Registered node
content:
application/json:
schema:
$ref: "#/components/schemas/NasNode"
/api/v1/nodes/{nodeId}/heartbeat:
post:
operationId: recordNodeHeartbeat
parameters:
- in: path
name: nodeId
required: true
schema:
type: string
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/NodeHeartbeatRequest"
responses:
"204":
description: Heartbeat accepted
/api/v1/exports:
get:
operationId: listExports
responses:
"200":
description: Export list
content:
application/json:
schema:
type: array
items:
$ref: "#/components/schemas/StorageExport"
/api/v1/mount-profiles/issue:
post:
operationId: issueMountProfile
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/MountProfileRequest"
responses:
"200":
description: Mount profile
content:
application/json:
schema:
$ref: "#/components/schemas/MountProfile"
/api/v1/cloud-profiles/issue:
post:
operationId: issueCloudProfile
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/CloudProfileRequest"
responses:
"200":
description: Cloud profile
content:
application/json:
schema:
$ref: "#/components/schemas/CloudProfile"
components:
schemas:
NasNode:
type: object
required:
- id
- machineId
- displayName
- agentVersion
- status
- lastSeenAt
- directAddress
- relayAddress
properties:
id:
type: string
machineId:
type: string
displayName:
type: string
agentVersion:
type: string
status:
type: string
enum: [online, offline, degraded]
lastSeenAt:
type: string
directAddress:
type:
- string
- "null"
relayAddress:
type:
- string
- "null"
StorageExport:
type: object
required:
- id
- nasNodeId
- label
- path
- protocols
- capacityBytes
- tags
properties:
id:
type: string
nasNodeId:
type: string
label:
type: string
path:
type: string
protocols:
type: array
items:
type: string
enum: [webdav]
capacityBytes:
type:
- integer
- "null"
tags:
type: array
items:
type: string
MountProfile:
type: object
required:
- id
- exportId
- protocol
- displayName
- mountUrl
- readonly
- credentialMode
properties:
id:
type: string
exportId:
type: string
protocol:
type: string
enum: [webdav]
displayName:
type: string
mountUrl:
type: string
readonly:
type: boolean
credentialMode:
type: string
enum: [session-token, app-password]
CloudProfile:
type: object
required:
- id
- exportId
- provider
- baseUrl
- path
properties:
id:
type: string
exportId:
type: string
provider:
type: string
enum: [nextcloud]
baseUrl:
type: string
path:
type: string
StorageExportInput:
type: object
required:
- label
- path
- protocols
- capacityBytes
- tags
properties:
label:
type: string
path:
type: string
protocols:
type: array
items:
type: string
enum: [webdav]
capacityBytes:
type:
- integer
- "null"
tags:
type: array
items:
type: string
NodeRegistrationRequest:
type: object
required:
- machineId
- displayName
- agentVersion
- directAddress
- relayAddress
- exports
properties:
machineId:
type: string
displayName:
type: string
agentVersion:
type: string
directAddress:
type:
- string
- "null"
relayAddress:
type:
- string
- "null"
exports:
type: array
items:
$ref: "#/components/schemas/StorageExportInput"
NodeHeartbeatRequest:
type: object
required:
- nodeId
- status
- lastSeenAt
properties:
nodeId:
type: string
status:
type: string
enum: [online, offline, degraded]
lastSeenAt:
type: string
MountProfileRequest:
type: object
required:
- userId
- deviceId
- exportId
properties:
userId:
type: string
deviceId:
type: string
exportId:
type: string
CloudProfileRequest:
type: object
required:
- userId
- exportId
- provider
properties:
userId:
type: string
exportId:
type: string
provider:
type: string
enum: [nextcloud]

View file

@ -7,6 +7,7 @@
"types": "./dist/index.d.ts",
"files": [
"dist",
"openapi",
"schemas"
],
"scripts": {
@ -14,4 +15,3 @@
"typecheck": "tsc --noEmit -p tsconfig.json"
}
}

View file

@ -0,0 +1,3 @@
# `@betternas/eslint-config`
Collection of internal eslint configurations.

View file

@ -0,0 +1,32 @@
import js from "@eslint/js";
import eslintConfigPrettier from "eslint-config-prettier";
import turboPlugin from "eslint-plugin-turbo";
import tseslint from "typescript-eslint";
import onlyWarn from "eslint-plugin-only-warn";
/**
* A shared ESLint configuration for the repository.
*
* @type {import("eslint").Linter.Config[]}
* */
export const config = [
js.configs.recommended,
eslintConfigPrettier,
...tseslint.configs.recommended,
{
plugins: {
turbo: turboPlugin,
},
rules: {
"turbo/no-undeclared-env-vars": "warn",
},
},
{
plugins: {
onlyWarn,
},
},
{
ignores: ["dist/**"],
},
];

View file

@ -0,0 +1,57 @@
import js from "@eslint/js";
import { globalIgnores } from "eslint/config";
import eslintConfigPrettier from "eslint-config-prettier";
import tseslint from "typescript-eslint";
import pluginReactHooks from "eslint-plugin-react-hooks";
import pluginReact from "eslint-plugin-react";
import globals from "globals";
import pluginNext from "@next/eslint-plugin-next";
import { config as baseConfig } from "./base.js";
/**
* A custom ESLint configuration for libraries that use Next.js.
*
* @type {import("eslint").Linter.Config[]}
* */
export const nextJsConfig = [
...baseConfig,
js.configs.recommended,
eslintConfigPrettier,
...tseslint.configs.recommended,
globalIgnores([
// Default ignores of eslint-config-next:
".next/**",
"out/**",
"build/**",
"next-env.d.ts",
]),
{
...pluginReact.configs.flat.recommended,
languageOptions: {
...pluginReact.configs.flat.recommended.languageOptions,
globals: {
...globals.serviceworker,
},
},
},
{
plugins: {
"@next/next": pluginNext,
},
rules: {
...pluginNext.configs.recommended.rules,
...pluginNext.configs["core-web-vitals"].rules,
},
},
{
plugins: {
"react-hooks": pluginReactHooks,
},
settings: { react: { version: "detect" } },
rules: {
...pluginReactHooks.configs.recommended.rules,
// React scope no longer necessary with new JSX transform.
"react/react-in-jsx-scope": "off",
},
},
];

View file

@ -0,0 +1,24 @@
{
"name": "@betternas/eslint-config",
"version": "0.0.0",
"type": "module",
"private": true,
"exports": {
"./base": "./base.js",
"./next-js": "./next.js",
"./react-internal": "./react-internal.js"
},
"devDependencies": {
"@eslint/js": "^9.39.1",
"@next/eslint-plugin-next": "^16.2.0",
"eslint": "^9.39.1",
"eslint-config-prettier": "^10.1.1",
"eslint-plugin-only-warn": "^1.1.0",
"eslint-plugin-react": "^7.37.5",
"eslint-plugin-react-hooks": "^5.2.0",
"eslint-plugin-turbo": "^2.7.1",
"globals": "^16.5.0",
"typescript": "^5.9.2",
"typescript-eslint": "^8.50.0"
}
}

View file

@ -0,0 +1,39 @@
import js from "@eslint/js";
import eslintConfigPrettier from "eslint-config-prettier";
import tseslint from "typescript-eslint";
import pluginReactHooks from "eslint-plugin-react-hooks";
import pluginReact from "eslint-plugin-react";
import globals from "globals";
import { config as baseConfig } from "./base.js";
/**
* A custom ESLint configuration for libraries that use React.
*
* @type {import("eslint").Linter.Config[]} */
export const config = [
...baseConfig,
js.configs.recommended,
eslintConfigPrettier,
...tseslint.configs.recommended,
pluginReact.configs.flat.recommended,
{
languageOptions: {
...pluginReact.configs.flat.recommended.languageOptions,
globals: {
...globals.serviceworker,
...globals.browser,
},
},
},
{
plugins: {
"react-hooks": pluginReactHooks,
},
settings: { react: { version: "detect" } },
rules: {
...pluginReactHooks.configs.recommended.rules,
// React scope no longer necessary with new JSX transform.
"react/react-in-jsx-scope": "off",
},
},
];

10
packages/sdk-ts/README.md Normal file
View file

@ -0,0 +1,10 @@
# `@betternas/sdk-ts`
Temporary TypeScript-facing SDK surface for the Next.js app.
The source of truth remains:
- [`packages/contracts/openapi`](/home/rathi/Documents/GitHub/betterNAS/packages/contracts/openapi)
- [`packages/contracts/src`](/home/rathi/Documents/GitHub/betterNAS/packages/contracts/src)
Later this package should become generated code from the OpenAPI contracts.

View file

@ -0,0 +1,3 @@
import { config } from "@betternas/eslint-config/base";
export default config;

View file

@ -0,0 +1,24 @@
{
"name": "@betternas/sdk-ts",
"version": "0.1.0",
"private": true,
"type": "module",
"exports": {
".": "./src/index.ts"
},
"scripts": {
"build": "tsc -p tsconfig.json",
"lint": "eslint . --max-warnings 0",
"check-types": "tsc --noEmit"
},
"dependencies": {
"@betternas/contracts": "*"
},
"devDependencies": {
"@betternas/eslint-config": "*",
"@betternas/typescript-config": "*",
"@types/node": "^22.15.3",
"eslint": "^9.39.1",
"typescript": "5.9.2"
}
}

View file

@ -0,0 +1 @@
export * from "@betternas/contracts";

View file

@ -0,0 +1,8 @@
{
"extends": "@betternas/typescript-config/base.json",
"compilerOptions": {
"outDir": "dist"
},
"include": ["src/**/*.ts"],
"exclude": ["node_modules", "dist"]
}

View file

@ -0,0 +1,19 @@
{
"$schema": "https://json.schemastore.org/tsconfig",
"compilerOptions": {
"declaration": true,
"declarationMap": true,
"esModuleInterop": true,
"incremental": false,
"isolatedModules": true,
"lib": ["es2022", "DOM", "DOM.Iterable"],
"module": "NodeNext",
"moduleDetection": "force",
"moduleResolution": "NodeNext",
"noUncheckedIndexedAccess": true,
"resolveJsonModule": true,
"skipLibCheck": true,
"strict": true,
"target": "ES2022"
}
}

View file

@ -0,0 +1,12 @@
{
"$schema": "https://json.schemastore.org/tsconfig",
"extends": "./base.json",
"compilerOptions": {
"plugins": [{ "name": "next" }],
"module": "ESNext",
"moduleResolution": "Bundler",
"allowJs": true,
"jsx": "preserve",
"noEmit": true
}
}

View file

@ -0,0 +1,9 @@
{
"name": "@betternas/typescript-config",
"version": "0.0.0",
"private": true,
"license": "MIT",
"publishConfig": {
"access": "public"
}
}

View file

@ -0,0 +1,7 @@
{
"$schema": "https://json.schemastore.org/tsconfig",
"extends": "./base.json",
"compilerOptions": {
"jsx": "react-jsx"
}
}

View file

@ -0,0 +1,4 @@
import { config } from "@betternas/eslint-config/react-internal";
/** @type {import("eslint").Linter.Config} */
export default config;

27
packages/ui/package.json Normal file
View file

@ -0,0 +1,27 @@
{
"name": "@betternas/ui",
"version": "0.0.0",
"private": true,
"exports": {
"./*": "./src/*.tsx"
},
"scripts": {
"build": "tsc -p tsconfig.json",
"lint": "eslint . --max-warnings 0",
"generate:component": "turbo gen react-component",
"check-types": "tsc --noEmit"
},
"devDependencies": {
"@betternas/eslint-config": "*",
"@betternas/typescript-config": "*",
"@types/node": "^22.15.3",
"@types/react": "19.2.2",
"@types/react-dom": "19.2.2",
"eslint": "^9.39.1",
"typescript": "5.9.2"
},
"dependencies": {
"react": "^19.2.0",
"react-dom": "^19.2.0"
}
}

View file

@ -0,0 +1,17 @@
"use client";
import { ReactNode } from "react";
interface ButtonProps {
children: ReactNode;
className?: string;
onClick?: () => void;
}
export const Button = ({ children, className, onClick }: ButtonProps) => {
return (
<button className={className} onClick={onClick} type="button">
{children}
</button>
);
};

22
packages/ui/src/card.tsx Normal file
View file

@ -0,0 +1,22 @@
import { type JSX } from "react";
export function Card({
className,
title,
children,
href,
}: {
className?: string;
title: string;
children: React.ReactNode;
href: string;
}): JSX.Element {
return (
<a className={className} href={href}>
<h2>
{title} <span>-&gt;</span>
</h2>
<p>{children}</p>
</a>
);
}

11
packages/ui/src/code.tsx Normal file
View file

@ -0,0 +1,11 @@
import { type JSX } from "react";
export function Code({
children,
className,
}: {
children: React.ReactNode;
className?: string;
}): JSX.Element {
return <code className={className}>{children}</code>;
}

View file

@ -0,0 +1,9 @@
{
"extends": "@betternas/typescript-config/react-library.json",
"compilerOptions": {
"outDir": "dist",
"strictNullChecks": true
},
"include": ["src"],
"exclude": ["node_modules", "dist"]
}