diff --git a/.dockerignore b/.dockerignore index 5bc266364..79ed5bc12 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,4 +1,40 @@ .dockerignore Dockerfile +docker-compose.yml + +# Dev / build artifacts (recreated inside the build stage) node_modules +dist +src/gui/dist +src/puter-js/dist +*.tsbuildinfo + +# Local runtime data +volatile +config.json +config.dev.json /puter + +# OS / editor +.DS_Store +.vscode +.idea + +# Git / CI +.git +.github + +# Logs +*.log +npm-debug.log* +.npm + +# Tests / coverage +coverage +.nyc_output + +# Secrets +.env +.env.* +creds* +*.pem diff --git a/.env.example b/.env.example new file mode 100644 index 000000000..b53caa16d --- /dev/null +++ b/.env.example @@ -0,0 +1,18 @@ +# Copy this file to `.env`, fill in the secrets, and `docker compose -f +# docker-compose.full.yml up -d`. None of the defaults below are safe for +# anything beyond a local laptop test. + +# ── Public-facing ports (nginx) --------------------------------------- +HTTP_PORT=80 +# HTTPS_PORT=443 # uncomment after you enable TLS in nginx/nginx.conf + +# ── MariaDB ------------------------------------------------------------ +MARIADB_ROOT_PASSWORD=replace-with-strong-password +MARIADB_DATABASE=puter +MARIADB_USER=puter +MARIADB_PASSWORD=replace-with-strong-password + +# ── S3 (RustFS) -------------------------------------------------------- +S3_ACCESS_KEY=puter +S3_SECRET_KEY=replace-with-strong-secret +S3_BUCKET=puter-local diff --git a/.github/workflows/docker-image.yaml b/.github/workflows/docker-image.yaml index 3ea3c4a80..e61655017 100644 --- a/.github/workflows/docker-image.yaml +++ b/.github/workflows/docker-image.yaml @@ -59,9 +59,17 @@ jobs: uses: docker/metadata-action@v5 with: images: "${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}" + # Tag plan: + # * version tag (vX.Y.Z) push → 1.2.3, 1.2, latest + # * branch push (main) → main + # selfhosted/docker.md tells users to pull `:latest`, which only + # resolves for tag pushes — never main, so unstable code can't + # claim `:latest`. tags: | type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} type=ref,event=branch + type=raw,value=latest,enable=${{ startsWith(github.ref, 'refs/tags/') }} # This step uses the `docker/build-push-action` action to build the # image, based on your repository's `Dockerfile`. If the build succeeds, diff --git a/Dockerfile b/Dockerfile index b9abf58ba..15bb3d133 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,91 +1,88 @@ -# /!\ NOTICE /!\ +# syntax=docker/dockerfile:1.7 +# +# OSS Puter image — multi-arch (linux/amd64, linux/arm64). +# +# Build & push: +# docker buildx build --platform linux/amd64,linux/arm64 \ +# -t ghcr.io/heyputer/puter:latest --push . +# +# Local single-arch build: +# docker build -t puter . +# +# Self-hosters inject configuration by mounting a config.json at +# /etc/puter/config.json. It is deep-merged over the bundled +# config.default.json, so partial overrides work. Absent file = defaults. -# Many of the developers DO NOT USE the Dockerfile or image. -# While we do test new changes to Docker configuration, it's -# possible that future changes to the repo might break it. -# When changing this file, please try to make it as resiliant -# to such changes as possible; developers shouldn't need to -# worry about Docker unless the build/run process changes. +# ---- Build stage ---- +FROM node:24-slim AS build -# Build stage -FROM node:24-alpine AS build +WORKDIR /opt/puter -# Install build dependencies -RUN apk add --no-cache git python3 make g++ \ - && ln -sf /usr/bin/python3 /usr/bin/python +# Build toolchain needed for native deps (bcrypt, sharp, better-sqlite3, …). +RUN apt-get update && \ + apt-get install -y --no-install-recommends python3 make g++ git && \ + rm -rf /var/lib/apt/lists/* -# Set up working directory -WORKDIR /app +ENV HUSKY=0 +ENV npm_config_fund=false +ENV npm_config_audit=false -# Copy package.json and package-lock.json +# ---- Dependency layer --------------------------------------------------- +# Copy ONLY package manifests + lockfile first so the npm-install layer +# stays cached when only source files change. COPY package.json package-lock.json ./ +COPY src/backend/package.json src/backend/ +COPY src/gui/package.json src/gui/ +COPY src/puter-js/package.json src/puter-js/package-lock.json src/puter-js/ +COPY src/worker/package.json src/worker/ +COPY src/docs/package.json src/docs/ -# Fail early if lockfile or manifest is missing -RUN test -f package.json && test -f package-lock.json +# extensionSetup.mjs runs as the postinstall hook during npm ci. (No-ops +# unless any packages/puter/extensions/* gain a package.json.) +COPY tools/extensionSetup.mjs tools/extensionSetup.mjs -# Copy the source files +RUN --mount=type=cache,target=/root/.npm \ + npm ci + +# ---- Source layer ------------------------------------------------------- COPY . . -# Install mocha -RUN npm i -g npm@latest -RUN npm install -g mocha +# Compile backend TS, then build GUI + puter-js webpack bundles in +# parallel. The GUI/puter-js bundles are how /dist/bundle.min.{js,css} +# and /sdk/puter.js fall back to local assets when the kernel-config +# CDN keys are unset. +RUN npm run build:ts +RUN set -e; \ + (cd src/gui && node ./build.js) & gui_pid=$!; \ + (cd src/puter-js && npm run build) & pjs_pid=$!; \ + wait $gui_pid; \ + wait $pjs_pid -# Install node modules -RUN npm cache clean --force && \ - for i in 1 2 3; do \ - npm ci && break || \ - if [ $i -lt 3 ]; then \ - sleep 15; \ - else \ - LOG_DIR="$(npm config get cache | tr -d '\"')/_logs"; \ - echo "npm install failed; dumping logs from $LOG_DIR"; \ - if [ -d "$LOG_DIR" ]; then \ - ls -al "$LOG_DIR" || true; \ - cat "$LOG_DIR"/* || true; \ - else \ - echo "Log directory not found (npm cache: $(npm config get cache))"; \ - fi; \ - exit 1; \ - fi; \ - done +# ---- Runtime stage (slim — no build tools) ---- +FROM node:24-slim -# Run the build command if necessary -RUN cd src/gui && npm run build && cd - +WORKDIR /opt/puter -# Production stage -FROM node:24-alpine +# git: runtime version probe. wget: HEALTHCHECK. +RUN apt-get update && \ + apt-get install -y --no-install-recommends git wget && \ + rm -rf /var/lib/apt/lists/* -# Set labels -LABEL repo="https://github.com/HeyPuter/puter" -LABEL license="AGPL-3.0,https://github.com/HeyPuter/puter/blob/master/LICENSE.txt" -LABEL version="1.2.46-beta-1" +COPY --from=build --chown=node:node /opt/puter . -# Install git (required by Puter to check version) -RUN apk add --no-cache git +RUN mkdir -p /etc/puter /var/puter && \ + chown -R node:node /etc/puter /var/puter -# Set up working directory -RUN mkdir -p /opt/puter/app -WORKDIR /opt/puter/app - -# Copy built artifacts and necessary files from the build stage -COPY --from=build /app/src/gui/dist ./dist -COPY --from=build /app/node_modules ./node_modules -COPY . . - -# Set permissions -RUN chown -R node:node /opt/puter/app -USER node +# Self-hosters mount their override at this exact path. The v2 loader +# deep-merges it over config.default.json (see backend/index.ts). +ENV PUTER_CONFIG_PATH=/etc/puter/config.json +ENV NODE_OPTIONS=--enable-source-maps EXPOSE 4100 -HEALTHCHECK --interval=30s --timeout=3s \ +USER node + +HEALTHCHECK --interval=30s --timeout=3s --start-period=30s --retries=3 \ CMD wget --no-verbose --tries=1 --spider http://puter.localhost:4100/test || exit 1 -ENV NO_VAR_RUNTUME=1 -ENV NODE_OPTIONS=--enable-source-maps - -# Attempt to fix `lru-cache@11.0.2` missing after build stage -# by doing a redundant `npm install` at this stage -RUN npm install - -CMD ["npm", "start"] +CMD ["node", "-r", "./dist/src/backend/telemetry.js", "./dist/src/backend/index.js"] diff --git a/docker-compose.full.yml b/docker-compose.full.yml new file mode 100644 index 000000000..34d365f34 --- /dev/null +++ b/docker-compose.full.yml @@ -0,0 +1,217 @@ +--- +# Self-hosted Puter — full stack. +# +# Brings up Puter + every external service it needs: +# - nginx : reverse proxy (mirrors prod ALB; handles TLS + Host fan-out) +# - valkey : redis-compatible cache / rate-limiter backend +# - mariadb : SQL database (Puter applies its schema on first boot) +# - dynamo : DynamoDB-local (KV store; Puter creates the table itself) +# - s3 : RustFS — S3-compatible object storage +# - s3-init : one-shot init container that creates the bucket +# - puter : the application +# +# Quick start: +# 1. Copy .env.example to .env (or set the variables in your shell). +# 2. Drop a config.json into ./puter/config/ — see selfhosted/full-stack.md +# for the example that pairs with this compose. +# 3. docker compose -f docker-compose.full.yml up -d +# +# Production: +# - Always replace the default passwords / S3 keys / Puter secrets. +# - Front Puter with TLS-terminating reverse proxy (Caddy / nginx). +# - Move state-bearing volumes to a backed-up location. + +services: + valkey: + image: valkey/valkey:8-alpine + container_name: puter-valkey + restart: unless-stopped + # Run as a single-node cluster so Puter's ioredis Cluster client + # (the only mode it speaks) can connect. On first boot we assign all + # 16384 slots to ourselves; subsequent boots find them already in + # nodes.conf and skip. `cluster-require-full-coverage no` keeps reads + # working if we ever land partial slots. + command: + - sh + - -c + - | + valkey-server \ + --port 6379 \ + --cluster-enabled yes \ + --cluster-config-file /data/nodes.conf \ + --cluster-node-timeout 5000 \ + --cluster-require-full-coverage no \ + --cluster-announce-ip valkey \ + --cluster-announce-port 6379 \ + --cluster-announce-bus-port 16379 \ + --appendonly yes \ + --save "60 1" & + SERVER_PID=$$! + until valkey-cli -p 6379 PING > /dev/null 2>&1; do sleep 0.5; done + if ! valkey-cli -p 6379 CLUSTER NODES | grep -q '0-16383'; then + valkey-cli -p 6379 CLUSTER ADDSLOTSRANGE 0 16383 + fi + wait $$SERVER_PID + volumes: + - ./puter/data/valkey:/data + healthcheck: + test: + ["CMD-SHELL", "valkey-cli -p 6379 cluster info | grep -q cluster_state:ok"] + interval: 5s + timeout: 3s + retries: 20 + start_period: 10s + + mariadb: + image: mariadb:11 + container_name: puter-mariadb + restart: unless-stopped + environment: + MARIADB_ROOT_PASSWORD: ${MARIADB_ROOT_PASSWORD:-root-change-me} + MARIADB_DATABASE: ${MARIADB_DATABASE:-puter} + MARIADB_USER: ${MARIADB_USER:-puter} + MARIADB_PASSWORD: ${MARIADB_PASSWORD:-puter-change-me} + volumes: + - ./puter/data/mariadb:/var/lib/mysql + healthcheck: + # `healthcheck.sh` ships with the mariadb image; --connect verifies + # the server is accepting auth, not just listening on the socket. + test: ["CMD", "healthcheck.sh", "--connect", "--innodb_initialized"] + interval: 5s + timeout: 5s + retries: 20 + start_period: 30s + + dynamo: + # Puter creates the `store-kv-v1` table itself on startup + # (config.dynamo.bootstrapTables = true does the work). + image: amazon/dynamodb-local:latest + container_name: puter-dynamo + restart: unless-stopped + user: "1000:1000" + working_dir: /home/dynamodblocal + command: + - "-jar" + - "DynamoDBLocal.jar" + - "-sharedDb" + - "-dbPath" + - "/home/dynamodblocal/data" + volumes: + - ./puter/data/dynamo:/home/dynamodblocal/data + + s3: + # RustFS — S3-compatible object storage. Drop-in alternative: + # MinIO (image: minio/minio, command: ["server", "/data", "--console-address", ":9001"]). + image: rustfs/rustfs:latest + container_name: puter-s3 + restart: unless-stopped + environment: + RUSTFS_ACCESS_KEY: ${S3_ACCESS_KEY:-puter} + RUSTFS_SECRET_KEY: ${S3_SECRET_KEY:-puter-secret-change-me} + volumes: + - ./puter/data/s3:/data + healthcheck: + # RustFS exposes /health on the S3 port. Use wget (curl is not in + # the slim image). + test: + [ + "CMD-SHELL", + "wget -qO- --tries=1 --timeout=2 http://localhost:9000/health || exit 1", + ] + interval: 5s + timeout: 3s + retries: 20 + start_period: 5s + + s3-init: + # One-shot container that creates the `puter-local` bucket on first + # boot. Exits 0 once the bucket exists; stays exited 0 thereafter. + image: amazon/aws-cli:latest + container_name: puter-s3-init + depends_on: + s3: + condition: service_healthy + environment: + AWS_ACCESS_KEY_ID: ${S3_ACCESS_KEY:-puter} + AWS_SECRET_ACCESS_KEY: ${S3_SECRET_KEY:-puter-secret-change-me} + AWS_DEFAULT_REGION: us-east-1 + entrypoint: + - /bin/sh + - -c + - | + set -e + endpoint=http://s3:9000 + bucket=${S3_BUCKET:-puter-local} + if aws --endpoint-url "$$endpoint" s3api head-bucket --bucket "$$bucket" 2>/dev/null; then + echo "bucket $$bucket already exists" + else + echo "creating bucket $$bucket" + aws --endpoint-url "$$endpoint" s3 mb "s3://$$bucket" + fi + restart: "no" + + puter: + # image: ghcr.io/heyputer/puter:latest + pull_policy: always + # Uncomment to build from this directory instead of pulling the published + # image. Also flip pull_policy to `never` so compose doesn't overwrite + # your local build by re-pulling :latest. + build: + context: . + # buildx-only: cross-compile to both archs in a single push + platforms: + # - linux/amd64 + - linux/arm64 + container_name: puter + restart: unless-stopped + depends_on: + valkey: + condition: service_healthy + mariadb: + condition: service_healthy + dynamo: + condition: service_started + s3-init: + condition: service_completed_successfully + # Internal-only: nginx reaches it on the compose network. Uncomment + # to also expose port 4100 directly on the host (useful for debugging). + # ports: + # - "4100:4100" + expose: + - "4100" + environment: + PUID: 1000 + PGID: 1000 + volumes: + # Drop your config.json here — see selfhosted/full-stack.md. + - ./puter/config:/etc/puter + # Persistent runtime data (anything your config points at /var/puter). + - ./puter/data/puter:/var/puter + healthcheck: + test: wget --no-verbose --tries=1 --spider http://puter.localhost:4100/test || exit 1 + interval: 30s + timeout: 3s + retries: 3 + start_period: 30s + + nginx: + image: nginx:1.27-alpine + container_name: puter-nginx + restart: unless-stopped + depends_on: + puter: + condition: service_started + ports: + - "${HTTP_PORT:-80}:80" + # Uncomment when you enable TLS in nginx/nginx.conf: + # - "${HTTPS_PORT:-443}:443" + volumes: + - ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro + # TLS certs (fullchain.pem + privkey.pem). Read-only inside. + - ./puter/tls:/etc/nginx/tls:ro + healthcheck: + test: ["CMD-SHELL", "wget -qO- --tries=1 --timeout=2 http://localhost/ || exit 1"] + interval: 10s + timeout: 3s + retries: 5 + start_period: 5s diff --git a/docker-compose.yml b/docker-compose.yml index f6edb79ff..e5e1cb7bc 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,21 +1,30 @@ --- -version: "3.8" services: puter: container_name: puter image: ghcr.io/heyputer/puter:latest pull_policy: always - # build: ./ + # Uncomment to build from this directory instead of pulling the published image: + # build: + # context: . + # # buildx-only: cross-compile to both archs in a single push + # # platforms: + # # - linux/amd64 + # # - linux/arm64 restart: unless-stopped ports: - '4100:4100' environment: # TZ: Europe/Paris - # CONFIG_PATH: /etc/puter PUID: 1000 PGID: 1000 volumes: + # Drop your config.json into ./puter/config/. It is deep-merged over + # config.default.json — only override what you care to change. + # Image expects /etc/puter/config.json (see PUTER_CONFIG_PATH in Dockerfile). - ./puter/config:/etc/puter + # Persistent runtime data (sqlite db, uploads, etc. — depends on your + # config). Maps to volatile/ inside the container by default. - ./puter/data:/var/puter healthcheck: test: wget --no-verbose --tries=1 --spider http://puter.localhost:4100/test || exit 1 diff --git a/nginx/nginx.conf b/nginx/nginx.conf new file mode 100644 index 000000000..90de1d473 --- /dev/null +++ b/nginx/nginx.conf @@ -0,0 +1,85 @@ +# Reverse proxy in front of Puter — mirrors what the prod ALB does: +# accepts every Host header, forwards to the Puter container, and lets +# the Puter app handle subdomain-based routing internally (api.*, +# site.*, app.*, etc). +# +# To enable TLS: +# 1. Drop your fullchain.pem + privkey.pem into ./puter/tls/. +# 2. Uncomment the 443 server{} block below. +# 3. Update server_name to your domain (and wildcard subdomains). + +worker_processes auto; +events { + worker_connections 4096; +} + +http { + # Required for Puter's WebSocket / socket.io upgrades. + map $http_upgrade $connection_upgrade { + default upgrade; + '' close; + } + + # Rough size cap that mirrors prod ALB defaults; tune for your + # uploads. Puter chunks large uploads, so 1 GiB per request is plenty. + client_max_body_size 1024m; + proxy_read_timeout 600s; + proxy_send_timeout 600s; + proxy_buffering off; + server_tokens off; + + upstream puter_backend { + server puter:4100; + keepalive 32; + } + + # ── HTTP (port 80) — catches all hostnames ───────────────────── + server { + listen 80 default_server; + listen [::]:80 default_server; + server_name _; + + # Note: when you enable TLS, replace this block with a redirect: + # return 301 https://$host$request_uri; + location / { + proxy_pass http://puter_backend; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Forwarded-Host $host; + proxy_set_header X-Forwarded-Port $server_port; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection $connection_upgrade; + } + } + + # ── HTTPS (port 443) — uncomment after dropping certs in ./puter/tls/ ─ + # server { + # listen 443 ssl default_server; + # listen [::]:443 ssl default_server; + # http2 on; + # server_name _; + # + # ssl_certificate /etc/nginx/tls/fullchain.pem; + # ssl_certificate_key /etc/nginx/tls/privkey.pem; + # ssl_protocols TLSv1.2 TLSv1.3; + # ssl_ciphers HIGH:!aNULL:!MD5; + # ssl_session_cache shared:SSL:10m; + # ssl_session_timeout 10m; + # + # location / { + # proxy_pass http://puter_backend; + # proxy_http_version 1.1; + # proxy_set_header Host $host; + # proxy_set_header X-Real-IP $remote_addr; + # proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + # proxy_set_header X-Forwarded-Proto $scheme; + # proxy_set_header X-Forwarded-Host $host; + # proxy_set_header X-Forwarded-Port $server_port; + # proxy_set_header Upgrade $http_upgrade; + # proxy_set_header Connection $connection_upgrade; + # } + # } +} diff --git a/package-lock.json b/package-lock.json index 6562ae481..8dfd134d6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -452,7 +452,6 @@ "resolved": "https://registry.npmjs.org/@aws-sdk/client-dynamodb/-/client-dynamodb-3.1032.0.tgz", "integrity": "sha512-kkXiZBNdWCQAg/8opqAu10TxzdpqMkcGrNAT2ScdfWhCpzYZ2pmSpP8W7BOlA32jYIWnYrEdb808UZsNWYBPAA==", "license": "Apache-2.0", - "peer": true, "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", @@ -1748,7 +1747,6 @@ } ], "license": "MIT", - "peer": true, "engines": { "node": ">=20.19.0" }, @@ -1797,7 +1795,6 @@ } ], "license": "MIT", - "peer": true, "engines": { "node": ">=20.19.0" } @@ -1812,6 +1809,29 @@ "node": ">=10.0.0" } }, + "node_modules/@emnapi/core": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz", + "integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.1", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz", + "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, "node_modules/@emnapi/wasi-threads": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", @@ -3073,7 +3093,6 @@ "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.1.tgz", "integrity": "sha512-gLyJlPHPZYdAk1JENA9LeHejZe1Ti77/pTeFm/nMXmQH/HFZlcS/O2XJB+L8fkbrNSqhdtlvjBVjxwUYanNH5Q==", "license": "Apache-2.0", - "peer": true, "engines": { "node": ">=8.0.0" } @@ -6807,7 +6826,6 @@ "integrity": "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@types/estree": "*", "@types/json-schema": "*" @@ -6952,7 +6970,6 @@ "resolved": "https://registry.npmjs.org/@types/node/-/node-24.12.2.tgz", "integrity": "sha512-A1sre26ke7HDIuY/M23nd9gfB+nrmhtYyMINbjI1zHJxYteKR6qSMX56FsmjMcDb3SMcjJg5BiRRgOCC/yBD0g==", "license": "MIT", - "peer": true, "dependencies": { "undici-types": "~7.16.0" } @@ -7094,7 +7111,6 @@ "integrity": "sha512-/Zb/xaIDfxeJnvishjGdcR4jmr7S+bda8PKNhRGdljDM+elXhlvN0FyPSsMnLmJUrVG9aPO6dof80wjMawsASg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.58.2", "@typescript-eslint/types": "8.58.2", @@ -7380,7 +7396,6 @@ "integrity": "sha512-x7FptB5oDruxNPDNY2+S8tCh0pcq7ymCe1gTHcsp733jYjrJl8V1gMUlVysuCD9Kz46Xz9t1akkv08dPcYDs1w==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@bcoe/v8-coverage": "^1.0.2", "@vitest/utils": "4.1.4", @@ -7520,7 +7535,6 @@ "integrity": "sha512-EgFR7nlj5iTDYZYCvavjFokNYwr3c3ry0sFiCg+N7B233Nwp+NNx7eoF/XvMWDCKY71xXAG3kFkt97ZHBJVL8A==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@vitest/utils": "4.1.4", "fflate": "^0.8.2", @@ -7858,7 +7872,6 @@ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -8478,7 +8491,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.10.12", "caniuse-lite": "^1.0.30001782", @@ -8660,7 +8672,6 @@ "resolved": "https://registry.npmjs.org/chai/-/chai-4.5.0.tgz", "integrity": "sha512-RITGBfijLkBddZvnn8jdqoTypxvqbOLYQkGGxXzeFjVHvudaPw0HNFD9x928/eUwYWd2dPCugVqspGALTZZQKw==", "license": "MIT", - "peer": true, "dependencies": { "assertion-error": "^1.1.0", "check-error": "^1.0.3", @@ -9251,7 +9262,6 @@ } ], "license": "MIT", - "peer": true, "engines": { "node": ">=18" }, @@ -9274,7 +9284,6 @@ } ], "license": "MIT", - "peer": true, "engines": { "node": ">=18" } @@ -9950,7 +9959,6 @@ "integrity": "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -10011,7 +10019,6 @@ "integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==", "dev": true, "license": "MIT", - "peer": true, "bin": { "eslint-config-prettier": "bin/cli.js" }, @@ -10645,7 +10652,6 @@ "resolved": "https://registry.npmjs.org/fengari/-/fengari-0.1.5.tgz", "integrity": "sha512-0DS4Nn4rV8qyFlQCpKK8brT61EUtswynrpfFTcgLErcilBIBskSMQ86fO2WVuybr14ywyKdRjv91FiRZwnEuvQ==", "license": "MIT", - "peer": true, "dependencies": { "readline-sync": "^1.4.10", "sprintf-js": "^1.1.3", @@ -11908,7 +11914,6 @@ "resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.10.1.tgz", "integrity": "sha512-HuEDBTI70aYdx1v6U97SbNx9F1+svQKBDo30o0b9fw055LMepzpOOd0Ccg9Q6tbqmBSJaMuY0fB7yw9/vjBYCA==", "license": "MIT", - "peer": true, "dependencies": { "@ioredis/commands": "1.5.1", "cluster-key-slot": "^1.1.0", @@ -14868,7 +14873,6 @@ "integrity": "sha512-7igPTM53cGHMW8xWuVTydi2KO233VFiTNyF5hLJqpilHfmn8C8gPf+PS7dUT64YcXFbiMGZxS9pCSxL/Dxm/Jw==", "dev": true, "license": "MIT", - "peer": true, "bin": { "prettier": "bin/prettier.cjs" }, @@ -15622,7 +15626,6 @@ "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -16159,7 +16162,6 @@ "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.6.tgz", "integrity": "sha512-DkkO/dz7MGln0dHn5bmN3pPy+JmywNICWrJqVWiVOyvXjWQFIv9c2h24JrQLLFJ2aQVQf/Cvl1vblnd4r2apLQ==", "license": "MIT", - "peer": true, "dependencies": { "debug": "~4.4.1", "ws": "~8.18.3" @@ -16988,7 +16990,6 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "devOptional": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -17213,7 +17214,6 @@ "integrity": "sha512-t7g7GVRpMXjNpa67HaVWI/8BWtdVIQPCL2WoozXXA7LBGEFK4AkkKkHx2hAQf5x1GZSlcmEDPkVLSGahxnEEZw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "lightningcss": "^1.32.0", "picomatch": "^4.0.4", @@ -17409,7 +17409,6 @@ "integrity": "sha512-tFuJqTxKb8AvfyqMfnavXdzfy3h3sWZRWwfluGbkeR7n0HUev+FmNgZ8SDrRBTVrVCjgH5cA21qGbCffMNtWvg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@vitest/expect": "4.1.4", "@vitest/mocker": "4.1.4", @@ -17541,7 +17540,6 @@ "integrity": "sha512-wGN3qcrBQIFmQ/c0AiOAQBvrZ5lmY8vbbMv4Mxfgzqd/B6+9pXtLo73WuS1dSGXM5QYY3hZnIbvx+K1xxe6FyA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@types/eslint-scope": "^3.7.7", "@types/estree": "^1.0.8", @@ -17590,7 +17588,6 @@ "integrity": "sha512-pIDJHIEI9LR0yxHXQ+Qh95k2EvXpWzZ5l+d+jIo+RdSm9MiHfzazIxwwni/p7+x4eJZuvG1AJwgC4TNQ7NRgsg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@discoveryjs/json-ext": "^0.5.0", "@webpack-cli/configtest": "^2.1.1", @@ -17962,7 +17959,6 @@ "integrity": "sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg==", "dev": true, "license": "ISC", - "peer": true, "bin": { "yaml": "bin.mjs" }, @@ -18029,7 +18025,6 @@ "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz", "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", "license": "MIT", - "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/package.json b/package.json index adbc576ea..8cf84dfc1 100644 --- a/package.json +++ b/package.json @@ -54,7 +54,7 @@ "check-translations": "node tools/check-translations.js", "prepare": "husky", "build:ts": "tsc -p tsconfig.json && node ./tools/write-dist-package-json.mjs", - "postinstall": "./tools/extensionSetup.sh" + "postinstall": "node ./tools/extensionSetup.mjs" }, "workspaces": [ "src/*", diff --git a/selfhosted/README.md b/selfhosted/README.md new file mode 100644 index 000000000..899e64b1d --- /dev/null +++ b/selfhosted/README.md @@ -0,0 +1,25 @@ +# Self-hosting Puter + +Three supported ways to run Puter, in increasing order of effort and capability. Pick one, follow that page, ignore the others. + +| Mode | Best for | External services | +| ------------------------------------------ | ----------------------------------------------------- | ----------------------------------------------------------- | +| [**1. Dev (npm start)**](./npm.md) | Hacking on the source / trying it on your laptop | None — everything runs in-process | +| [**2. Docker (single container)**](./docker.md) | Production single-host; bring your own DB / S3 | None bundled — point at services you already run | +| [**3. Full self-hosted stack**](./full-stack.md) | Production with a self-managed stack | Bundled: MariaDB, Valkey, DynamoDB-local, RustFS S3, nginx | + +--- + +## 1. Dev (`npm start`) → [npm.md](./npm.md) + +Clone, `npm install`, `npm start`. Backend, GUI, and `puter.js` run from the source tree on Node 24+. SQLite + in-process S3 / DynamoDB / Redis stand-ins start automatically — no external services needed. Best for contributing or kicking the tires. + +**Not safe to expose publicly** — uses dev secrets and an in-process key store. + +## 2. Docker (single container) → [docker.md](./docker.md) + +One `docker run` against `ghcr.io/heyputer/puter:latest`. Out of the box uses the same in-process defaults as dev mode; drop a `config.json` into the mounted `/etc/puter/` to point at real services (MariaDB, S3, DynamoDB, Redis) one block at a time. Best when you already operate the dependencies you want Puter to use. + +## 3. Full self-hosted stack → [full-stack.md](./full-stack.md) + +`docker compose -f docker-compose.full.yml up -d` brings up Puter **plus every external service it needs** (MariaDB, Valkey, DynamoDB-local, RustFS S3, nginx) wired together. Closest to production you can run on a single host; supports your own domain and TLS. Best when you want a public Puter and don't already run the dependencies. diff --git a/selfhosted/docker.md b/selfhosted/docker.md new file mode 100644 index 000000000..840a095c1 --- /dev/null +++ b/selfhosted/docker.md @@ -0,0 +1,180 @@ +# 2. Docker (single container) + +One Puter container. You bring your own database, S3, etc. — or run with the bundled in-process defaults for a quick spin. The image is multi-arch (`linux/amd64`, `linux/arm64`). + +## Requirements + +- **Docker** (any recent version). + +## Bare minimum — defaults, single command + +```bash +mkdir -p puter/config puter/data + +docker run -d \ + --name puter \ + --restart unless-stopped \ + -p 4100:4100 \ + -v $(pwd)/puter/config:/etc/puter \ + -v $(pwd)/puter/data:/var/puter \ + ghcr.io/heyputer/puter:latest +``` + +Open . With nothing in `puter/config/`, the in-process defaults kick in (same SQLite + dynalite + fauxqs + redis-mock as dev mode). State lands in `puter/data/`. Login is `admin` — temp password is printed once in `docker logs puter`. + +That's enough to confirm the image works. Now configure for real. + +## Add a config + +The container reads **`/etc/puter/config.json`** and deep-merges it on top of the bundled defaults. You only put the keys you want to change. + +```bash +cat > puter/config/config.json <<'JSON' +{ + "domain": "puter.example.com", + "protocol": "https", + "pub_port": 443, + "env": "prod", + + "jwt_secret": "REPLACE-WITH-openssl-rand-hex-64", + "url_signature_secret": "REPLACE-WITH-A-DIFFERENT-openssl-rand-hex-64" +} +JSON + +docker restart puter +``` + +> 🔒 **Always replace `jwt_secret` and `url_signature_secret`.** The defaults are baked into the public image. Generate with `openssl rand -hex 64`. + +Watch the logs: + +```bash +docker logs -f puter +``` + +Look for `[config] override from /etc/puter/config.json` — that's the success signal. + +## Wire to external services + +Drop the relevant block(s) into `config.json`. Mix and match. Restart with `docker restart puter` after any change. + +### MySQL / MariaDB + +Puter applies its schema on first boot when you set `migrationPaths`: + +```json +{ + "database": { + "engine": "mysql", + "host": "db.internal", "port": 3306, + "user": "puter", "password": "...", "database": "puter", + "migrationPaths": ["/opt/puter/dist/src/backend/clients/database/migrations/mysql"] + } +} +``` + +Two files run in order: `mysql_mig_1.sql` (tables) and `mysql_mig_2.sql` (default apps — editor, viewer, pdf, camera, player, recorder, git, dev-center, puter-linux). Both are idempotent — safe to re-run. + +### S3 (real or S3-compatible) + +```json +{ + "s3": { + "s3Config": { + "endpoint": "https://s3.example.com", + "accessKeyId": "...", "secretAccessKey": "...", + "region": "us-east-1" + } + }, + "s3_bucket": "my-puter-bucket", + "s3_region": "us-east-1" +} +``` + +The bucket must exist already — Puter doesn't create it. + +> ⚠️ **S3 uses camelCase keys** (`accessKeyId` / `secretAccessKey`). DynamoDB below uses snake_case. They're not the same. + +### DynamoDB (real AWS) + +Provision the table externally (e.g. Terraform): + +```json +{ + "dynamo": { + "aws": { "access_key": "...", "secret_key": "...", "region": "us-east-1" } + } +} +``` + +The KV table is named `store-kv-v1`. Schema: hash `namespace` (S), range `key` (S), LSI `lsi1-index` on `lsi1` (S), TTL on `ttl`. + +### Redis / Valkey cluster + +Puter speaks ioredis cluster protocol. Real Redis cluster: + +```json +{ "redis": { "startupNodes": [{ "host": "redis-0", "port": 6379 }] } } +``` + +For a self-hosted single Valkey/Redis container, run it in cluster mode (one node, all 16384 slots assigned to itself) and turn off TLS: + +```json +{ + "redis": { + "startupNodes": [{ "host": "valkey", "port": 6379 }], + "tls": false + } +} +``` + +TLS defaults to on (matches the prod ElastiCache shape) — set `false` for plain-TCP self-host. + +## What persists? + +Anything your config points at `/var/puter/...` lives on the host via the `puter/data` mount (SQLite path, fauxqs data dirs if you use them, etc.). If you've moved every dependency to external services, the data volume is mostly empty and optional. + +## Updating + +```bash +docker pull ghcr.io/heyputer/puter:latest +docker rm -f puter +# re-run the docker run command above +``` + +Your `config.json` and persistent data are untouched. + +## Building the image yourself + +```bash +docker build -t puter . + +# Multi-arch (requires buildx, on by default in modern Docker): +docker buildx build \ + --platform linux/amd64,linux/arm64 \ + -t your-registry/puter:latest \ + --push . +``` + +A `docker-compose.yml` in this directory has a commented-out `build:` block — uncomment it (and flip `pull_policy` to `never`) to build from your local checkout instead of pulling. + +## Troubleshooting + +**`docker logs puter` shows the container restarting.** +Almost always JSON syntax in `config.json`. Validate: `jq . puter/config/config.json`. + +**The config file isn't picked up.** +Confirm it resolves to `/etc/puter/config.json` *inside* the container: +```bash +docker exec puter cat /etc/puter/config.json +``` +Empty / missing → the volume mount path is wrong. + +**Healthcheck reports unhealthy but the site works.** +The healthcheck hits `puter.localhost:4100/test` from inside the container. If you changed `domain` or `port`, the check still uses defaults. The site itself is fine. + +**`Error: DynamoDB aws config requires both access_key and secret_key`.** +You wrote `accessKeyId` / `secretAccessKey` (the AWS SDK form) under `dynamo.aws`. DynamoDB config uses snake_case. See above. + +**Architecture mismatch on Apple Silicon / ARM hosts.** +Use the published `:latest` tag — it's already multi-arch. If you built locally with `docker build` on an Intel Mac, the resulting image will be `linux/amd64` only. diff --git a/selfhosted/full-stack.md b/selfhosted/full-stack.md new file mode 100644 index 000000000..10266ad59 --- /dev/null +++ b/selfhosted/full-stack.md @@ -0,0 +1,243 @@ +# 3. Full self-hosted stack + +`docker-compose.full.yml` brings up Puter **plus every external service it needs** — MariaDB, Valkey, DynamoDB-local, RustFS S3, nginx — wired together. Closest thing to a production deployment you can self-manage on a single host. + +## Requirements + +- **Docker** with the `compose` plugin. +- A **domain** with DNS access — you need a wildcard record (`*.your-domain.com` → server IP). Puter routes by subdomain (`api.`, `site.`, `app.`). +- Optional: **TLS certs** (or `certbot` to grab them — see Step 4). + +## What's running + +| Container | Image | Role | +| --------------- | ------------------------ | ---------------------------------------------------------- | +| `puter-nginx` | `nginx:1.27-alpine` | Reverse proxy on 80 (and 443 if TLS); forwards to Puter | +| `puter` | `ghcr.io/heyputer/puter` | The app | +| `puter-mariadb` | `mariadb:11` | SQL database — schema applied automatically on first boot | +| `puter-valkey` | `valkey/valkey:8-alpine` | Redis-compatible cache + rate-limiter | +| `puter-dynamo` | `amazon/dynamodb-local` | KV store — table auto-created on first boot | +| `puter-s3` | `rustfs/rustfs` | S3-compatible object storage (MinIO drop-in noted in file) | +| `puter-s3-init` | `amazon/aws-cli` | One-shot — creates the bucket on first boot, then exits | + +State lives under `./puter/data//`. + +--- + +## Step 1 — Create `.env` and `puter/config/config.json` + +> ⚠️ **Run this whole block in one shell session.** It generates secrets once and writes them into both `.env` (read by docker compose) and `config.json` (read by Puter). The two files **must** agree on the MariaDB password and the S3 secret — if they drift, MariaDB initialises with one password and Puter tries to log in with another, and you get `ER_ACCESS_DENIED_ERROR`. + +```bash +MARIADB_ROOT_PASSWORD=$(openssl rand -hex 32) +MARIADB_PASSWORD=$(openssl rand -hex 32) +S3_SECRET_KEY=$(openssl rand -hex 32) +JWT_SECRET=$(openssl rand -hex 64) +URL_SIGNATURE_SECRET=$(openssl rand -hex 64) + +cat > .env < puter/config/config.json <` tag instead of waiting on a manifest that doesn't exist. +- `database.migrationPaths` — Puter applies the bundled MySQL schema on boot. `mysql_mig_1.sql` (tables) and `mysql_mig_2.sql` (default apps: editor, viewer, pdf, camera, player, recorder, git, dev-center, puter-linux). Idempotent — safe to re-run. +- `dynamo.bootstrapTables: true` — Puter creates its KV table on boot. **Only set against a local emulator**, never real AWS. +- `dynamo.aws` keys are dummies; DynamoDB-local doesn't validate them but the AWS SDK requires _something_. **Note:** DynamoDB uses `access_key` / `secret_key` (snake_case); S3 below uses `accessKeyId` / `secretAccessKey` (camelCase). Not interchangeable. + +> If you ever change `MARIADB_PASSWORD` after first boot, `.env` alone won't update MariaDB — its credentials are baked into `./puter/data/mariadb/` on first init. Either rotate the password inside MariaDB by hand or `docker compose down && rm -rf ./puter/data/mariadb` to start fresh. + +## Step 2 — Point DNS at the server \[Optional\] + +In your DNS provider, add **two records**: + +``` +A puter.local → +A puter.sitelocal → +A *.puter.sitelocal → +A puter.hostlocal → +A *.puter.hostlocal → +A puter.applocal → +A *.puter.applocal → +A puter.devlocal → +A *.puter.devlocal → +``` + +The wildcard is required — Puter routes via subdomains. + +If you only need these to resolve these locally to test you can add this (any any other needed subdomain) to your hosts file + +``` +127.0.0.1 puter.local +``` + +## Step 3 — TLS (recommended for public installs) \[Optional\] + +Skip this for a quick local demo. Don't skip it for users typing passwords. + +**Get a wildcard cert.** Easiest path with Let's Encrypt + DNS-01 (works for wildcards): + +```bash +sudo certbot certonly --manual --preferred-challenges dns \ + -d puter.local -d puter.sitelocal -d "*.puter.sitelocal" -d puter.hostlocal -d "*.puter.hostlocal" -d puter.applocal -d "*.puter.applocal" -d puter.devlocal -d "*.puter.devlocal" +``` + +Drop the resulting `fullchain.pem` and `privkey.pem` into `./puter/tls/`. + +**Wire nginx to use them:** + +1. Open [nginx/nginx.conf](../nginx/nginx.conf), uncomment the entire `# server { listen 443 ssl … }` block. +2. (Optional) Replace the body of the port-80 block with `return 301 https://$host$request_uri;` to force HTTPS. +3. In [docker-compose.full.yml](../docker-compose.full.yml), uncomment the `443:443` port mapping under the `nginx` service. +4. In `.env`, uncomment `HTTPS_PORT=443`. +5. In `config.json`, switch: + ```json + { "protocol": "https", "pub_port": 443 } + ``` + +## Step 4 — Bring it up + +```bash +docker compose -f docker-compose.full.yml up -d +``` + +First boot takes ~30s while MariaDB initialises and Puter applies the schema + default apps. Watch: + +```bash +docker compose -f docker-compose.full.yml logs -f puter +``` + +Healthy startup: + +``` +[config] override from /etc/puter/config.json +[mysql] running migrations from /opt/puter/dist/src/backend/clients/database/migrations/mysql: 2 file(s) +[mysql] applied mysql_mig_1.sql (...) +[mysql] applied mysql_mig_2.sql (9 statements) +``` + +Then open **** (or `http://` if you skipped TLS). Login is `admin` — the temp password is printed once in the puter container logs on first boot: + +```bash +docker compose -f docker-compose.full.yml logs puter | grep tmp_password +``` + +Change it in Settings after first login. + +## Building from source instead of pulling + +If you want to test local Dockerfile changes against the full stack, uncomment the `build:` block in [docker-compose.full.yml](../docker-compose.full.yml) under the `puter` service, change `pull_policy: always` → `pull_policy: never`, then: + +```bash +docker compose -f docker-compose.full.yml up -d --build +``` + +--- + +## Re-starting backend + +```bash +# update +docker compose -f docker-compose.full.yml pull +docker compose -f docker-compose.full.yml up -d + +# logs +docker compose -f docker-compose.full.yml logs -f puter + +# stop, keep data +docker compose -f docker-compose.full.yml down + +# stop, NUKE all state (irreversible) +docker compose -f docker-compose.full.yml down +rm -rf puter/data +``` + +Migrations re-apply idempotently across pulls. Volumes are preserved. + +## Troubleshooting + +**Site loads but I get "Bad Gateway" / nginx errors.** +The puter container failed to come up. `docker compose -f docker-compose.full.yml logs puter` will tell you which dependency rejected it (most often DB password mismatch between `.env` and `config.json`). + +**Login screen says "admin password not set".** +First-boot temp password is logged once. Find it: `docker compose -f docker-compose.full.yml logs puter | grep "tmp_password"`. After login, change it in Settings. + +**Healthcheck reports unhealthy but the site works.** +The healthcheck hits `puter.localhost:4100/test` from inside the container. If you changed `domain` or `port`, the check still uses defaults. The site itself is fine. + +**Nothing resolves at `puter.example.com` after DNS changes.** +DNS propagates slowly. `dig puter.example.com` and `dig api.puter.example.com` should both return your server IP. If not, give it 5–60 minutes. + +**`docker compose up` hangs at "waiting for service to be healthy".** +`docker compose -f docker-compose.full.yml ps` shows which container is unhealthy. MariaDB takes ~20–30s on a cold boot; everything else under 5s. If something stays unhealthy, `logs ` will tell you why. + +**`Error: DynamoDB aws config requires both access_key and secret_key`.** +You wrote `accessKeyId` / `secretAccessKey` under `dynamo.aws`. That config block uses snake_case (`access_key` / `secret_key`). Only the `s3.s3Config` block uses camelCase. diff --git a/selfhosted/npm.md b/selfhosted/npm.md new file mode 100644 index 000000000..c428d06a4 --- /dev/null +++ b/selfhosted/npm.md @@ -0,0 +1,64 @@ +# 1. Dev mode (`npm start`) + +Run Puter directly from the source tree on Node. Everything runs in-process — no databases, no Redis, no external services. Best for hacking on Puter or a quick local trial on your LAN. + +> ⚠️ **Not safe to expose publicly.** Default JWT secrets ship in the source tree and the in-process key store has no real security boundary. + +## Requirements + +- **Node.js 24+** (`nvm install 24` if you don't have it). +- **C toolchain** for native deps (`bcrypt`, `sharp`, `better-sqlite3`): + - macOS: `xcode-select --install` + - Debian / Ubuntu: `sudo apt install build-essential python3` + +## Setup + +```bash +cd packages/puter # if you cloned the heyputer parent repo + # (skip this if you cloned puter directly) +npm install +npm run build # one-time: compiles backend + GUI + puter.js +npm start # daily use: re-builds backend, then starts +``` + +Open . Sign in as `admin` — the temp password is printed once in the boot logs. + +## What runs in-process + +Out of the box (no `config.json`): + +- SQLite at `volatile/runtime/puter-database.sqlite` (auto-created). +- In-process S3 (`fauxqs`) with the `puter-local` bucket auto-created. +- In-process DynamoDB (`dynalite`) with its KV table auto-created. +- In-process Redis (`ioredis-mock`). + +State lives under `./volatile/`. Delete the folder to reset. + +## Configuring (optional) + +Drop a `config.json` next to `package.json`. It deep-merges over `config.default.json` — only put what you want to change: + +```json +{ "port": 5101, "domain": "myhost.local" } +``` + +Restart with `npm start`. + +For real external services (MySQL, S3, DynamoDB, Redis), the config blocks are the same as in [docker.md → "Wiring to external services"](./docker.md#wire-to-external-services). The mode is meant for in-process defaults though — if you're wiring real services, you probably want [docker.md](./docker.md) instead. + +## Daily workflow + +- Backend changes → `npm start` re-runs the TS compile (~5–10s) and restarts. +- GUI / `puter.js` changes → `npm run build` (full webpack — slower). +- Reset state → `rm -rf volatile/` and start over. + +## Troubleshooting + +**`npm start` says missing `dist/`.** +You skipped `npm run build`. The `prestart` hook only re-builds the backend; the GUI + `puter.js` bundles need the full build at least once. + +**Native module build failures during `npm install`.** +Missing C toolchain. Install it (see Requirements), delete `node_modules`, re-run `npm install`. + +**Port 4100 already in use.** +Set `"port": ` in `config.json`. The browser URL changes accordingly. diff --git a/src/backend/clients/database/MySQLDatabaseClient.ts b/src/backend/clients/database/MySQLDatabaseClient.ts index 605afbe9e..7ac54c798 100644 --- a/src/backend/clients/database/MySQLDatabaseClient.ts +++ b/src/backend/clients/database/MySQLDatabaseClient.ts @@ -17,9 +17,12 @@ * along with this program. If not, see . */ +import { readdirSync, readFileSync } from 'fs'; +import { isAbsolute, resolve as resolvePath } from 'path'; import { createPool, type Pool } from 'mysql2'; import { AbstractDatabaseClient, type WriteResult } from './DatabaseClient'; import { SQLBatcher } from './SQLBatcher.js'; +import { splitMysqlStatements } from './splitMysqlStatements.js'; import type { IConfig } from '../../types'; const RETRIABLE_ERROR_CODES = new Set([ @@ -91,6 +94,8 @@ export class MySQLDatabaseClient extends AbstractDatabaseClient { } this.dbReplica = new SQLBatcher(this.replicaPool, 10, 5); + + await this.runMigrations(); } override async onServerPrepareShutdown(): Promise { @@ -213,6 +218,74 @@ export class MySQLDatabaseClient extends AbstractDatabaseClient { return (primaryResult?.[0] as Record[]) ?? []; } + // ------------------------------------------------------------------ + // Migrations + // ------------------------------------------------------------------ + + /** + * Apply `.sql` files from each configured migration directory in order. + * Files within a directory are sorted lexically. Files MUST be + * idempotent — there is no per-file applied-state tracking. Failures + * abort startup so operators see schema problems loud. + */ + private async runMigrations(): Promise { + const paths = this.config.database?.migrationPaths; + if (!paths || paths.length === 0) return; + + const conn = await this.primaryPool.promise().getConnection(); + try { + for (const rawPath of paths) { + const dir = isAbsolute(rawPath) + ? rawPath + : resolvePath(process.cwd(), rawPath); + + let files: string[]; + try { + files = readdirSync(dir) + .filter( + (f) => f.endsWith('.sql') && f.startsWith('mysql'), + ) + .sort(); + } catch (e) { + throw new Error( + `[mysql] migration path is unreadable: ${dir}`, + { cause: e }, + ); + } + + if (files.length === 0) { + console.log(`[mysql] no migrations in ${dir}`); + continue; + } + + console.log( + `[mysql] running migrations from ${dir}: ${files.length} file(s)`, + ); + + for (const file of files) { + const filePath = resolvePath(dir, file); + const contents = readFileSync(filePath, 'utf8'); + const statements = splitMysqlStatements(contents); + for (let i = 0; i < statements.length; i++) { + try { + await conn.query(statements[i]); + } catch (e) { + throw new Error( + `[mysql] failed to apply ${file} at statement ${i}`, + { cause: e }, + ); + } + } + console.log( + `[mysql] applied ${file} (${statements.length} statements)`, + ); + } + } + } finally { + conn.release(); + } + } + // ------------------------------------------------------------------ // Pool management // ------------------------------------------------------------------ diff --git a/src/backend/clients/database/SQLBatcher.js b/src/backend/clients/database/SQLBatcher.js index d585c4a53..a358f7ea7 100644 --- a/src/backend/clients/database/SQLBatcher.js +++ b/src/backend/clients/database/SQLBatcher.js @@ -22,6 +22,7 @@ import { metrics } from '@opentelemetry/api'; const DEFAULT_MAX_QUEUE_SIZE = 1000; const DEFAULT_FAILURE_THRESHOLD = 5; const DEFAULT_COOLDOWN_MS = 5_000; +const FALLBACK_RETRY_CONCURRENCY = 8; const meter = metrics.getMeter('puter-backend'); const enqueueDroppedCounter = meter.createCounter( @@ -38,6 +39,20 @@ const enqueueRejectedCounter = meter.createCounter( const flushFailureCounter = meter.createCounter('sql_batcher.flush.failed', { description: 'SQLBatcher flush attempts that threw', }); +const fallbackInvocationsCounter = meter.createCounter( + 'sql_batcher.fallback.invocations', + { + description: + 'Times SQLBatcher fell back to per-item retry after a batch error', + }, +); +const fallbackItemFailuresCounter = meter.createCounter( + 'sql_batcher.fallback.item_failures', + { + description: + 'Per-item failures observed during SQLBatcher per-item retry', + }, +); export class SQLBatcher { dbPool; @@ -137,24 +152,111 @@ export class SQLBatcher { const query = `${batch.map((b) => b.sql.replace(/;+\s*$/, '')).join(';')}; SELECT 1`; // SELECT 1 forces mysql2 to return array const values = batch.map((b) => b.values ?? []).flat(); + let connection; try { - const [results, fields] = await this.dbPool - .promise() - .query(query, values); + connection = await this.dbPool.promise().getConnection(); + } catch (error) { + this.#consecutiveFailures++; + this.#lastFailureAt = Date.now(); + flushFailureCounter.add(1); + console.warn( + 'SQLBatcher could not acquire connection for flush:', + error, + ); + for (const b of batch) { + b.reject(this.#createPublicBatchError()); + } + return; + } + // Run the coalesced multi-statement inside an explicit transaction so + // a single bad statement (e.g. a duplicate-key INSERT) rolls back the + // whole batch atomically, leaving us free to re-run each item + // individually below. Without this, MySQL would commit every + // statement up to the failure point and a per-item retry would + // misreport already-committed inserts as duplicate-key failures. + let batchSucceeded = false; + try { + await connection.beginTransaction(); + const [results, fields] = await connection.query(query, values); + await connection.commit(); + batchSucceeded = true; this.#consecutiveFailures = 0; for (let i = 0; i < batch.length; i++) { const b = batch[i]; b.resolve([results[i], fields?.[i]]); } - } catch (error) { - this.#consecutiveFailures++; - this.#lastFailureAt = Date.now(); - flushFailureCounter.add(1); - console.warn('Error in SQLBatcher flush:', error); - for (const b of batch) { - b.reject(this.#createPublicBatchError()); + } catch (batchError) { + try { + await connection.rollback(); + } catch (rollbackError) { + console.warn('SQLBatcher rollback failed:', rollbackError); } + console.warn( + 'SQLBatcher batch failed; retrying items individually:', + batchError, + ); + } finally { + connection.release(); + } + + if (batchSucceeded) return; + + // Per-item fallback. The transaction was rolled back so no statement + // committed; re-running each item independently produces clean + // success/failure outcomes for each caller. Concurrency is capped to + // avoid briefly saturating the pool when a large batch fails. + flushFailureCounter.add(1); + fallbackInvocationsCounter.add(1); + + const settled = new Array(batch.length); + let cursor = 0; + const workers = Array.from( + { length: Math.min(FALLBACK_RETRY_CONCURRENCY, batch.length) }, + async () => { + while (cursor < batch.length) { + const i = cursor++; + const b = batch[i]; + try { + settled[i] = { + ok: true, + value: await this.dbPool + .promise() + .query(b.sql, b.values ?? []), + }; + } catch (error) { + settled[i] = { ok: false, error }; + } + } + }, + ); + await Promise.all(workers); + + let anySucceeded = false; + let failureCount = 0; + for (let i = 0; i < batch.length; i++) { + const b = batch[i]; + const r = settled[i]; + if (r.ok) { + anySucceeded = true; + b.resolve(r.value); + } else { + failureCount++; + b.reject(r.error); + } + } + if (failureCount > 0) { + fallbackItemFailuresCounter.add(failureCount); + } + + // Only escalate the breaker when the database itself looks unhealthy + // (no item got through). Row-level errors like duplicate-key are + // application concerns, not DB outages, and shouldn't trip it. + this.#lastFailureAt = Date.now(); + if (anySucceeded) { + this.#consecutiveFailures = 0; + } else { + this.#consecutiveFailures++; } } } diff --git a/src/backend/clients/database/SqliteDatabaseClient.ts b/src/backend/clients/database/SqliteDatabaseClient.ts index 03749a81e..4e31d9f77 100644 --- a/src/backend/clients/database/SqliteDatabaseClient.ts +++ b/src/backend/clients/database/SqliteDatabaseClient.ts @@ -23,7 +23,7 @@ import { createContext, runInContext } from 'vm'; import { AbstractDatabaseClient, type WriteResult } from './DatabaseClient'; import type { IConfig } from '../../types'; -const MIGRATIONS_DIR = resolve(__dirname, './migrations'); +const MIGRATIONS_DIR = resolve(__dirname, './migrations/sqlite'); /** * Ordered list of [threshold_version, files[]] pairs. diff --git a/src/backend/clients/database/migrations/mysql/mysql_mig_1.sql b/src/backend/clients/database/migrations/mysql/mysql_mig_1.sql new file mode 100644 index 000000000..928b9c4b8 --- /dev/null +++ b/src/backend/clients/database/migrations/mysql/mysql_mig_1.sql @@ -0,0 +1,1347 @@ +-- MySQL dump 10.13 Distrib 8.0.46, for macos15 (arm64) +-- +-- Host: puter-db.ctcdlrc15nt3.us-west-2.rds.amazonaws.com Database: filecream +-- ------------------------------------------------------ +-- Server version 8.0.44 + +/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */; +/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */; +/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */; +/*!50503 SET NAMES utf8 */; +/*!40103 SET @OLD_TIME_ZONE=@@TIME_ZONE */; +/*!40103 SET TIME_ZONE='+00:00' */; +/*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */; +/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */; +/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */; +/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */; + +-- +-- mysqldump prelude that required SUPER / BINLOG_ADMIN / GTID_PURGED +-- privileges has been stripped (SQL_LOG_BIN toggle, GTID_PURGED set). +-- These are only meaningful for replication-aware restores; self-host +-- single-instance MariaDB doesn't need them and the bundled `puter` +-- user doesn't have those privileges. +-- + +-- +-- Idempotent column-ensure helper (used by CALLs below; dropped at end of file) +-- + +DROP PROCEDURE IF EXISTS _puter_add_col; +DELIMITER // +CREATE PROCEDURE _puter_add_col(IN tbl VARCHAR(64), IN col VARCHAR(64), IN def TEXT) +BEGIN + IF NOT EXISTS ( + SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS + WHERE TABLE_SCHEMA = DATABASE() + AND TABLE_NAME = tbl + AND COLUMN_NAME = col + ) THEN + SET @s := CONCAT('ALTER TABLE `', tbl, '` ADD COLUMN ', def); + PREPARE stmt FROM @s; + EXECUTE stmt; + DEALLOCATE PREPARE stmt; + END IF; +END// +DELIMITER ; + +-- +-- Table structure for table `access_token_permissions` +-- + +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE IF NOT EXISTS `access_token_permissions` ( + `id` int unsigned NOT NULL AUTO_INCREMENT, + `token_uid` char(40) COLLATE utf8mb4_unicode_ci NOT NULL, + `authorizer_user_id` int unsigned DEFAULT NULL, + `authorizer_app_id` int unsigned DEFAULT NULL, + `permission` varchar(255) CHARACTER SET ascii COLLATE ascii_general_ci NOT NULL, + `extra` json DEFAULT NULL, + `created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (`id`) +) ENGINE=InnoDB AUTO_INCREMENT=43870 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +CALL _puter_add_col('access_token_permissions', 'id', '`id` int unsigned NOT NULL AUTO_INCREMENT'); +CALL _puter_add_col('access_token_permissions', 'token_uid', '`token_uid` char(40) COLLATE utf8mb4_unicode_ci NOT NULL'); +CALL _puter_add_col('access_token_permissions', 'authorizer_user_id', '`authorizer_user_id` int unsigned DEFAULT NULL'); +CALL _puter_add_col('access_token_permissions', 'authorizer_app_id', '`authorizer_app_id` int unsigned DEFAULT NULL'); +CALL _puter_add_col('access_token_permissions', 'permission', '`permission` varchar(255) CHARACTER SET ascii COLLATE ascii_general_ci NOT NULL'); +CALL _puter_add_col('access_token_permissions', 'extra', '`extra` json DEFAULT NULL'); +CALL _puter_add_col('access_token_permissions', 'created_at', '`created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP'); + +-- +-- Table structure for table `ai_usage` +-- + +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE IF NOT EXISTS `ai_usage` ( + `id` int NOT NULL AUTO_INCREMENT, + `user_id` int unsigned NOT NULL, + `app_id` int unsigned DEFAULT NULL, + `service_name` char(64) DEFAULT NULL, + `model_name` char(128) DEFAULT NULL, + `price_modifier` char(40) DEFAULT NULL, + `cost` int DEFAULT NULL, + `value_uint_1` int unsigned DEFAULT NULL, + `value_uint_2` int unsigned DEFAULT NULL, + `value_uint_3` int unsigned DEFAULT NULL, + `value_uint_4` int unsigned DEFAULT NULL, + `value_uint_5` int unsigned DEFAULT NULL, + `created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (`id`), + KEY `app_id` (`app_id`), + KEY `idx_ai_usage_service_name` (`service_name`), + KEY `idx_ai_usage_model_name` (`model_name`), + KEY `idx_ai_usage_price_modifier` (`price_modifier`), + KEY `idx_ai_usage_created_at` (`created_at`), + KEY `idx_ai_usage_user_timestamp` (`user_id`,`created_at`), + CONSTRAINT `ai_usage_ibfk_1` FOREIGN KEY (`user_id`) REFERENCES `user` (`id`) ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT `ai_usage_ibfk_2` FOREIGN KEY (`app_id`) REFERENCES `apps` (`id`) ON DELETE SET NULL ON UPDATE CASCADE +) ENGINE=InnoDB AUTO_INCREMENT=935038 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +CALL _puter_add_col('ai_usage', 'id', '`id` int NOT NULL AUTO_INCREMENT'); +CALL _puter_add_col('ai_usage', 'user_id', '`user_id` int unsigned NOT NULL'); +CALL _puter_add_col('ai_usage', 'app_id', '`app_id` int unsigned DEFAULT NULL'); +CALL _puter_add_col('ai_usage', 'service_name', '`service_name` char(64) DEFAULT NULL'); +CALL _puter_add_col('ai_usage', 'model_name', '`model_name` char(128) DEFAULT NULL'); +CALL _puter_add_col('ai_usage', 'price_modifier', '`price_modifier` char(40) DEFAULT NULL'); +CALL _puter_add_col('ai_usage', 'cost', '`cost` int DEFAULT NULL'); +CALL _puter_add_col('ai_usage', 'value_uint_1', '`value_uint_1` int unsigned DEFAULT NULL'); +CALL _puter_add_col('ai_usage', 'value_uint_2', '`value_uint_2` int unsigned DEFAULT NULL'); +CALL _puter_add_col('ai_usage', 'value_uint_3', '`value_uint_3` int unsigned DEFAULT NULL'); +CALL _puter_add_col('ai_usage', 'value_uint_4', '`value_uint_4` int unsigned DEFAULT NULL'); +CALL _puter_add_col('ai_usage', 'value_uint_5', '`value_uint_5` int unsigned DEFAULT NULL'); +CALL _puter_add_col('ai_usage', 'created_at', '`created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP'); + +-- +-- Table structure for table `app_filetype_association` +-- + +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE IF NOT EXISTS `app_filetype_association` ( + `id` int unsigned NOT NULL AUTO_INCREMENT, + `app_id` int unsigned NOT NULL, + `type` varchar(60) NOT NULL, + PRIMARY KEY (`id`), + KEY `app_id` (`app_id`), + KEY `type` (`type`), + CONSTRAINT `app_filetype_association_ibfk_1` FOREIGN KEY (`app_id`) REFERENCES `apps` (`id`) ON DELETE CASCADE ON UPDATE CASCADE +) ENGINE=InnoDB AUTO_INCREMENT=28897 DEFAULT CHARSET=utf8mb3; +/*!40101 SET character_set_client = @saved_cs_client */; + +CALL _puter_add_col('app_filetype_association', 'id', '`id` int unsigned NOT NULL AUTO_INCREMENT'); +CALL _puter_add_col('app_filetype_association', 'app_id', '`app_id` int unsigned NOT NULL'); +CALL _puter_add_col('app_filetype_association', 'type', '`type` varchar(60) NOT NULL'); + +-- +-- Table structure for table `app_opens` +-- + +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE IF NOT EXISTS `app_opens` ( + `_id` bigint unsigned NOT NULL AUTO_INCREMENT, + `app_uid` char(40) CHARACTER SET latin1 COLLATE latin1_swedish_ci NOT NULL, + `user_id` int unsigned NOT NULL, + `ts` int unsigned NOT NULL, + `human_ts` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (`_id`), + KEY `user_id` (`user_id`), + KEY `app_uid` (`app_uid`), + KEY `idx_app_opens_uid_ts` (`app_uid`,`ts`), + KEY `idx_app_opens_app_user` (`app_uid`,`user_id`), + CONSTRAINT `app_opens_ibfk_2` FOREIGN KEY (`user_id`) REFERENCES `user` (`id`) ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT `app_opens_ibfk_3` FOREIGN KEY (`app_uid`) REFERENCES `apps` (`uid`) ON DELETE CASCADE ON UPDATE CASCADE +) ENGINE=InnoDB AUTO_INCREMENT=14510891 DEFAULT CHARSET=utf8mb3; +/*!40101 SET character_set_client = @saved_cs_client */; + +CALL _puter_add_col('app_opens', '_id', '`_id` bigint unsigned NOT NULL AUTO_INCREMENT'); +CALL _puter_add_col('app_opens', 'app_uid', '`app_uid` char(40) CHARACTER SET latin1 COLLATE latin1_swedish_ci NOT NULL'); +CALL _puter_add_col('app_opens', 'user_id', '`user_id` int unsigned NOT NULL'); +CALL _puter_add_col('app_opens', 'ts', '`ts` int unsigned NOT NULL'); +CALL _puter_add_col('app_opens', 'human_ts', '`human_ts` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP'); + +-- +-- Table structure for table `app_update_audit` +-- + +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE IF NOT EXISTS `app_update_audit` ( + `id` int unsigned NOT NULL AUTO_INCREMENT, + `app_id` int unsigned DEFAULT NULL, + `app_id_keep` int unsigned NOT NULL, + `created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + `old_name` varchar(50) CHARACTER SET latin1 COLLATE latin1_swedish_ci DEFAULT NULL, + `new_name` varchar(50) CHARACTER SET latin1 COLLATE latin1_swedish_ci DEFAULT NULL, + `reason` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + PRIMARY KEY (`id`), + KEY `fk_app_update_audit_app_id` (`app_id`), + CONSTRAINT `fk_app_update_audit_app_id` FOREIGN KEY (`app_id`) REFERENCES `apps` (`id`) ON DELETE SET NULL ON UPDATE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +CALL _puter_add_col('app_update_audit', 'id', '`id` int unsigned NOT NULL AUTO_INCREMENT'); +CALL _puter_add_col('app_update_audit', 'app_id', '`app_id` int unsigned DEFAULT NULL'); +CALL _puter_add_col('app_update_audit', 'app_id_keep', '`app_id_keep` int unsigned NOT NULL'); +CALL _puter_add_col('app_update_audit', 'created_at', '`created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP'); +CALL _puter_add_col('app_update_audit', 'old_name', '`old_name` varchar(50) CHARACTER SET latin1 COLLATE latin1_swedish_ci DEFAULT NULL'); +CALL _puter_add_col('app_update_audit', 'new_name', '`new_name` varchar(50) CHARACTER SET latin1 COLLATE latin1_swedish_ci DEFAULT NULL'); +CALL _puter_add_col('app_update_audit', 'reason', '`reason` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL'); + +-- +-- Table structure for table `apps` +-- + +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE IF NOT EXISTS `apps` ( + `id` int unsigned NOT NULL AUTO_INCREMENT, + `uid` char(40) CHARACTER SET latin1 COLLATE latin1_swedish_ci NOT NULL, + `owner_user_id` int unsigned DEFAULT NULL, + `icon` longtext, + `name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL, + `title` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL, + `description` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci, + `godmode` tinyint(1) DEFAULT '0', + `maximize_on_start` tinyint(1) DEFAULT '0', + `index_url` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL, + `approved_for_listing` tinyint(1) DEFAULT '0', + `approved_for_opening_items` tinyint(1) DEFAULT '0', + `approved_for_incentive_program` tinyint(1) DEFAULT '0', + `timestamp` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + `last_review` timestamp NULL DEFAULT NULL, + `tags` varchar(255) DEFAULT NULL, + `app_owner` int unsigned DEFAULT NULL, + `background` tinyint(1) DEFAULT '0', + `metadata` json DEFAULT NULL, + `protected` tinyint(1) DEFAULT '0', + `is_private` tinyint(1) DEFAULT '0', + PRIMARY KEY (`id`), + UNIQUE KEY `uid` (`uid`), + UNIQUE KEY `name` (`name`), + KEY `owner_user_id` (`owner_user_id`), + KEY `fk_apps_app_owner` (`app_owner`), + KEY `idx_apps_owner_timestamp` (`owner_user_id`,`timestamp` DESC), + KEY `idx_apps_listing_timestamp` (`approved_for_listing`,`timestamp` DESC), + CONSTRAINT `apps_ibfk_1` FOREIGN KEY (`owner_user_id`) REFERENCES `user` (`id`) ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT `fk_apps_app_owner` FOREIGN KEY (`app_owner`) REFERENCES `apps` (`id`) ON DELETE SET NULL ON UPDATE CASCADE +) ENGINE=InnoDB AUTO_INCREMENT=169455 DEFAULT CHARSET=utf8mb3; +/*!40101 SET character_set_client = @saved_cs_client */; + +CALL _puter_add_col('apps', 'id', '`id` int unsigned NOT NULL AUTO_INCREMENT'); +CALL _puter_add_col('apps', 'uid', '`uid` char(40) CHARACTER SET latin1 COLLATE latin1_swedish_ci NOT NULL'); +CALL _puter_add_col('apps', 'owner_user_id', '`owner_user_id` int unsigned DEFAULT NULL'); +CALL _puter_add_col('apps', 'icon', '`icon` longtext'); +CALL _puter_add_col('apps', 'name', '`name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL'); +CALL _puter_add_col('apps', 'title', '`title` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL'); +CALL _puter_add_col('apps', 'description', '`description` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci'); +CALL _puter_add_col('apps', 'godmode', '`godmode` tinyint(1) DEFAULT ''0'''); +CALL _puter_add_col('apps', 'maximize_on_start', '`maximize_on_start` tinyint(1) DEFAULT ''0'''); +CALL _puter_add_col('apps', 'index_url', '`index_url` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL'); +CALL _puter_add_col('apps', 'approved_for_listing', '`approved_for_listing` tinyint(1) DEFAULT ''0'''); +CALL _puter_add_col('apps', 'approved_for_opening_items', '`approved_for_opening_items` tinyint(1) DEFAULT ''0'''); +CALL _puter_add_col('apps', 'approved_for_incentive_program', '`approved_for_incentive_program` tinyint(1) DEFAULT ''0'''); +CALL _puter_add_col('apps', 'timestamp', '`timestamp` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP'); +CALL _puter_add_col('apps', 'last_review', '`last_review` timestamp NULL DEFAULT NULL'); +CALL _puter_add_col('apps', 'tags', '`tags` varchar(255) DEFAULT NULL'); +CALL _puter_add_col('apps', 'app_owner', '`app_owner` int unsigned DEFAULT NULL'); +CALL _puter_add_col('apps', 'background', '`background` tinyint(1) DEFAULT ''0'''); +CALL _puter_add_col('apps', 'metadata', '`metadata` json DEFAULT NULL'); +CALL _puter_add_col('apps', 'protected', '`protected` tinyint(1) DEFAULT ''0'''); +CALL _puter_add_col('apps', 'is_private', '`is_private` tinyint(1) DEFAULT ''0'''); + +-- +-- Table structure for table `audit_dev_to_app_permissions` +-- + +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE IF NOT EXISTS `audit_dev_to_app_permissions` ( + `id` int unsigned NOT NULL AUTO_INCREMENT, + `user_id` int unsigned DEFAULT NULL, + `user_id_keep` int unsigned NOT NULL, + `app_id` int unsigned DEFAULT NULL, + `app_id_keep` int unsigned NOT NULL, + `permission` varchar(255) CHARACTER SET ascii COLLATE ascii_general_ci NOT NULL, + `extra` json DEFAULT NULL, + `action` varchar(16) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `reason` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (`id`), + KEY `fk_audit_dev_to_app_permissions_user_id` (`user_id`), + KEY `fk_audit_dev_to_app_permissions_app_id` (`app_id`), + CONSTRAINT `fk_audit_dev_to_app_permissions_app_id` FOREIGN KEY (`app_id`) REFERENCES `apps` (`id`) ON DELETE SET NULL ON UPDATE CASCADE, + CONSTRAINT `fk_audit_dev_to_app_permissions_user_id` FOREIGN KEY (`user_id`) REFERENCES `user` (`id`) ON DELETE SET NULL ON UPDATE CASCADE +) ENGINE=InnoDB AUTO_INCREMENT=34 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +CALL _puter_add_col('audit_dev_to_app_permissions', 'id', '`id` int unsigned NOT NULL AUTO_INCREMENT'); +CALL _puter_add_col('audit_dev_to_app_permissions', 'user_id', '`user_id` int unsigned DEFAULT NULL'); +CALL _puter_add_col('audit_dev_to_app_permissions', 'user_id_keep', '`user_id_keep` int unsigned NOT NULL'); +CALL _puter_add_col('audit_dev_to_app_permissions', 'app_id', '`app_id` int unsigned DEFAULT NULL'); +CALL _puter_add_col('audit_dev_to_app_permissions', 'app_id_keep', '`app_id_keep` int unsigned NOT NULL'); +CALL _puter_add_col('audit_dev_to_app_permissions', 'permission', '`permission` varchar(255) CHARACTER SET ascii COLLATE ascii_general_ci NOT NULL'); +CALL _puter_add_col('audit_dev_to_app_permissions', 'extra', '`extra` json DEFAULT NULL'); +CALL _puter_add_col('audit_dev_to_app_permissions', 'action', '`action` varchar(16) COLLATE utf8mb4_unicode_ci DEFAULT NULL'); +CALL _puter_add_col('audit_dev_to_app_permissions', 'reason', '`reason` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL'); +CALL _puter_add_col('audit_dev_to_app_permissions', 'created_at', '`created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP'); + +-- +-- Table structure for table `audit_user_to_app_permissions` +-- + +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE IF NOT EXISTS `audit_user_to_app_permissions` ( + `id` int unsigned NOT NULL AUTO_INCREMENT, + `user_id` int unsigned DEFAULT NULL, + `user_id_keep` int unsigned NOT NULL, + `app_id` int unsigned DEFAULT NULL, + `app_id_keep` int unsigned NOT NULL, + `permission` varchar(255) CHARACTER SET ascii COLLATE ascii_general_ci NOT NULL, + `extra` json DEFAULT NULL, + `action` varchar(16) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `reason` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (`id`), + KEY `fk_audit_user_to_app_permissions_user_id` (`user_id`), + KEY `fk_audit_user_to_app_permissions_app_id` (`app_id`), + CONSTRAINT `fk_audit_user_to_app_permissions_app_id` FOREIGN KEY (`app_id`) REFERENCES `apps` (`id`) ON DELETE SET NULL ON UPDATE CASCADE, + CONSTRAINT `fk_audit_user_to_app_permissions_user_id` FOREIGN KEY (`user_id`) REFERENCES `user` (`id`) ON DELETE SET NULL ON UPDATE CASCADE +) ENGINE=InnoDB AUTO_INCREMENT=7352860 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +CALL _puter_add_col('audit_user_to_app_permissions', 'id', '`id` int unsigned NOT NULL AUTO_INCREMENT'); +CALL _puter_add_col('audit_user_to_app_permissions', 'user_id', '`user_id` int unsigned DEFAULT NULL'); +CALL _puter_add_col('audit_user_to_app_permissions', 'user_id_keep', '`user_id_keep` int unsigned NOT NULL'); +CALL _puter_add_col('audit_user_to_app_permissions', 'app_id', '`app_id` int unsigned DEFAULT NULL'); +CALL _puter_add_col('audit_user_to_app_permissions', 'app_id_keep', '`app_id_keep` int unsigned NOT NULL'); +CALL _puter_add_col('audit_user_to_app_permissions', 'permission', '`permission` varchar(255) CHARACTER SET ascii COLLATE ascii_general_ci NOT NULL'); +CALL _puter_add_col('audit_user_to_app_permissions', 'extra', '`extra` json DEFAULT NULL'); +CALL _puter_add_col('audit_user_to_app_permissions', 'action', '`action` varchar(16) COLLATE utf8mb4_unicode_ci DEFAULT NULL'); +CALL _puter_add_col('audit_user_to_app_permissions', 'reason', '`reason` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL'); +CALL _puter_add_col('audit_user_to_app_permissions', 'created_at', '`created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP'); + +-- +-- Table structure for table `audit_user_to_group_permissions` +-- + +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE IF NOT EXISTS `audit_user_to_group_permissions` ( + `id` int unsigned NOT NULL AUTO_INCREMENT, + `user_id` int unsigned DEFAULT NULL, + `user_id_keep` int unsigned NOT NULL, + `group_id` int unsigned DEFAULT NULL, + `group_id_keep` int unsigned NOT NULL, + `permission` varchar(255) CHARACTER SET ascii COLLATE ascii_general_ci NOT NULL, + `extra` json DEFAULT NULL, + `action` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `reason` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (`id`), + KEY `user_id` (`user_id`), + KEY `group_id` (`group_id`), + CONSTRAINT `audit_user_to_group_permissions_ibfk_1` FOREIGN KEY (`user_id`) REFERENCES `user` (`id`) ON DELETE SET NULL ON UPDATE CASCADE, + CONSTRAINT `audit_user_to_group_permissions_ibfk_2` FOREIGN KEY (`group_id`) REFERENCES `group` (`id`) ON DELETE SET NULL ON UPDATE CASCADE +) ENGINE=InnoDB AUTO_INCREMENT=17 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +CALL _puter_add_col('audit_user_to_group_permissions', 'id', '`id` int unsigned NOT NULL AUTO_INCREMENT'); +CALL _puter_add_col('audit_user_to_group_permissions', 'user_id', '`user_id` int unsigned DEFAULT NULL'); +CALL _puter_add_col('audit_user_to_group_permissions', 'user_id_keep', '`user_id_keep` int unsigned NOT NULL'); +CALL _puter_add_col('audit_user_to_group_permissions', 'group_id', '`group_id` int unsigned DEFAULT NULL'); +CALL _puter_add_col('audit_user_to_group_permissions', 'group_id_keep', '`group_id_keep` int unsigned NOT NULL'); +CALL _puter_add_col('audit_user_to_group_permissions', 'permission', '`permission` varchar(255) CHARACTER SET ascii COLLATE ascii_general_ci NOT NULL'); +CALL _puter_add_col('audit_user_to_group_permissions', 'extra', '`extra` json DEFAULT NULL'); +CALL _puter_add_col('audit_user_to_group_permissions', 'action', '`action` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL'); +CALL _puter_add_col('audit_user_to_group_permissions', 'reason', '`reason` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL'); +CALL _puter_add_col('audit_user_to_group_permissions', 'created_at', '`created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP'); + +-- +-- Table structure for table `audit_user_to_user_permissions` +-- + +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE IF NOT EXISTS `audit_user_to_user_permissions` ( + `id` int unsigned NOT NULL AUTO_INCREMENT, + `issuer_user_id` int unsigned DEFAULT NULL, + `issuer_user_id_keep` int unsigned NOT NULL, + `holder_user_id` int unsigned DEFAULT NULL, + `holder_user_id_keep` int unsigned NOT NULL, + `permission` varchar(255) CHARACTER SET ascii COLLATE ascii_general_ci NOT NULL, + `extra` json DEFAULT NULL, + `action` varchar(16) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `reason` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (`id`), + KEY `fk_audit_user_to_user_permissions_issuer_user_id` (`issuer_user_id`), + KEY `fk_audit_user_to_user_permissions_holder_user_id` (`holder_user_id`) +) ENGINE=InnoDB AUTO_INCREMENT=981 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +CALL _puter_add_col('audit_user_to_user_permissions', 'id', '`id` int unsigned NOT NULL AUTO_INCREMENT'); +CALL _puter_add_col('audit_user_to_user_permissions', 'issuer_user_id', '`issuer_user_id` int unsigned DEFAULT NULL'); +CALL _puter_add_col('audit_user_to_user_permissions', 'issuer_user_id_keep', '`issuer_user_id_keep` int unsigned NOT NULL'); +CALL _puter_add_col('audit_user_to_user_permissions', 'holder_user_id', '`holder_user_id` int unsigned DEFAULT NULL'); +CALL _puter_add_col('audit_user_to_user_permissions', 'holder_user_id_keep', '`holder_user_id_keep` int unsigned NOT NULL'); +CALL _puter_add_col('audit_user_to_user_permissions', 'permission', '`permission` varchar(255) CHARACTER SET ascii COLLATE ascii_general_ci NOT NULL'); +CALL _puter_add_col('audit_user_to_user_permissions', 'extra', '`extra` json DEFAULT NULL'); +CALL _puter_add_col('audit_user_to_user_permissions', 'action', '`action` varchar(16) COLLATE utf8mb4_unicode_ci DEFAULT NULL'); +CALL _puter_add_col('audit_user_to_user_permissions', 'reason', '`reason` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL'); +CALL _puter_add_col('audit_user_to_user_permissions', 'created_at', '`created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP'); + + +-- +-- Table structure for table `dev_to_app_permissions` +-- + +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE IF NOT EXISTS `dev_to_app_permissions` ( + `user_id` int unsigned NOT NULL, + `app_id` int unsigned NOT NULL, + `permission` varchar(255) CHARACTER SET ascii COLLATE ascii_general_ci NOT NULL, + `extra` json DEFAULT NULL, + PRIMARY KEY (`user_id`,`app_id`,`permission`), + KEY `fk_dev_to_app_permissions_app_id` (`app_id`), + KEY `idx_dev_app_perms_permission` (`permission`), + CONSTRAINT `fk_dev_to_app_permissions_app_id` FOREIGN KEY (`app_id`) REFERENCES `apps` (`id`) ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT `fk_dev_to_app_permissions_user_id` FOREIGN KEY (`user_id`) REFERENCES `user` (`id`) ON DELETE CASCADE ON UPDATE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +CALL _puter_add_col('dev_to_app_permissions', 'user_id', '`user_id` int unsigned NOT NULL'); +CALL _puter_add_col('dev_to_app_permissions', 'app_id', '`app_id` int unsigned NOT NULL'); +CALL _puter_add_col('dev_to_app_permissions', 'permission', '`permission` varchar(255) CHARACTER SET ascii COLLATE ascii_general_ci NOT NULL'); +CALL _puter_add_col('dev_to_app_permissions', 'extra', '`extra` json DEFAULT NULL'); + +-- +-- Table structure for table `feedback` +-- + +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE IF NOT EXISTS `feedback` ( + `id` int unsigned NOT NULL AUTO_INCREMENT, + `user_id` int unsigned NOT NULL, + `message` text, + `ts` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (`id`), + KEY `user_id` (`user_id`), + CONSTRAINT `feedback_ibfk_1` FOREIGN KEY (`user_id`) REFERENCES `user` (`id`) ON DELETE CASCADE ON UPDATE CASCADE +) ENGINE=InnoDB AUTO_INCREMENT=815 DEFAULT CHARSET=utf8mb3; +/*!40101 SET character_set_client = @saved_cs_client */; + +CALL _puter_add_col('feedback', 'id', '`id` int unsigned NOT NULL AUTO_INCREMENT'); +CALL _puter_add_col('feedback', 'user_id', '`user_id` int unsigned NOT NULL'); +CALL _puter_add_col('feedback', 'message', '`message` text'); +CALL _puter_add_col('feedback', 'ts', '`ts` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP'); + +-- +-- Table structure for table `fsentries` +-- + +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE IF NOT EXISTS `fsentries` ( + `id` int unsigned NOT NULL AUTO_INCREMENT, + `uuid` char(36) CHARACTER SET latin1 COLLATE latin1_swedish_ci NOT NULL, + `bucket` varchar(50) DEFAULT NULL, + `bucket_region` varchar(30) DEFAULT NULL, + `public_token` char(36) CHARACTER SET latin1 COLLATE latin1_swedish_ci DEFAULT NULL, + `file_request_token` char(36) DEFAULT NULL, + `is_shortcut` tinyint(1) DEFAULT '0', + `shortcut_to` int unsigned DEFAULT NULL, + `user_id` int unsigned NOT NULL, + `parent_id` int unsigned DEFAULT NULL, + `associated_app_id` int unsigned DEFAULT NULL, + `is_dir` tinyint(1) DEFAULT '0', + `layout` varchar(30) DEFAULT NULL, + `sort_by` enum('name','modified','type','size') DEFAULT NULL, + `sort_order` enum('asc','desc') DEFAULT NULL, + `is_public` tinyint(1) DEFAULT NULL, + `thumbnail` longtext, + `immutable` tinyint(1) NOT NULL DEFAULT '0', + `name` varchar(767) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL, + `metadata` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci, + `modified` int unsigned NOT NULL, + `created` int unsigned DEFAULT NULL, + `accessed` int unsigned DEFAULT NULL, + `size` bigint DEFAULT NULL, + `symlink_path` varchar(260) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci DEFAULT NULL, + `is_symlink` tinyint(1) DEFAULT '0', + `parent_uid` char(36) DEFAULT NULL, + `path` varchar(4096) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `uuid` (`uuid`) USING BTREE, + UNIQUE KEY `parent_id_filename` (`parent_id`,`name`) USING BTREE, + UNIQUE KEY `public_token` (`public_token`) USING BTREE, + UNIQUE KEY `file_request_token` (`file_request_token`), + KEY `filename` (`name`), + KEY `modified` (`modified`), + KEY `parent_id` (`parent_id`), + KEY `is_dir` (`is_dir`), + KEY `user_id` (`user_id`) USING BTREE, + KEY `shortcut_to` (`shortcut_to`), + KEY `associated_app_id` (`associated_app_id`), + KEY `bucket` (`bucket`), + KEY `bucket_region` (`bucket_region`), + KEY `parent_uid` (`parent_uid`), + KEY `idx_fsentries_path` (`path`(767)), + KEY `idx_fsentries_accessed` (`accessed`), + KEY `idx_fsentries_user_parent_name` (`user_id`,`parent_uid`,`name`(191)), + KEY `idx_fsentries_parent_uid_name` (`parent_uid`,`name`(191)), + CONSTRAINT `fsentries_ibfk_1` FOREIGN KEY (`user_id`) REFERENCES `user` (`id`) ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT `fsentries_ibfk_2` FOREIGN KEY (`parent_id`) REFERENCES `fsentries` (`id`) ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT `fsentries_ibfk_3` FOREIGN KEY (`shortcut_to`) REFERENCES `fsentries` (`id`) ON DELETE SET NULL ON UPDATE CASCADE, + CONSTRAINT `fsentries_ibfk_4` FOREIGN KEY (`associated_app_id`) REFERENCES `apps` (`id`) ON DELETE SET NULL ON UPDATE CASCADE +) ENGINE=InnoDB AUTO_INCREMENT=37893498 DEFAULT CHARSET=latin1; +/*!40101 SET character_set_client = @saved_cs_client */; + +CALL _puter_add_col('fsentries', 'id', '`id` int unsigned NOT NULL AUTO_INCREMENT'); +CALL _puter_add_col('fsentries', 'uuid', '`uuid` char(36) CHARACTER SET latin1 COLLATE latin1_swedish_ci NOT NULL'); +CALL _puter_add_col('fsentries', 'bucket', '`bucket` varchar(50) DEFAULT NULL'); +CALL _puter_add_col('fsentries', 'bucket_region', '`bucket_region` varchar(30) DEFAULT NULL'); +CALL _puter_add_col('fsentries', 'public_token', '`public_token` char(36) CHARACTER SET latin1 COLLATE latin1_swedish_ci DEFAULT NULL'); +CALL _puter_add_col('fsentries', 'file_request_token', '`file_request_token` char(36) DEFAULT NULL'); +CALL _puter_add_col('fsentries', 'is_shortcut', '`is_shortcut` tinyint(1) DEFAULT ''0'''); +CALL _puter_add_col('fsentries', 'shortcut_to', '`shortcut_to` int unsigned DEFAULT NULL'); +CALL _puter_add_col('fsentries', 'user_id', '`user_id` int unsigned NOT NULL'); +CALL _puter_add_col('fsentries', 'parent_id', '`parent_id` int unsigned DEFAULT NULL'); +CALL _puter_add_col('fsentries', 'associated_app_id', '`associated_app_id` int unsigned DEFAULT NULL'); +CALL _puter_add_col('fsentries', 'is_dir', '`is_dir` tinyint(1) DEFAULT ''0'''); +CALL _puter_add_col('fsentries', 'layout', '`layout` varchar(30) DEFAULT NULL'); +CALL _puter_add_col('fsentries', 'sort_by', '`sort_by` enum(''name'',''modified'',''type'',''size'') DEFAULT NULL'); +CALL _puter_add_col('fsentries', 'sort_order', '`sort_order` enum(''asc'',''desc'') DEFAULT NULL'); +CALL _puter_add_col('fsentries', 'is_public', '`is_public` tinyint(1) DEFAULT NULL'); +CALL _puter_add_col('fsentries', 'thumbnail', '`thumbnail` longtext'); +CALL _puter_add_col('fsentries', 'immutable', '`immutable` tinyint(1) NOT NULL DEFAULT ''0'''); +CALL _puter_add_col('fsentries', 'name', '`name` varchar(767) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL'); +CALL _puter_add_col('fsentries', 'metadata', '`metadata` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci'); +CALL _puter_add_col('fsentries', 'modified', '`modified` int unsigned NOT NULL'); +CALL _puter_add_col('fsentries', 'created', '`created` int unsigned DEFAULT NULL'); +CALL _puter_add_col('fsentries', 'accessed', '`accessed` int unsigned DEFAULT NULL'); +CALL _puter_add_col('fsentries', 'size', '`size` bigint DEFAULT NULL'); +CALL _puter_add_col('fsentries', 'symlink_path', '`symlink_path` varchar(260) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci DEFAULT NULL'); +CALL _puter_add_col('fsentries', 'is_symlink', '`is_symlink` tinyint(1) DEFAULT ''0'''); +CALL _puter_add_col('fsentries', 'parent_uid', '`parent_uid` char(36) DEFAULT NULL'); +CALL _puter_add_col('fsentries', 'path', '`path` varchar(4096) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL'); + +-- +-- Table structure for table `fsentry_versions` +-- + +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE IF NOT EXISTS `fsentry_versions` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `fsentry_id` int unsigned NOT NULL, + `fsentry_uuid` char(36) NOT NULL, + `version_id` varchar(60) NOT NULL, + `user_id` int unsigned DEFAULT NULL, + `message` mediumtext CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci, + `ts_epoch` int unsigned DEFAULT NULL, + PRIMARY KEY (`id`), + KEY `fsentry_id` (`fsentry_id`), + KEY `fsentry_uuid` (`fsentry_uuid`), + KEY `user_id` (`user_id`), + CONSTRAINT `fsentry_versions_ibfk_1` FOREIGN KEY (`fsentry_id`) REFERENCES `fsentries` (`id`) ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT `fsentry_versions_ibfk_2` FOREIGN KEY (`user_id`) REFERENCES `user` (`id`) ON DELETE CASCADE ON UPDATE CASCADE +) ENGINE=InnoDB AUTO_INCREMENT=24662498 DEFAULT CHARSET=utf8mb3; +/*!40101 SET character_set_client = @saved_cs_client */; + +CALL _puter_add_col('fsentry_versions', 'id', '`id` bigint NOT NULL AUTO_INCREMENT'); +CALL _puter_add_col('fsentry_versions', 'fsentry_id', '`fsentry_id` int unsigned NOT NULL'); +CALL _puter_add_col('fsentry_versions', 'fsentry_uuid', '`fsentry_uuid` char(36) NOT NULL'); +CALL _puter_add_col('fsentry_versions', 'version_id', '`version_id` varchar(60) NOT NULL'); +CALL _puter_add_col('fsentry_versions', 'user_id', '`user_id` int unsigned DEFAULT NULL'); +CALL _puter_add_col('fsentry_versions', 'message', '`message` mediumtext CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci'); +CALL _puter_add_col('fsentry_versions', 'ts_epoch', '`ts_epoch` int unsigned DEFAULT NULL'); + +-- +-- Table structure for table `general_analytics` +-- + +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE IF NOT EXISTS `general_analytics` ( + `id` int unsigned NOT NULL AUTO_INCREMENT, + `created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + `uid` char(40) COLLATE utf8mb4_unicode_ci NOT NULL, + `trace_id` varchar(40) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `user_id` int unsigned DEFAULT NULL, + `user_id_keep` int unsigned DEFAULT NULL, + `app_id` int unsigned DEFAULT NULL, + `app_id_keep` int unsigned DEFAULT NULL, + `server_id` varchar(40) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `actor_type` varchar(40) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `tags` json DEFAULT NULL, + `fields` json DEFAULT NULL, + PRIMARY KEY (`id`), + KEY `fk_general_analytics_user_id` (`user_id`), + KEY `fk_general_analytics_app_id` (`app_id`), + CONSTRAINT `fk_general_analytics_app_id` FOREIGN KEY (`app_id`) REFERENCES `apps` (`id`) ON DELETE SET NULL ON UPDATE CASCADE, + CONSTRAINT `fk_general_analytics_user_id` FOREIGN KEY (`user_id`) REFERENCES `user` (`id`) ON DELETE SET NULL ON UPDATE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +CALL _puter_add_col('general_analytics', 'id', '`id` int unsigned NOT NULL AUTO_INCREMENT'); +CALL _puter_add_col('general_analytics', 'created_at', '`created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP'); +CALL _puter_add_col('general_analytics', 'uid', '`uid` char(40) COLLATE utf8mb4_unicode_ci NOT NULL'); +CALL _puter_add_col('general_analytics', 'trace_id', '`trace_id` varchar(40) COLLATE utf8mb4_unicode_ci DEFAULT NULL'); +CALL _puter_add_col('general_analytics', 'user_id', '`user_id` int unsigned DEFAULT NULL'); +CALL _puter_add_col('general_analytics', 'user_id_keep', '`user_id_keep` int unsigned DEFAULT NULL'); +CALL _puter_add_col('general_analytics', 'app_id', '`app_id` int unsigned DEFAULT NULL'); +CALL _puter_add_col('general_analytics', 'app_id_keep', '`app_id_keep` int unsigned DEFAULT NULL'); +CALL _puter_add_col('general_analytics', 'server_id', '`server_id` varchar(40) COLLATE utf8mb4_unicode_ci DEFAULT NULL'); +CALL _puter_add_col('general_analytics', 'actor_type', '`actor_type` varchar(40) COLLATE utf8mb4_unicode_ci DEFAULT NULL'); +CALL _puter_add_col('general_analytics', 'tags', '`tags` json DEFAULT NULL'); +CALL _puter_add_col('general_analytics', 'fields', '`fields` json DEFAULT NULL'); + +-- +-- Table structure for table `group` +-- + +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE IF NOT EXISTS `group` ( + `id` int unsigned NOT NULL AUTO_INCREMENT, + `uid` char(40) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `owner_user_id` int unsigned DEFAULT NULL, + `extra` json DEFAULT NULL, + `metadata` json DEFAULT NULL, + `created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (`id`), + UNIQUE KEY `uid` (`uid`), + KEY `owner_user_id` (`owner_user_id`), + CONSTRAINT `group_ibfk_1` FOREIGN KEY (`owner_user_id`) REFERENCES `user` (`id`) ON DELETE SET NULL ON UPDATE CASCADE +) ENGINE=InnoDB AUTO_INCREMENT=27 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +CALL _puter_add_col('group', 'id', '`id` int unsigned NOT NULL AUTO_INCREMENT'); +CALL _puter_add_col('group', 'uid', '`uid` char(40) COLLATE utf8mb4_unicode_ci DEFAULT NULL'); +CALL _puter_add_col('group', 'owner_user_id', '`owner_user_id` int unsigned DEFAULT NULL'); +CALL _puter_add_col('group', 'extra', '`extra` json DEFAULT NULL'); +CALL _puter_add_col('group', 'metadata', '`metadata` json DEFAULT NULL'); +CALL _puter_add_col('group', 'created_at', '`created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP'); + +-- +-- Table structure for table `jct_user_group` +-- + +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE IF NOT EXISTS `jct_user_group` ( + `id` int unsigned NOT NULL AUTO_INCREMENT, + `user_id` int unsigned NOT NULL, + `group_id` int unsigned NOT NULL, + `extra` json DEFAULT NULL, + `metadata` json DEFAULT NULL, + `created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (`id`), + KEY `user_id` (`user_id`), + KEY `group_id` (`group_id`), + CONSTRAINT `jct_user_group_ibfk_1` FOREIGN KEY (`user_id`) REFERENCES `user` (`id`) ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT `jct_user_group_ibfk_2` FOREIGN KEY (`group_id`) REFERENCES `group` (`id`) ON DELETE CASCADE ON UPDATE CASCADE +) ENGINE=InnoDB AUTO_INCREMENT=3513197 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +CALL _puter_add_col('jct_user_group', 'id', '`id` int unsigned NOT NULL AUTO_INCREMENT'); +CALL _puter_add_col('jct_user_group', 'user_id', '`user_id` int unsigned NOT NULL'); +CALL _puter_add_col('jct_user_group', 'group_id', '`group_id` int unsigned NOT NULL'); +CALL _puter_add_col('jct_user_group', 'extra', '`extra` json DEFAULT NULL'); +CALL _puter_add_col('jct_user_group', 'metadata', '`metadata` json DEFAULT NULL'); +CALL _puter_add_col('jct_user_group', 'created_at', '`created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP'); + +-- +-- Table structure for table `kv` +-- + +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE IF NOT EXISTS `kv` ( + `id` int unsigned NOT NULL AUTO_INCREMENT, + `app` char(40) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci DEFAULT NULL, + `user_id` int unsigned NOT NULL, + `kkey_hash` bigint unsigned NOT NULL, + `kkey` text NOT NULL, + `value` text, + `migrated` tinyint(1) DEFAULT '0', + PRIMARY KEY (`id`), + UNIQUE KEY `app_2` (`app`,`user_id`,`kkey_hash`), + KEY `app` (`app`), + KEY `user_id` (`user_id`), + KEY `kkey_hash` (`kkey_hash`), + CONSTRAINT `kv_ibfk_1` FOREIGN KEY (`user_id`) REFERENCES `user` (`id`) ON DELETE CASCADE ON UPDATE CASCADE +) ENGINE=InnoDB AUTO_INCREMENT=101649 DEFAULT CHARSET=utf8mb3; +/*!40101 SET character_set_client = @saved_cs_client */; + +CALL _puter_add_col('kv', 'id', '`id` int unsigned NOT NULL AUTO_INCREMENT'); +CALL _puter_add_col('kv', 'app', '`app` char(40) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci DEFAULT NULL'); +CALL _puter_add_col('kv', 'user_id', '`user_id` int unsigned NOT NULL'); +CALL _puter_add_col('kv', 'kkey_hash', '`kkey_hash` bigint unsigned NOT NULL'); +CALL _puter_add_col('kv', 'kkey', '`kkey` text NOT NULL'); +CALL _puter_add_col('kv', 'value', '`value` text'); +CALL _puter_add_col('kv', 'migrated', '`migrated` tinyint(1) DEFAULT ''0'''); + +-- +-- Table structure for table `monthly_usage_counts` +-- + +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE IF NOT EXISTS `monthly_usage_counts` ( + `year` int unsigned NOT NULL, + `month` int unsigned NOT NULL, + `service_type` varchar(40) NOT NULL, + `service_name` varchar(40) NOT NULL, + `actor_key` varchar(255) NOT NULL, + `pricing_category` json NOT NULL, + `pricing_category_hash` binary(20) NOT NULL, + `count` int unsigned DEFAULT '0', + `value_uint_1` int unsigned DEFAULT NULL, + `value_uint_2` int unsigned DEFAULT NULL, + `value_uint_3` int unsigned DEFAULT NULL, + PRIMARY KEY (`year`,`month`,`service_type`,`service_name`,`actor_key`,`pricing_category_hash`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +CALL _puter_add_col('monthly_usage_counts', 'year', '`year` int unsigned NOT NULL'); +CALL _puter_add_col('monthly_usage_counts', 'month', '`month` int unsigned NOT NULL'); +CALL _puter_add_col('monthly_usage_counts', 'service_type', '`service_type` varchar(40) NOT NULL'); +CALL _puter_add_col('monthly_usage_counts', 'service_name', '`service_name` varchar(40) NOT NULL'); +CALL _puter_add_col('monthly_usage_counts', 'actor_key', '`actor_key` varchar(255) NOT NULL'); +CALL _puter_add_col('monthly_usage_counts', 'pricing_category', '`pricing_category` json NOT NULL'); +CALL _puter_add_col('monthly_usage_counts', 'pricing_category_hash', '`pricing_category_hash` binary(20) NOT NULL'); +CALL _puter_add_col('monthly_usage_counts', 'count', '`count` int unsigned DEFAULT ''0'''); +CALL _puter_add_col('monthly_usage_counts', 'value_uint_1', '`value_uint_1` int unsigned DEFAULT NULL'); +CALL _puter_add_col('monthly_usage_counts', 'value_uint_2', '`value_uint_2` int unsigned DEFAULT NULL'); +CALL _puter_add_col('monthly_usage_counts', 'value_uint_3', '`value_uint_3` int unsigned DEFAULT NULL'); + +-- +-- Table structure for table `notification` +-- + +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE IF NOT EXISTS `notification` ( + `id` int unsigned NOT NULL AUTO_INCREMENT, + `user_id` int unsigned NOT NULL, + `uid` char(40) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `value` json NOT NULL, + `acknowledged` tinyint(1) DEFAULT NULL, + `shown` tinyint(1) DEFAULT NULL, + `created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (`id`), + UNIQUE KEY `uid` (`uid`), + KEY `user_id` (`user_id`), + CONSTRAINT `notification_ibfk_1` FOREIGN KEY (`user_id`) REFERENCES `user` (`id`) ON DELETE CASCADE ON UPDATE CASCADE +) ENGINE=InnoDB AUTO_INCREMENT=164238 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +CALL _puter_add_col('notification', 'id', '`id` int unsigned NOT NULL AUTO_INCREMENT'); +CALL _puter_add_col('notification', 'user_id', '`user_id` int unsigned NOT NULL'); +CALL _puter_add_col('notification', 'uid', '`uid` char(40) COLLATE utf8mb4_unicode_ci DEFAULT NULL'); +CALL _puter_add_col('notification', 'value', '`value` json NOT NULL'); +CALL _puter_add_col('notification', 'acknowledged', '`acknowledged` tinyint(1) DEFAULT NULL'); +CALL _puter_add_col('notification', 'shown', '`shown` tinyint(1) DEFAULT NULL'); +CALL _puter_add_col('notification', 'created_at', '`created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP'); + +-- +-- Table structure for table `old_app_names` +-- + +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE IF NOT EXISTS `old_app_names` ( + `id` int unsigned NOT NULL AUTO_INCREMENT, + `app_uid` char(40) CHARACTER SET latin1 COLLATE latin1_swedish_ci NOT NULL, + `name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL, + `timestamp` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (`id`), + KEY `app_uid` (`app_uid`), + KEY `old_app_names_app_name` (`name`), + CONSTRAINT `old_app_names_ibfk_1` FOREIGN KEY (`app_uid`) REFERENCES `apps` (`uid`) ON DELETE CASCADE +) ENGINE=InnoDB AUTO_INCREMENT=45405 DEFAULT CHARSET=utf8mb3; +/*!40101 SET character_set_client = @saved_cs_client */; + +CALL _puter_add_col('old_app_names', 'id', '`id` int unsigned NOT NULL AUTO_INCREMENT'); +CALL _puter_add_col('old_app_names', 'app_uid', '`app_uid` char(40) CHARACTER SET latin1 COLLATE latin1_swedish_ci NOT NULL'); +CALL _puter_add_col('old_app_names', 'name', '`name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL'); +CALL _puter_add_col('old_app_names', 'timestamp', '`timestamp` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP'); + +-- +-- Table structure for table `per_user_credit` +-- + +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE IF NOT EXISTS `per_user_credit` ( + `id` int unsigned NOT NULL AUTO_INCREMENT, + `user_id` int unsigned NOT NULL, + `amount` bigint NOT NULL, + `last_updated_at` bigint unsigned NOT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `user_id` (`user_id`), + CONSTRAINT `per_user_credit_ibfk_1` FOREIGN KEY (`user_id`) REFERENCES `user` (`id`) ON DELETE CASCADE ON UPDATE CASCADE +) ENGINE=InnoDB AUTO_INCREMENT=388003 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +CALL _puter_add_col('per_user_credit', 'id', '`id` int unsigned NOT NULL AUTO_INCREMENT'); +CALL _puter_add_col('per_user_credit', 'user_id', '`user_id` int unsigned NOT NULL'); +CALL _puter_add_col('per_user_credit', 'amount', '`amount` bigint NOT NULL'); +CALL _puter_add_col('per_user_credit', 'last_updated_at', '`last_updated_at` bigint unsigned NOT NULL'); + +-- +-- Table structure for table `service_usage_monthly` +-- + +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE IF NOT EXISTS `service_usage_monthly` ( + `key` varchar(255) CHARACTER SET ascii COLLATE ascii_general_ci NOT NULL, + `year` int unsigned NOT NULL, + `month` int unsigned NOT NULL, + `user_id` int unsigned DEFAULT NULL, + `app_id` int unsigned DEFAULT NULL, + `count` int unsigned NOT NULL, + `extra` json DEFAULT NULL, + PRIMARY KEY (`key`,`year`,`month`), + KEY `fk_service_usage_monthly_user_id` (`user_id`), + KEY `fk_service_usage_monthly_app_id` (`app_id`), + CONSTRAINT `fk_service_usage_monthly_app_id` FOREIGN KEY (`app_id`) REFERENCES `apps` (`id`) ON DELETE SET NULL ON UPDATE CASCADE, + CONSTRAINT `fk_service_usage_monthly_user_id` FOREIGN KEY (`user_id`) REFERENCES `user` (`id`) ON DELETE SET NULL ON UPDATE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +CALL _puter_add_col('service_usage_monthly', 'key', '`key` varchar(255) CHARACTER SET ascii COLLATE ascii_general_ci NOT NULL'); +CALL _puter_add_col('service_usage_monthly', 'year', '`year` int unsigned NOT NULL'); +CALL _puter_add_col('service_usage_monthly', 'month', '`month` int unsigned NOT NULL'); +CALL _puter_add_col('service_usage_monthly', 'user_id', '`user_id` int unsigned DEFAULT NULL'); +CALL _puter_add_col('service_usage_monthly', 'app_id', '`app_id` int unsigned DEFAULT NULL'); +CALL _puter_add_col('service_usage_monthly', 'count', '`count` int unsigned NOT NULL'); +CALL _puter_add_col('service_usage_monthly', 'extra', '`extra` json DEFAULT NULL'); + +-- +-- Table structure for table `sessions` +-- + +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE IF NOT EXISTS `sessions` ( + `id` int unsigned NOT NULL AUTO_INCREMENT, + `user_id` int unsigned NOT NULL, + `uuid` char(40) COLLATE utf8mb4_unicode_ci NOT NULL, + `meta` json DEFAULT NULL, + `created_at` bigint DEFAULT '0', + `last_activity` bigint DEFAULT '0', + PRIMARY KEY (`id`), + KEY `fk_sessions_user_id` (`user_id`), + KEY `uuid` (`uuid`), + CONSTRAINT `fk_sessions_user_id` FOREIGN KEY (`user_id`) REFERENCES `user` (`id`) ON DELETE CASCADE ON UPDATE CASCADE +) ENGINE=InnoDB AUTO_INCREMENT=2293466 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +CALL _puter_add_col('sessions', 'id', '`id` int unsigned NOT NULL AUTO_INCREMENT'); +CALL _puter_add_col('sessions', 'user_id', '`user_id` int unsigned NOT NULL'); +CALL _puter_add_col('sessions', 'uuid', '`uuid` char(40) COLLATE utf8mb4_unicode_ci NOT NULL'); +CALL _puter_add_col('sessions', 'meta', '`meta` json DEFAULT NULL'); +CALL _puter_add_col('sessions', 'created_at', '`created_at` bigint DEFAULT ''0'''); +CALL _puter_add_col('sessions', 'last_activity', '`last_activity` bigint DEFAULT ''0'''); + +-- +-- Table structure for table `share` +-- + +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE IF NOT EXISTS `share` ( + `id` int unsigned NOT NULL AUTO_INCREMENT, + `uid` char(40) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `issuer_user_id` int unsigned NOT NULL, + `recipient_email` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, + `data` json DEFAULT NULL, + `created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (`id`), + UNIQUE KEY `uid` (`uid`), + KEY `issuer_user_id` (`issuer_user_id`), + KEY `recipient_email` (`recipient_email`), + CONSTRAINT `share_ibfk_1` FOREIGN KEY (`issuer_user_id`) REFERENCES `user` (`id`) ON DELETE CASCADE ON UPDATE CASCADE +) ENGINE=InnoDB AUTO_INCREMENT=40 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +CALL _puter_add_col('share', 'id', '`id` int unsigned NOT NULL AUTO_INCREMENT'); +CALL _puter_add_col('share', 'uid', '`uid` char(40) COLLATE utf8mb4_unicode_ci DEFAULT NULL'); +CALL _puter_add_col('share', 'issuer_user_id', '`issuer_user_id` int unsigned NOT NULL'); +CALL _puter_add_col('share', 'recipient_email', '`recipient_email` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL'); +CALL _puter_add_col('share', 'data', '`data` json DEFAULT NULL'); +CALL _puter_add_col('share', 'created_at', '`created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP'); + +-- +-- Table structure for table `storage_audit` +-- + +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE IF NOT EXISTS `storage_audit` ( + `id` int unsigned NOT NULL AUTO_INCREMENT, + `user_id` int unsigned DEFAULT NULL, + `user_id_keep` int unsigned NOT NULL, + `is_subtract` tinyint(1) NOT NULL DEFAULT '0', + `amount` bigint unsigned NOT NULL, + `field_a` varchar(16) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `field_b` varchar(16) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `reason` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (`id`), + KEY `fk_storage_audit_user_id` (`user_id`), + CONSTRAINT `fk_storage_audit_user_id` FOREIGN KEY (`user_id`) REFERENCES `user` (`id`) ON DELETE SET NULL ON UPDATE CASCADE +) ENGINE=InnoDB AUTO_INCREMENT=22080 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +CALL _puter_add_col('storage_audit', 'id', '`id` int unsigned NOT NULL AUTO_INCREMENT'); +CALL _puter_add_col('storage_audit', 'user_id', '`user_id` int unsigned DEFAULT NULL'); +CALL _puter_add_col('storage_audit', 'user_id_keep', '`user_id_keep` int unsigned NOT NULL'); +CALL _puter_add_col('storage_audit', 'is_subtract', '`is_subtract` tinyint(1) NOT NULL DEFAULT ''0'''); +CALL _puter_add_col('storage_audit', 'amount', '`amount` bigint unsigned NOT NULL'); +CALL _puter_add_col('storage_audit', 'field_a', '`field_a` varchar(16) COLLATE utf8mb4_unicode_ci DEFAULT NULL'); +CALL _puter_add_col('storage_audit', 'field_b', '`field_b` varchar(16) COLLATE utf8mb4_unicode_ci DEFAULT NULL'); +CALL _puter_add_col('storage_audit', 'reason', '`reason` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL'); +CALL _puter_add_col('storage_audit', 'created_at', '`created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP'); + +-- +-- Table structure for table `subdomains` +-- + +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE IF NOT EXISTS `subdomains` ( + `id` int unsigned NOT NULL AUTO_INCREMENT, + `uuid` varchar(40) DEFAULT NULL, + `subdomain` varchar(64) NOT NULL, + `user_id` int unsigned NOT NULL, + `root_dir_id` int unsigned DEFAULT NULL, + `associated_app_id` int unsigned DEFAULT NULL, + `ts` timestamp NULL DEFAULT CURRENT_TIMESTAMP, + `app_owner` int unsigned DEFAULT NULL, + `protected` tinyint(1) DEFAULT '0', + `domain` varchar(265) DEFAULT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `subdomain` (`subdomain`), + UNIQUE KEY `uuid` (`uuid`), + KEY `user_id` (`user_id`), + KEY `root_dir` (`root_dir_id`), + KEY `associated_app_id` (`associated_app_id`), + KEY `fk_subdomains_app_owner` (`app_owner`), + KEY `idx_subdomains_domain` (`domain`), + KEY `idx_subdomains_root_user` (`root_dir_id`,`user_id`), + KEY `idx_subdomains_app_user` (`associated_app_id`,`user_id`), + CONSTRAINT `fk_subdomains_app_owner` FOREIGN KEY (`app_owner`) REFERENCES `apps` (`id`) ON DELETE SET NULL ON UPDATE CASCADE, + CONSTRAINT `subdomains_ibfk_1` FOREIGN KEY (`user_id`) REFERENCES `user` (`id`) ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT `subdomains_ibfk_2` FOREIGN KEY (`root_dir_id`) REFERENCES `fsentries` (`id`) ON DELETE SET NULL ON UPDATE CASCADE, + CONSTRAINT `subdomains_ibfk_3` FOREIGN KEY (`associated_app_id`) REFERENCES `apps` (`id`) ON DELETE CASCADE ON UPDATE CASCADE +) ENGINE=InnoDB AUTO_INCREMENT=171443 DEFAULT CHARSET=utf8mb3; +/*!40101 SET character_set_client = @saved_cs_client */; + +CALL _puter_add_col('subdomains', 'id', '`id` int unsigned NOT NULL AUTO_INCREMENT'); +CALL _puter_add_col('subdomains', 'uuid', '`uuid` varchar(40) DEFAULT NULL'); +CALL _puter_add_col('subdomains', 'subdomain', '`subdomain` varchar(64) NOT NULL'); +CALL _puter_add_col('subdomains', 'user_id', '`user_id` int unsigned NOT NULL'); +CALL _puter_add_col('subdomains', 'root_dir_id', '`root_dir_id` int unsigned DEFAULT NULL'); +CALL _puter_add_col('subdomains', 'associated_app_id', '`associated_app_id` int unsigned DEFAULT NULL'); +CALL _puter_add_col('subdomains', 'ts', '`ts` timestamp NULL DEFAULT CURRENT_TIMESTAMP'); +CALL _puter_add_col('subdomains', 'app_owner', '`app_owner` int unsigned DEFAULT NULL'); +CALL _puter_add_col('subdomains', 'protected', '`protected` tinyint(1) DEFAULT ''0'''); +CALL _puter_add_col('subdomains', 'domain', '`domain` varchar(265) DEFAULT NULL'); + +-- +-- Table structure for table `thread` +-- + +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE IF NOT EXISTS `thread` ( + `id` int unsigned NOT NULL AUTO_INCREMENT, + `uid` char(40) NOT NULL, + `parent_uid` char(40) DEFAULT NULL, + `owner_user_id` int unsigned NOT NULL, + `schema` text, + `text` text NOT NULL, + `created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (`id`), + UNIQUE KEY `uid` (`uid`), + KEY `parent_uid` (`parent_uid`), + KEY `owner_user_id` (`owner_user_id`), + KEY `idx_thread_uid` (`uid`), + CONSTRAINT `thread_ibfk_1` FOREIGN KEY (`parent_uid`) REFERENCES `thread` (`uid`) ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT `thread_ibfk_2` FOREIGN KEY (`owner_user_id`) REFERENCES `user` (`id`) ON DELETE CASCADE ON UPDATE CASCADE +) ENGINE=InnoDB AUTO_INCREMENT=1987 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +CALL _puter_add_col('thread', 'id', '`id` int unsigned NOT NULL AUTO_INCREMENT'); +CALL _puter_add_col('thread', 'uid', '`uid` char(40) NOT NULL'); +CALL _puter_add_col('thread', 'parent_uid', '`parent_uid` char(40) DEFAULT NULL'); +CALL _puter_add_col('thread', 'owner_user_id', '`owner_user_id` int unsigned NOT NULL'); +CALL _puter_add_col('thread', 'schema', '`schema` text'); +CALL _puter_add_col('thread', 'text', '`text` text NOT NULL'); +CALL _puter_add_col('thread', 'created_at', '`created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP'); + +-- +-- Table structure for table `user` +-- + +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE IF NOT EXISTS `user` ( + `id` int unsigned NOT NULL AUTO_INCREMENT, + `uuid` char(36) CHARACTER SET latin1 COLLATE latin1_swedish_ci NOT NULL, + `username` varchar(50) CHARACTER SET ascii COLLATE ascii_general_ci DEFAULT NULL, + `email` varchar(256) CHARACTER SET latin1 COLLATE latin1_swedish_ci DEFAULT NULL, + `password` varchar(225) CHARACTER SET latin1 COLLATE latin1_swedish_ci DEFAULT NULL, + `free_storage` bigint unsigned DEFAULT NULL, + `max_subdomains` int unsigned DEFAULT NULL, + `taskbar_items` text, + `desktop_uuid` char(36) CHARACTER SET latin1 COLLATE latin1_swedish_ci DEFAULT NULL, + `appdata_uuid` char(36) CHARACTER SET latin1 COLLATE latin1_swedish_ci DEFAULT NULL, + `documents_uuid` char(36) CHARACTER SET latin1 COLLATE latin1_swedish_ci DEFAULT NULL, + `pictures_uuid` char(36) CHARACTER SET latin1 COLLATE latin1_swedish_ci DEFAULT NULL, + `videos_uuid` char(36) CHARACTER SET latin1 COLLATE latin1_swedish_ci DEFAULT NULL, + `trash_uuid` char(36) CHARACTER SET latin1 COLLATE latin1_swedish_ci DEFAULT NULL, + `trash_id` int unsigned DEFAULT NULL, + `appdata_id` int unsigned DEFAULT NULL, + `desktop_id` int unsigned DEFAULT NULL, + `documents_id` int unsigned DEFAULT NULL, + `pictures_id` int unsigned DEFAULT NULL, + `videos_id` int unsigned DEFAULT NULL, + `referrer` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL, + `desktop_bg_url` text, + `desktop_bg_color` varchar(20) DEFAULT NULL, + `desktop_bg_fit` varchar(16) DEFAULT NULL, + `pass_recovery_token` char(36) DEFAULT NULL, + `requires_email_confirmation` tinyint(1) NOT NULL DEFAULT '0', + `email_confirm_code` varchar(8) DEFAULT NULL, + `email_confirm_token` char(36) CHARACTER SET latin1 COLLATE latin1_swedish_ci DEFAULT NULL, + `email_confirmed` tinyint(1) NOT NULL DEFAULT '0', + `dev_first_name` varchar(100) DEFAULT NULL, + `dev_last_name` varchar(100) DEFAULT NULL, + `dev_paypal` varchar(100) DEFAULT NULL, + `dev_approved_for_incentive_program` tinyint(1) DEFAULT '0', + `dev_joined_incentive_program` tinyint(1) DEFAULT '0', + `suspended` tinyint(1) DEFAULT NULL, + `unsubscribed` tinyint NOT NULL DEFAULT '0', + `timestamp` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + `last_activity_ts` timestamp NULL DEFAULT NULL, + `referral_code` varchar(16) DEFAULT NULL, + `referred_by` int unsigned DEFAULT NULL, + `unconfirmed_change_email` varchar(256) CHARACTER SET latin1 COLLATE latin1_swedish_ci DEFAULT NULL, + `change_email_confirm_token` varchar(256) CHARACTER SET latin1 COLLATE latin1_swedish_ci DEFAULT NULL, + `otp_secret` text, + `otp_enabled` tinyint(1) DEFAULT '0', + `otp_recovery_codes` text, + `stripe_customer_id` varchar(40) DEFAULT NULL, + `public_uuid` char(36) CHARACTER SET latin1 COLLATE latin1_swedish_ci DEFAULT NULL, + `public_id` int DEFAULT NULL, + `clean_email` varchar(256) DEFAULT NULL, + `audit_metadata` json DEFAULT NULL, + `signup_ip` varchar(45) DEFAULT NULL COMMENT 'Supports IPv6 addresses', + `signup_ip_forwarded` varchar(45) DEFAULT NULL COMMENT 'Supports IPv6 addresses', + `signup_user_agent` varchar(512) DEFAULT NULL, + `signup_origin` varchar(255) DEFAULT NULL, + `signup_server` varchar(255) DEFAULT NULL, + `metadata` json DEFAULT (json_object()), + `reputation` smallint DEFAULT '100', + PRIMARY KEY (`id`), + UNIQUE KEY `uid` (`uuid`), + UNIQUE KEY `username` (`username`), + UNIQUE KEY `referral_code` (`referral_code`), + KEY `email` (`email`), + KEY `pass_recovery_token` (`pass_recovery_token`), + KEY `referrer` (`referrer`), + KEY `email_confirm_token` (`email_confirm_token`), + KEY `last_activity_ts` (`last_activity_ts`), + KEY `desktop_uuid` (`desktop_uuid`), + KEY `appdata_uuid` (`appdata_uuid`), + KEY `documents_uuid` (`documents_uuid`), + KEY `pictures_uuid` (`pictures_uuid`), + KEY `videos_uuid` (`videos_uuid`), + KEY `trash_uuid` (`trash_uuid`), + KEY `trash_id` (`trash_id`), + KEY `appdata_id` (`appdata_id`), + KEY `desktop_id` (`desktop_id`), + KEY `documents_id` (`documents_id`), + KEY `pictures_id` (`pictures_id`), + KEY `videos_id` (`videos_id`), + KEY `idx_user_referral_code` (`referral_code`), + KEY `idx_user_referred_by` (`referred_by`), + KEY `referrer_2` (`referrer`), + KEY `idx_user_stripe_customer_id` (`stripe_customer_id`), + KEY `idx_user_clean_email` (`clean_email`), + KEY `idx_user_signup_ip` (`signup_ip`), + KEY `idx_user_signup_ip_forwarded` (`signup_ip_forwarded`), + KEY `idx_user_signup_user_agent` (`signup_user_agent`), + KEY `idx_user_signup_origin` (`signup_origin`), + KEY `idx_user_signup_server` (`signup_server`), + CONSTRAINT `fk_user_referred_by` FOREIGN KEY (`referred_by`) REFERENCES `user` (`id`) ON DELETE SET NULL ON UPDATE CASCADE +) ENGINE=InnoDB AUTO_INCREMENT=2987224 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +CALL _puter_add_col('user', 'id', '`id` int unsigned NOT NULL AUTO_INCREMENT'); +CALL _puter_add_col('user', 'uuid', '`uuid` char(36) CHARACTER SET latin1 COLLATE latin1_swedish_ci NOT NULL'); +CALL _puter_add_col('user', 'username', '`username` varchar(50) CHARACTER SET ascii COLLATE ascii_general_ci DEFAULT NULL'); +CALL _puter_add_col('user', 'email', '`email` varchar(256) CHARACTER SET latin1 COLLATE latin1_swedish_ci DEFAULT NULL'); +CALL _puter_add_col('user', 'password', '`password` varchar(225) CHARACTER SET latin1 COLLATE latin1_swedish_ci DEFAULT NULL'); +CALL _puter_add_col('user', 'free_storage', '`free_storage` bigint unsigned DEFAULT NULL'); +CALL _puter_add_col('user', 'max_subdomains', '`max_subdomains` int unsigned DEFAULT NULL'); +CALL _puter_add_col('user', 'taskbar_items', '`taskbar_items` text'); +CALL _puter_add_col('user', 'desktop_uuid', '`desktop_uuid` char(36) CHARACTER SET latin1 COLLATE latin1_swedish_ci DEFAULT NULL'); +CALL _puter_add_col('user', 'appdata_uuid', '`appdata_uuid` char(36) CHARACTER SET latin1 COLLATE latin1_swedish_ci DEFAULT NULL'); +CALL _puter_add_col('user', 'documents_uuid', '`documents_uuid` char(36) CHARACTER SET latin1 COLLATE latin1_swedish_ci DEFAULT NULL'); +CALL _puter_add_col('user', 'pictures_uuid', '`pictures_uuid` char(36) CHARACTER SET latin1 COLLATE latin1_swedish_ci DEFAULT NULL'); +CALL _puter_add_col('user', 'videos_uuid', '`videos_uuid` char(36) CHARACTER SET latin1 COLLATE latin1_swedish_ci DEFAULT NULL'); +CALL _puter_add_col('user', 'trash_uuid', '`trash_uuid` char(36) CHARACTER SET latin1 COLLATE latin1_swedish_ci DEFAULT NULL'); +CALL _puter_add_col('user', 'trash_id', '`trash_id` int unsigned DEFAULT NULL'); +CALL _puter_add_col('user', 'appdata_id', '`appdata_id` int unsigned DEFAULT NULL'); +CALL _puter_add_col('user', 'desktop_id', '`desktop_id` int unsigned DEFAULT NULL'); +CALL _puter_add_col('user', 'documents_id', '`documents_id` int unsigned DEFAULT NULL'); +CALL _puter_add_col('user', 'pictures_id', '`pictures_id` int unsigned DEFAULT NULL'); +CALL _puter_add_col('user', 'videos_id', '`videos_id` int unsigned DEFAULT NULL'); +CALL _puter_add_col('user', 'referrer', '`referrer` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL'); +CALL _puter_add_col('user', 'desktop_bg_url', '`desktop_bg_url` text'); +CALL _puter_add_col('user', 'desktop_bg_color', '`desktop_bg_color` varchar(20) DEFAULT NULL'); +CALL _puter_add_col('user', 'desktop_bg_fit', '`desktop_bg_fit` varchar(16) DEFAULT NULL'); +CALL _puter_add_col('user', 'pass_recovery_token', '`pass_recovery_token` char(36) DEFAULT NULL'); +CALL _puter_add_col('user', 'requires_email_confirmation', '`requires_email_confirmation` tinyint(1) NOT NULL DEFAULT ''0'''); +CALL _puter_add_col('user', 'email_confirm_code', '`email_confirm_code` varchar(8) DEFAULT NULL'); +CALL _puter_add_col('user', 'email_confirm_token', '`email_confirm_token` char(36) CHARACTER SET latin1 COLLATE latin1_swedish_ci DEFAULT NULL'); +CALL _puter_add_col('user', 'email_confirmed', '`email_confirmed` tinyint(1) NOT NULL DEFAULT ''0'''); +CALL _puter_add_col('user', 'dev_first_name', '`dev_first_name` varchar(100) DEFAULT NULL'); +CALL _puter_add_col('user', 'dev_last_name', '`dev_last_name` varchar(100) DEFAULT NULL'); +CALL _puter_add_col('user', 'dev_paypal', '`dev_paypal` varchar(100) DEFAULT NULL'); +CALL _puter_add_col('user', 'dev_approved_for_incentive_program', '`dev_approved_for_incentive_program` tinyint(1) DEFAULT ''0'''); +CALL _puter_add_col('user', 'dev_joined_incentive_program', '`dev_joined_incentive_program` tinyint(1) DEFAULT ''0'''); +CALL _puter_add_col('user', 'suspended', '`suspended` tinyint(1) DEFAULT NULL'); +CALL _puter_add_col('user', 'unsubscribed', '`unsubscribed` tinyint NOT NULL DEFAULT ''0'''); +CALL _puter_add_col('user', 'timestamp', '`timestamp` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP'); +CALL _puter_add_col('user', 'last_activity_ts', '`last_activity_ts` timestamp NULL DEFAULT NULL'); +CALL _puter_add_col('user', 'referral_code', '`referral_code` varchar(16) DEFAULT NULL'); +CALL _puter_add_col('user', 'referred_by', '`referred_by` int unsigned DEFAULT NULL'); +CALL _puter_add_col('user', 'unconfirmed_change_email', '`unconfirmed_change_email` varchar(256) CHARACTER SET latin1 COLLATE latin1_swedish_ci DEFAULT NULL'); +CALL _puter_add_col('user', 'change_email_confirm_token', '`change_email_confirm_token` varchar(256) CHARACTER SET latin1 COLLATE latin1_swedish_ci DEFAULT NULL'); +CALL _puter_add_col('user', 'otp_secret', '`otp_secret` text'); +CALL _puter_add_col('user', 'otp_enabled', '`otp_enabled` tinyint(1) DEFAULT ''0'''); +CALL _puter_add_col('user', 'otp_recovery_codes', '`otp_recovery_codes` text'); +CALL _puter_add_col('user', 'stripe_customer_id', '`stripe_customer_id` varchar(40) DEFAULT NULL'); +CALL _puter_add_col('user', 'public_uuid', '`public_uuid` char(36) CHARACTER SET latin1 COLLATE latin1_swedish_ci DEFAULT NULL'); +CALL _puter_add_col('user', 'public_id', '`public_id` int DEFAULT NULL'); +CALL _puter_add_col('user', 'clean_email', '`clean_email` varchar(256) DEFAULT NULL'); +CALL _puter_add_col('user', 'audit_metadata', '`audit_metadata` json DEFAULT NULL'); +CALL _puter_add_col('user', 'signup_ip', '`signup_ip` varchar(45) DEFAULT NULL COMMENT ''Supports IPv6 addresses'''); +CALL _puter_add_col('user', 'signup_ip_forwarded', '`signup_ip_forwarded` varchar(45) DEFAULT NULL COMMENT ''Supports IPv6 addresses'''); +CALL _puter_add_col('user', 'signup_user_agent', '`signup_user_agent` varchar(512) DEFAULT NULL'); +CALL _puter_add_col('user', 'signup_origin', '`signup_origin` varchar(255) DEFAULT NULL'); +CALL _puter_add_col('user', 'signup_server', '`signup_server` varchar(255) DEFAULT NULL'); +CALL _puter_add_col('user', 'metadata', '`metadata` json DEFAULT (json_object())'); +CALL _puter_add_col('user', 'reputation', '`reputation` smallint DEFAULT ''100'''); + +-- +-- Table structure for table `user_comments` +-- + +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE IF NOT EXISTS `user_comments` ( + `id` int unsigned NOT NULL AUTO_INCREMENT, + `uid` char(40) NOT NULL, + `user_id` int unsigned NOT NULL, + `metadata` json DEFAULT NULL, + `text` text NOT NULL, + `created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (`id`), + UNIQUE KEY `uid` (`uid`), + KEY `user_id` (`user_id`), + KEY `idx_user_comments_uid` (`uid`), + CONSTRAINT `user_comments_ibfk_1` FOREIGN KEY (`user_id`) REFERENCES `user` (`id`) ON DELETE CASCADE ON UPDATE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3; +/*!40101 SET character_set_client = @saved_cs_client */; + +CALL _puter_add_col('user_comments', 'id', '`id` int unsigned NOT NULL AUTO_INCREMENT'); +CALL _puter_add_col('user_comments', 'uid', '`uid` char(40) NOT NULL'); +CALL _puter_add_col('user_comments', 'user_id', '`user_id` int unsigned NOT NULL'); +CALL _puter_add_col('user_comments', 'metadata', '`metadata` json DEFAULT NULL'); +CALL _puter_add_col('user_comments', 'text', '`text` text NOT NULL'); +CALL _puter_add_col('user_comments', 'created_at', '`created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP'); + +-- +-- Table structure for table `user_fsentry_comments` +-- + +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE IF NOT EXISTS `user_fsentry_comments` ( + `id` int unsigned NOT NULL AUTO_INCREMENT, + `user_comment_id` int unsigned NOT NULL, + `fsentry_id` int unsigned NOT NULL, + PRIMARY KEY (`id`), + KEY `user_comment_id` (`user_comment_id`), + KEY `fsentry_id` (`fsentry_id`), + CONSTRAINT `user_fsentry_comments_ibfk_1` FOREIGN KEY (`user_comment_id`) REFERENCES `user_comments` (`id`) ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT `user_fsentry_comments_ibfk_2` FOREIGN KEY (`fsentry_id`) REFERENCES `fsentries` (`id`) ON DELETE CASCADE ON UPDATE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3; +/*!40101 SET character_set_client = @saved_cs_client */; + +CALL _puter_add_col('user_fsentry_comments', 'id', '`id` int unsigned NOT NULL AUTO_INCREMENT'); +CALL _puter_add_col('user_fsentry_comments', 'user_comment_id', '`user_comment_id` int unsigned NOT NULL'); +CALL _puter_add_col('user_fsentry_comments', 'fsentry_id', '`fsentry_id` int unsigned NOT NULL'); + +-- +-- Table structure for table `user_oidc_providers` +-- + +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE IF NOT EXISTS `user_oidc_providers` ( + `id` int unsigned NOT NULL AUTO_INCREMENT, + `user_id` int unsigned NOT NULL, + `provider` varchar(64) NOT NULL, + `provider_sub` varchar(255) NOT NULL, + `refresh_token` text, + `created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (`id`), + KEY `user_id` (`user_id`), + KEY `idx_user_oidc_providers_provider` (`provider`), + CONSTRAINT `user_oidc_providers_ibfk_1` FOREIGN KEY (`user_id`) REFERENCES `user` (`id`) ON DELETE CASCADE ON UPDATE CASCADE +) ENGINE=InnoDB AUTO_INCREMENT=51584 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +CALL _puter_add_col('user_oidc_providers', 'id', '`id` int unsigned NOT NULL AUTO_INCREMENT'); +CALL _puter_add_col('user_oidc_providers', 'user_id', '`user_id` int unsigned NOT NULL'); +CALL _puter_add_col('user_oidc_providers', 'provider', '`provider` varchar(64) NOT NULL'); +CALL _puter_add_col('user_oidc_providers', 'provider_sub', '`provider_sub` varchar(255) NOT NULL'); +CALL _puter_add_col('user_oidc_providers', 'refresh_token', '`refresh_token` text'); +CALL _puter_add_col('user_oidc_providers', 'created_at', '`created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP'); + +-- +-- Table structure for table `user_to_app_permissions` +-- + +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE IF NOT EXISTS `user_to_app_permissions` ( + `user_id` int unsigned NOT NULL, + `app_id` int unsigned NOT NULL, + `permission` varchar(255) CHARACTER SET ascii COLLATE ascii_general_ci NOT NULL, + `extra` json DEFAULT NULL, + `dt` datetime DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (`user_id`,`app_id`,`permission`), + KEY `idx_utap_user_permission` (`user_id`,`permission`), + KEY `idx_utap_app_permission` (`app_id`,`permission`), + CONSTRAINT `fk_user_to_app_permissions_app_id` FOREIGN KEY (`app_id`) REFERENCES `apps` (`id`) ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT `fk_user_to_app_permissions_user_id` FOREIGN KEY (`user_id`) REFERENCES `user` (`id`) ON DELETE CASCADE ON UPDATE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +CALL _puter_add_col('user_to_app_permissions', 'user_id', '`user_id` int unsigned NOT NULL'); +CALL _puter_add_col('user_to_app_permissions', 'app_id', '`app_id` int unsigned NOT NULL'); +CALL _puter_add_col('user_to_app_permissions', 'permission', '`permission` varchar(255) CHARACTER SET ascii COLLATE ascii_general_ci NOT NULL'); +CALL _puter_add_col('user_to_app_permissions', 'extra', '`extra` json DEFAULT NULL'); +CALL _puter_add_col('user_to_app_permissions', 'dt', '`dt` datetime DEFAULT CURRENT_TIMESTAMP'); + +-- +-- Table structure for table `user_to_group_permissions` +-- + +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE IF NOT EXISTS `user_to_group_permissions` ( + `user_id` int unsigned NOT NULL, + `group_id` int unsigned NOT NULL, + `permission` varchar(255) CHARACTER SET ascii COLLATE ascii_general_ci NOT NULL, + `extra` json DEFAULT NULL, + PRIMARY KEY (`user_id`,`group_id`,`permission`), + KEY `group_id` (`group_id`), + CONSTRAINT `user_to_group_permissions_ibfk_1` FOREIGN KEY (`user_id`) REFERENCES `user` (`id`) ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT `user_to_group_permissions_ibfk_2` FOREIGN KEY (`group_id`) REFERENCES `group` (`id`) ON DELETE CASCADE ON UPDATE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +CALL _puter_add_col('user_to_group_permissions', 'user_id', '`user_id` int unsigned NOT NULL'); +CALL _puter_add_col('user_to_group_permissions', 'group_id', '`group_id` int unsigned NOT NULL'); +CALL _puter_add_col('user_to_group_permissions', 'permission', '`permission` varchar(255) CHARACTER SET ascii COLLATE ascii_general_ci NOT NULL'); +CALL _puter_add_col('user_to_group_permissions', 'extra', '`extra` json DEFAULT NULL'); + +-- +-- Table structure for table `user_to_user_permissions` +-- + +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE IF NOT EXISTS `user_to_user_permissions` ( + `issuer_user_id` int unsigned NOT NULL, + `holder_user_id` int unsigned NOT NULL, + `permission` varchar(255) CHARACTER SET ascii COLLATE ascii_general_ci NOT NULL, + `extra` json DEFAULT NULL, + PRIMARY KEY (`issuer_user_id`,`holder_user_id`,`permission`), + KEY `fk_user_to_user_permissions_holder_user_id` (`holder_user_id`), + CONSTRAINT `fk_user_to_user_permissions_holder_user_id` FOREIGN KEY (`holder_user_id`) REFERENCES `user` (`id`) ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT `fk_user_to_user_permissions_issuer_user_id` FOREIGN KEY (`issuer_user_id`) REFERENCES `user` (`id`) ON DELETE CASCADE ON UPDATE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +CALL _puter_add_col('user_to_user_permissions', 'issuer_user_id', '`issuer_user_id` int unsigned NOT NULL'); +CALL _puter_add_col('user_to_user_permissions', 'holder_user_id', '`holder_user_id` int unsigned NOT NULL'); +CALL _puter_add_col('user_to_user_permissions', 'permission', '`permission` varchar(255) CHARACTER SET ascii COLLATE ascii_general_ci NOT NULL'); +CALL _puter_add_col('user_to_user_permissions', 'extra', '`extra` json DEFAULT NULL'); + +-- +-- Table structure for table `user_update_audit` +-- + +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE IF NOT EXISTS `user_update_audit` ( + `id` int unsigned NOT NULL AUTO_INCREMENT, + `user_id` int unsigned DEFAULT NULL, + `user_id_keep` int unsigned NOT NULL, + `created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + `old_email` varchar(256) CHARACTER SET latin1 COLLATE latin1_swedish_ci DEFAULT NULL, + `new_email` varchar(256) CHARACTER SET latin1 COLLATE latin1_swedish_ci DEFAULT NULL, + `old_username` varchar(50) CHARACTER SET latin1 COLLATE latin1_swedish_ci DEFAULT NULL, + `new_username` varchar(50) CHARACTER SET latin1 COLLATE latin1_swedish_ci DEFAULT NULL, + `reason` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + PRIMARY KEY (`id`), + KEY `fk_user_update_audit_user_id` (`user_id`), + CONSTRAINT `fk_user_update_audit_user_id` FOREIGN KEY (`user_id`) REFERENCES `user` (`id`) ON DELETE SET NULL ON UPDATE CASCADE +) ENGINE=InnoDB AUTO_INCREMENT=8078 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +CALL _puter_add_col('user_update_audit', 'id', '`id` int unsigned NOT NULL AUTO_INCREMENT'); +CALL _puter_add_col('user_update_audit', 'user_id', '`user_id` int unsigned DEFAULT NULL'); +CALL _puter_add_col('user_update_audit', 'user_id_keep', '`user_id_keep` int unsigned NOT NULL'); +CALL _puter_add_col('user_update_audit', 'created_at', '`created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP'); +CALL _puter_add_col('user_update_audit', 'old_email', '`old_email` varchar(256) CHARACTER SET latin1 COLLATE latin1_swedish_ci DEFAULT NULL'); +CALL _puter_add_col('user_update_audit', 'new_email', '`new_email` varchar(256) CHARACTER SET latin1 COLLATE latin1_swedish_ci DEFAULT NULL'); +CALL _puter_add_col('user_update_audit', 'old_username', '`old_username` varchar(50) CHARACTER SET latin1 COLLATE latin1_swedish_ci DEFAULT NULL'); +CALL _puter_add_col('user_update_audit', 'new_username', '`new_username` varchar(50) CHARACTER SET latin1 COLLATE latin1_swedish_ci DEFAULT NULL'); +CALL _puter_add_col('user_update_audit', 'reason', '`reason` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL'); + + +DROP PROCEDURE IF EXISTS _puter_add_col; +/*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */; + +/*!40101 SET SQL_MODE=@OLD_SQL_MODE */; +/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */; +/*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */; +/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */; +/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */; +/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */; +/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */; + +-- Dump completed on 2026-05-02 0:09:09 diff --git a/src/backend/clients/database/migrations/mysql/mysql_mig_2.sql b/src/backend/clients/database/migrations/mysql/mysql_mig_2.sql new file mode 100644 index 000000000..922215098 --- /dev/null +++ b/src/backend/clients/database/migrations/mysql/mysql_mig_2.sql @@ -0,0 +1,34 @@ +-- Copyright (C) 2024-present Puter Technologies Inc. +-- +-- Default apps (editor, viewer, pdf, camera, player, recorder, git, +-- dev-center, puter-linux). Folds the equivalent SQLite migrations' +-- final state (subsequent godmode / maximize_on_start UPDATEs baked in, +-- all owners set to user.id=1 = admin). +-- +-- INSERT IGNORE makes it safe to re-run; uid has a UNIQUE constraint. +-- +-- FK temporarily disabled because apps.owner_user_id references user.id, +-- and the admin (id=1) is created by DefaultUserService AFTER this +-- migration runs. Once the admin row exists, the references resolve. + +/*!40014 SET @OLD_FK = @@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */; + +INSERT IGNORE INTO `apps` (`uid`, `owner_user_id`, `icon`, `name`, `title`, `description`, `index_url`, `godmode`, `maximize_on_start`, `background`, `approved_for_listing`, `approved_for_opening_items`, `approved_for_incentive_program`, `tags`, `timestamp`) VALUES ('app-838dfbc4-bf8b-48c2-b47b-c4adc77fab58', 1, 'data:image/svg+xml;base64,PHN2ZyB2ZXJzaW9uPSIxLjIiIGJhc2VQcm9maWxlPSJ0aW55LXBzIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA0OCA0OCIgd2lkdGg9IjQ4IiBoZWlnaHQ9IjQ4Ij4KCTx0aXRsZT5hcHAtaWNvbi1lZGl0b3Itc3ZnPC90aXRsZT4KCTxkZWZzPgoJCTxsaW5lYXJHcmFkaWVudCBpZD0iZ3JkMSIgZ3JhZGllbnRVbml0cz0idXNlclNwYWNlT25Vc2UiICB4MT0iNDciIHkxPSIzOS41MTQiIHgyPSIxIiB5Mj0iOC40ODYiPgoJCQk8c3RvcCBvZmZzZXQ9IjAiIHN0b3AtY29sb3I9IiM3MTAxZTgiICAvPgoJCQk8c3RvcCBvZmZzZXQ9IjEiIHN0b3AtY29sb3I9IiM5MTY3YmUiICAvPgoJCTwvbGluZWFyR3JhZGllbnQ+Cgk8L2RlZnM+Cgk8c3R5bGU+CgkJdHNwYW4geyB3aGl0ZS1zcGFjZTpwcmUgfQoJCS5zaHAwIHsgZmlsbDogdXJsKCNncmQxKSB9IAoJCS5zaHAxIHsgZmlsbDogI2ZmZmZmZiB9IAoJPC9zdHlsZT4KCTxnIGlkPSJMYXllciI+CgkJPHBhdGggaWQ9IkxheWVyIiBjbGFzcz0ic2hwMCIgZD0iTTQ3IDNMNDcgNDVDNDcgNDYuMSA0Ni4xIDQ3IDQ1IDQ3TDMgNDdDMS45IDQ3IDEgNDYuMSAxIDQ1TDEgM0MxIDEuOSAxLjkgMSAzIDFMNDUgMUM0Ni4xIDEgNDcgMS45IDQ3IDNaIiAvPgoJCTxwYXRoIGlkPSJMYXllciIgZmlsbC1ydWxlPSJldmVub2RkIiBjbGFzcz0ic2hwMSIgZD0iTTI4LjYyIDQwTDI4LjYyIDM3LjYxTDMyLjI1IDM3LjIyTDI5Ljg2IDMwTDE3LjUzIDMwTDE1LjE4IDM3LjIyTDE4Ljc2IDM3LjYxTDE4Ljc2IDQwTDguNiA0MEw4LjYgMzcuNjZMMTAuNSAzNy4xN0MxMS4yMSAzNi45OSAxMS40MyAzNi44NiAxMS42IDM2LjMzTDIxLjMzIDhMMjYuNDUgOEwzNi4zNiAzNi4zOEMzNi41MyAzNi45MSAzNi44OCAzNi45OSAzNy40MiAzNy4xM0wzOS40IDM3LjYxTDM5LjQgNDBMMjguNjIgNDBaTTIzLjc2IDExLjQ1TDE4LjU0IDI3TDI4Ljg4IDI3TDIzLjc2IDExLjQ1WiIgLz4KCTwvZz4KPC9zdmc+', 'editor', 'Editor', 'A simple text editor', 'https://editor.puter.com/index.html', 0, 0, 0, 1, 1, 0, NULL, '2020-01-01 00:00:00'); + +INSERT IGNORE INTO `apps` (`uid`, `owner_user_id`, `icon`, `name`, `title`, `description`, `index_url`, `godmode`, `maximize_on_start`, `background`, `approved_for_listing`, `approved_for_opening_items`, `approved_for_incentive_program`, `tags`, `timestamp`) VALUES ('app-7870be61-8dff-4a99-af64-e9ae6811e367', 1, 'data:image/svg+xml;base64,PHN2ZyB2ZXJzaW9uPSIxLjIiIGJhc2VQcm9maWxlPSJ0aW55LXBzIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA0OCA0OCIgd2lkdGg9IjQ4IiBoZWlnaHQ9IjQ4Ij4KCTx0aXRsZT5hcHAtaWNvbi12aWV3ZXItc3ZnPC90aXRsZT4KCTxkZWZzPgoJCTxsaW5lYXJHcmFkaWVudCBpZD0iZ3JkMSIgZ3JhZGllbnRVbml0cz0idXNlclNwYWNlT25Vc2UiICB4MT0iNDciIHkxPSIzOS41MTQiIHgyPSIxIiB5Mj0iOC40ODYiPgoJCQk8c3RvcCBvZmZzZXQ9IjAiIHN0b3AtY29sb3I9IiMwMzYzYWQiICAvPgoJCQk8c3RvcCBvZmZzZXQ9IjEiIHN0b3AtY29sb3I9IiM1Njg0ZjUiICAvPgoJCTwvbGluZWFyR3JhZGllbnQ+Cgk8L2RlZnM+Cgk8c3R5bGU+CgkJdHNwYW4geyB3aGl0ZS1zcGFjZTpwcmUgfQoJCS5zaHAwIHsgZmlsbDogdXJsKCNncmQxKSB9IAoJCS5zaHAxIHsgZmlsbDogI2ZmZDc2NCB9IAoJCS5zaHAyIHsgZmlsbDogI2NiZWFmYiB9IAoJPC9zdHlsZT4KCTxnIGlkPSJMYXllciI+CgkJPHBhdGggaWQ9IlNoYXBlIDEiIGNsYXNzPSJzaHAwIiBkPSJNMSAxTDQ3IDFMNDcgNDdMMSA0N0wxIDFaIiAvPgoJCTxwYXRoIGlkPSJMYXllciIgY2xhc3M9InNocDEiIGQ9Ik0xOCAxOEMxNS43OSAxOCAxNCAxNi4yMSAxNCAxNEMxNCAxMS43OSAxNS43OSAxMCAxOCAxMEMyMC4yMSAxMCAyMiAxMS43OSAyMiAxNEMyMiAxNi4yMSAyMC4yMSAxOCAxOCAxOFoiIC8+CgkJPHBhdGggaWQ9IkxheWVyIiBjbGFzcz0ic2hwMiIgZD0iTTM5Ljg2IDM2LjUxQzM5LjgyIDM2LjU4IDM5Ljc3IDM2LjY1IDM5LjcgMzYuNzFDMzkuNjQgMzYuNzcgMzkuNTcgMzYuODIgMzkuNSAzNi44N0MzOS40MiAzNi45MSAzOS4zNCAzNi45NCAzOS4yNiAzNi45N0MzOS4xNyAzNi45OSAzOS4wOSAzNyAzOSAzN0w5IDM3QzguODIgMzcgOC42NCAzNi45NSA4LjQ5IDM2Ljg2QzguMzMgMzYuNzYgOC4yIDM2LjYzIDguMTIgMzYuNDdDOC4wMyAzNi4zMSA3Ljk5IDM2LjEzIDggMzUuOTVDOC4wMSAzNS43NyA4LjA3IDM1LjYgOC4xNyAzNS40NEwxNC4xNyAyNi40NUMxNC4yNCAyNi4zNCAxNC4zMyAyNi4yNCAxNC40NCAyNi4xN0MxNC41NSAyNi4xIDE0LjY4IDI2LjA0IDE0LjggMjYuMDJDMTQuOTMgMjUuOTkgMTUuMDcgMjUuOTkgMTUuMTkgMjYuMDJDMTUuMzIgMjYuMDQgMTUuNDUgMjYuMSAxNS41NSAyNi4xN0MxNS41NyAyNi4xOCAxNS41OCAyNi4xOSAxNS42IDI2LjJDMTUuNjEgMjYuMjEgMTUuNjIgMjYuMjIgMTUuNjMgMjYuMjNDMTUuNjUgMjYuMjQgMTUuNjYgMjYuMjUgMTUuNjcgMjYuMjZDMTUuNjggMjYuMjcgMTUuNyAyNi4yOCAxNS43MSAyNi4yOUwyMC44NiAzMS40NUwyOS4xOCAxOS40M0MyOS4yMyAxOS4zNiAyOS4yOCAxOS4zIDI5LjM1IDE5LjI0QzI5LjQxIDE5LjE5IDI5LjQ4IDE5LjE0IDI5LjU2IDE5LjFDMjkuNjMgMTkuMDYgMjkuNzEgMTkuMDQgMjkuNzkgMTkuMDJDMjkuODggMTkgMjkuOTYgMTkgMzAuMDUgMTlDMzAuMTMgMTkgMzAuMjEgMTkuMDIgMzAuMjkgMTkuMDRDMzAuMzggMTkuMDcgMzAuNDUgMTkuMSAzMC41MiAxOS4xNUMzMC42IDE5LjE5IDMwLjY2IDE5LjI1IDMwLjcyIDE5LjMxQzMwLjc4IDE5LjM3IDMwLjgzIDE5LjQ0IDMwLjg3IDE5LjUxTDM5Ljg3IDM1LjUxQzM5LjkxIDM1LjU5IDM5Ljk1IDM1LjY3IDM5Ljk3IDM1Ljc1QzM5Ljk5IDM1Ljg0IDQwIDM1LjkyIDQwIDM2LjAxQzQwIDM2LjEgMzkuOTkgMzYuMTggMzkuOTYgMzYuMjdDMzkuOTQgMzYuMzUgMzkuOTEgMzYuNDMgMzkuODYgMzYuNTFaIiAvPgoJPC9nPgo8L3N2Zz4=', 'viewer', 'Viewer', '', 'https://viewer.puter.com/index.html', 0, 1, 0, 1, 0, 0, NULL, '2020-01-01 00:00:00'); + +INSERT IGNORE INTO `apps` (`uid`, `owner_user_id`, `icon`, `name`, `title`, `description`, `index_url`, `godmode`, `maximize_on_start`, `background`, `approved_for_listing`, `approved_for_opening_items`, `approved_for_incentive_program`, `tags`, `timestamp`) VALUES ('app-3920851d-bda8-479b-9407-8517293c7d44', 1, 'data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iaXNvLTg4NTktMSI/Pg0KPCEtLSBHZW5lcmF0b3I6IEFkb2JlIElsbHVzdHJhdG9yIDE4LjAuMCwgU1ZHIEV4cG9ydCBQbHVnLUluIC4gU1ZHIFZlcnNpb246IDYuMDAgQnVpbGQgMCkgIC0tPg0KPCFET0NUWVBFIHN2ZyBQVUJMSUMgIi0vL1czQy8vRFREIFNWRyAxLjEvL0VOIiAiaHR0cDovL3d3dy53My5vcmcvR3JhcGhpY3MvU1ZHLzEuMS9EVEQvc3ZnMTEuZHRkIj4NCjxzdmcgdmVyc2lvbj0iMS4xIiBpZD0iQ2FwYV8xIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB4PSIwcHgiIHk9IjBweCINCgkgdmlld0JveD0iMCAwIDU2IDU2IiBzdHlsZT0iZW5hYmxlLWJhY2tncm91bmQ6bmV3IDAgMCA1NiA1NjsiIHhtbDpzcGFjZT0icHJlc2VydmUiPg0KPGc+DQoJPHBhdGggc3R5bGU9ImZpbGw6I0U5RTlFMDsiIGQ9Ik0zNi45ODUsMEg3Ljk2M0M3LjE1NSwwLDYuNSwwLjY1NSw2LjUsMS45MjZWNTVjMCwwLjM0NSwwLjY1NSwxLDEuNDYzLDFoNDAuMDc0DQoJCWMwLjgwOCwwLDEuNDYzLTAuNjU1LDEuNDYzLTFWMTIuOTc4YzAtMC42OTYtMC4wOTMtMC45Mi0wLjI1Ny0xLjA4NUwzNy42MDcsMC4yNTdDMzcuNDQyLDAuMDkzLDM3LjIxOCwwLDM2Ljk4NSwweiIvPg0KCTxwb2x5Z29uIHN0eWxlPSJmaWxsOiNEOUQ3Q0E7IiBwb2ludHM9IjM3LjUsMC4xNTEgMzcuNSwxMiA0OS4zNDksMTIgCSIvPg0KCTxwYXRoIHN0eWxlPSJmaWxsOiNDQzRCNEM7IiBkPSJNMTkuNTE0LDMzLjMyNEwxOS41MTQsMzMuMzI0Yy0wLjM0OCwwLTAuNjgyLTAuMTEzLTAuOTY3LTAuMzI2DQoJCWMtMS4wNDEtMC43ODEtMS4xODEtMS42NS0xLjExNS0yLjI0MmMwLjE4Mi0xLjYyOCwyLjE5NS0zLjMzMiw1Ljk4NS01LjA2OGMxLjUwNC0zLjI5NiwyLjkzNS03LjM1NywzLjc4OC0xMC43NQ0KCQljLTAuOTk4LTIuMTcyLTEuOTY4LTQuOTktMS4yNjEtNi42NDNjMC4yNDgtMC41NzksMC41NTctMS4wMjMsMS4xMzQtMS4yMTVjMC4yMjgtMC4wNzYsMC44MDQtMC4xNzIsMS4wMTYtMC4xNzINCgkJYzAuNTA0LDAsMC45NDcsMC42NDksMS4yNjEsMS4wNDljMC4yOTUsMC4zNzYsMC45NjQsMS4xNzMtMC4zNzMsNi44MDJjMS4zNDgsMi43ODQsMy4yNTgsNS42Miw1LjA4OCw3LjU2Mg0KCQljMS4zMTEtMC4yMzcsMi40MzktMC4zNTgsMy4zNTgtMC4zNThjMS41NjYsMCwyLjUxNSwwLjM2NSwyLjkwMiwxLjExN2MwLjMyLDAuNjIyLDAuMTg5LDEuMzQ5LTAuMzksMi4xNg0KCQljLTAuNTU3LDAuNzc5LTEuMzI1LDEuMTkxLTIuMjIsMS4xOTFjLTEuMjE2LDAtMi42MzItMC43NjgtNC4yMTEtMi4yODVjLTIuODM3LDAuNTkzLTYuMTUsMS42NTEtOC44MjgsMi44MjINCgkJYy0wLjgzNiwxLjc3NC0xLjYzNywzLjIwMy0yLjM4Myw0LjI1MUMyMS4yNzMsMzIuNjU0LDIwLjM4OSwzMy4zMjQsMTkuNTE0LDMzLjMyNHogTTIyLjE3NiwyOC4xOTgNCgkJYy0yLjEzNywxLjIwMS0zLjAwOCwyLjE4OC0zLjA3MSwyLjc0NGMtMC4wMSwwLjA5Mi0wLjAzNywwLjMzNCwwLjQzMSwwLjY5MkMxOS42ODUsMzEuNTg3LDIwLjU1NSwzMS4xOSwyMi4xNzYsMjguMTk4eg0KCQkgTTM1LjgxMywyMy43NTZjMC44MTUsMC42MjcsMS4wMTQsMC45NDQsMS41NDcsMC45NDRjMC4yMzQsMCwwLjkwMS0wLjAxLDEuMjEtMC40NDFjMC4xNDktMC4yMDksMC4yMDctMC4zNDMsMC4yMy0wLjQxNQ0KCQljLTAuMTIzLTAuMDY1LTAuMjg2LTAuMTk3LTEuMTc1LTAuMTk3QzM3LjEyLDIzLjY0OCwzNi40ODUsMjMuNjcsMzUuODEzLDIzLjc1NnogTTI4LjM0MywxNy4xNzQNCgkJYy0wLjcxNSwyLjQ3NC0xLjY1OSw1LjE0NS0yLjY3NCw3LjU2NGMyLjA5LTAuODExLDQuMzYyLTEuNTE5LDYuNDk2LTIuMDJDMzAuODE1LDIxLjE1LDI5LjQ2NiwxOS4xOTIsMjguMzQzLDE3LjE3NHoNCgkJIE0yNy43MzYsOC43MTJjLTAuMDk4LDAuMDMzLTEuMzMsMS43NTcsMC4wOTYsMy4yMTZDMjguNzgxLDkuODEzLDI3Ljc3OSw4LjY5OCwyNy43MzYsOC43MTJ6Ii8+DQoJPHBhdGggc3R5bGU9ImZpbGw6I0NDNEI0QzsiIGQ9Ik00OC4wMzcsNTZINy45NjNDNy4xNTUsNTYsNi41LDU1LjM0NSw2LjUsNTQuNTM3VjM5aDQzdjE1LjUzN0M0OS41LDU1LjM0NSw0OC44NDUsNTYsNDguMDM3LDU2eiIvPg0KCTxnPg0KCQk8cGF0aCBzdHlsZT0iZmlsbDojRkZGRkZGOyIgZD0iTTE3LjM4NSw1M2gtMS42NDFWNDIuOTI0aDIuODk4YzAuNDI4LDAsMC44NTIsMC4wNjgsMS4yNzEsMC4yMDUNCgkJCWMwLjQxOSwwLjEzNywwLjc5NSwwLjM0MiwxLjEyOCwwLjYxNWMwLjMzMywwLjI3MywwLjYwMiwwLjYwNCwwLjgwNywwLjk5MXMwLjMwOCwwLjgyMiwwLjMwOCwxLjMwNg0KCQkJYzAsMC41MTEtMC4wODcsMC45NzMtMC4yNiwxLjM4OGMtMC4xNzMsMC40MTUtMC40MTUsMC43NjQtMC43MjUsMS4wNDZjLTAuMzEsMC4yODItMC42ODQsMC41MDEtMS4xMjEsMC42NTYNCgkJCXMtMC45MjEsMC4yMzItMS40NDksMC4yMzJoLTEuMjE3VjUzeiBNMTcuMzg1LDQ0LjE2OHYzLjk5MmgxLjUwNGMwLjIsMCwwLjM5OC0wLjAzNCwwLjU5NS0wLjEwMw0KCQkJYzAuMTk2LTAuMDY4LDAuMzc2LTAuMTgsMC41NC0wLjMzNWMwLjE2NC0wLjE1NSwwLjI5Ni0wLjM3MSwwLjM5Ni0wLjY0OWMwLjEtMC4yNzgsMC4xNS0wLjYyMiwwLjE1LTEuMDMyDQoJCQljMC0wLjE2NC0wLjAyMy0wLjM1NC0wLjA2OC0wLjU2N2MtMC4wNDYtMC4yMTQtMC4xMzktMC40MTktMC4yOC0wLjYxNWMtMC4xNDItMC4xOTYtMC4zNC0wLjM2LTAuNTk1LTAuNDkyDQoJCQljLTAuMjU1LTAuMTMyLTAuNTkzLTAuMTk4LTEuMDEyLTAuMTk4SDE3LjM4NXoiLz4NCgkJPHBhdGggc3R5bGU9ImZpbGw6I0ZGRkZGRjsiIGQ9Ik0zMi4yMTksNDcuNjgyYzAsMC44MjktMC4wODksMS41MzgtMC4yNjcsMi4xMjZzLTAuNDAzLDEuMDgtMC42NzcsMS40NzdzLTAuNTgxLDAuNzA5LTAuOTIzLDAuOTM3DQoJCQlzLTAuNjcyLDAuMzk4LTAuOTkxLDAuNTEzYy0wLjMxOSwwLjExNC0wLjYxMSwwLjE4Ny0wLjg3NSwwLjIxOUMyOC4yMjIsNTIuOTg0LDI4LjAyNiw1MywyNy44OTgsNTNoLTMuODE0VjQyLjkyNGgzLjAzNQ0KCQkJYzAuODQ4LDAsMS41OTMsMC4xMzUsMi4yMzUsMC40MDNzMS4xNzYsMC42MjcsMS42LDEuMDczczAuNzQsMC45NTUsMC45NSwxLjUyNEMzMi4xMTQsNDYuNDk0LDMyLjIxOSw0Ny4wOCwzMi4yMTksNDcuNjgyeg0KCQkJIE0yNy4zNTIsNTEuNzk3YzEuMTEyLDAsMS45MTQtMC4zNTUsMi40MDYtMS4wNjZzMC43MzgtMS43NDEsMC43MzgtMy4wOWMwLTAuNDE5LTAuMDUtMC44MzQtMC4xNS0xLjI0NA0KCQkJYy0wLjEwMS0wLjQxLTAuMjk0LTAuNzgxLTAuNTgxLTEuMTE0cy0wLjY3Ny0wLjYwMi0xLjE2OS0wLjgwN3MtMS4xMy0wLjMwOC0xLjkxNC0wLjMwOGgtMC45NTd2Ny42MjlIMjcuMzUyeiIvPg0KCQk8cGF0aCBzdHlsZT0iZmlsbDojRkZGRkZGOyIgZD0iTTM2LjI2Niw0NC4xNjh2My4xNzJoNC4yMTF2MS4xMjFoLTQuMjExVjUzaC0xLjY2OFY0Mi45MjRINDAuOXYxLjI0NEgzNi4yNjZ6Ii8+DQoJPC9nPg0KPC9nPg0KPGc+DQo8L2c+DQo8Zz4NCjwvZz4NCjxnPg0KPC9nPg0KPGc+DQo8L2c+DQo8Zz4NCjwvZz4NCjxnPg0KPC9nPg0KPGc+DQo8L2c+DQo8Zz4NCjwvZz4NCjxnPg0KPC9nPg0KPGc+DQo8L2c+DQo8Zz4NCjwvZz4NCjxnPg0KPC9nPg0KPGc+DQo8L2c+DQo8Zz4NCjwvZz4NCjxnPg0KPC9nPg0KPC9zdmc+DQo=', 'pdf', 'PDF', '', 'https://pdf.puter.com/index.html', 0, 1, 0, 1, 0, 0, 'productivity', '2020-01-01 00:00:00'); + +INSERT IGNORE INTO `apps` (`uid`, `owner_user_id`, `icon`, `name`, `title`, `description`, `index_url`, `godmode`, `maximize_on_start`, `background`, `approved_for_listing`, `approved_for_opening_items`, `approved_for_incentive_program`, `tags`, `timestamp`) VALUES ('app-5584fbf7-ed69-41fc-99cd-85da21b1ef51', 1, 'data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4KPHN2ZyB2aWV3Qm94PSIwIDAgNTEyIDUxMiIgd2lkdGg9IjUxMiIgaGVpZ2h0PSI1MTIiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CiAgPGRlZnM+CiAgICA8bGluZWFyR3JhZGllbnQgZ3JhZGllbnRVbml0cz0idXNlclNwYWNlT25Vc2UiIHgxPSIyNTYiIHkxPSIwIiB4Mj0iMjU2IiB5Mj0iNTEyIiBpZD0iZ3JhZGllbnQtMCI+CiAgICAgIDxzdG9wIG9mZnNldD0iMCIgc3R5bGU9InN0b3AtY29sb3I6IHJnYigwLCAxMiwgMTA4KTsiLz4KICAgICAgPHN0b3Agb2Zmc2V0PSIxIiBzdHlsZT0ic3RvcC1jb2xvcjogcmdiKDE2LCAwLCAxNDkpOyIvPgogICAgPC9saW5lYXJHcmFkaWVudD4KICA8L2RlZnM+CiAgPHJlY3Qgc3R5bGU9InBhaW50LW9yZGVyOiBmaWxsOyBmaWxsLXJ1bGU6IG5vbnplcm87IGZpbGw6IHVybCgnI2dyYWRpZW50LTAnKTsiIHg9IjAiIHk9IjAiIHdpZHRoPSI1MTIiIGhlaWdodD0iNTEyIiByeD0iNzAiIHJ5PSI3MCIvPgogIDxjaXJjbGUgY3g9IjE3OC4zMzciIGN5PSIyNTguODc2IiBmaWxsPSIjYzBkYWRjIiByPSIyOSIgc3R5bGU9IiIgdHJhbnNmb3JtPSJtYXRyaXgoNi4xMDExMTEsIDAsIDAsIDYuMTI2OTY2LCAtODMzLjU4ODg2NywgLTEzMzAuODY4MDQyKSIvPgogIDxjaXJjbGUgY3g9IjE3OC4zMzciIGN5PSIyNTguODc2IiBmaWxsPSIjNGQ2ZmM0IiByPSIyMyIgc3R5bGU9IiIgdHJhbnNmb3JtPSJtYXRyaXgoNi4xMDExMTEsIDAsIDAsIDYuMTI2OTY2LCAtODMzLjU4ODg2NywgLTEzMzAuODY4MDQyKSIvPgogIDxjaXJjbGUgY3g9IjE3OC4zMzciIGN5PSIyNTguODc2IiBmaWxsPSIjM2Q1ZmEzIiByPSIxOCIgc3R5bGU9IiIgdHJhbnNmb3JtPSJtYXRyaXgoNi4xMDExMTEsIDAsIDAsIDYuMTI2OTY2LCAtODMzLjU4ODg2NywgLTEzMzAuODY4MDQyKSIvPgogIDxwYXRoIGQ9Ik0gMjExLjAyNSAxODguNjU2IEMgMjYyLjE0NiAxNTUuMDA2IDMzMC4zNzQgMTg5LjU1IDMzMy44MzQgMjUwLjgzOCBDIDMzNy4yOTMgMzEyLjEyNyAyNzMuMzkgMzU0LjE4OSAyMTguODA5IDMyNi41NTUgQyAxNzYuNDc0IDMwNS4xMjMgMTYyLjE1NSAyNTEuNDUxIDE4OC4xNDYgMjExLjYzMiBMIDIxMS4wMjUgMTg4LjY1NiBaIiBmaWxsPSIjMmY0Yjc3IiBzdHlsZT0iIi8+CiAgPGcgZmlsbD0iI2ZmZiIgdHJhbnNmb3JtPSJtYXRyaXgoNi4xMDExMTEsIDAsIDAsIDYuMTI2OTY2LCA3MS40MzIxOSwgNzEuNDQ5NjIzKSIgc3R5bGU9IiI+CiAgICA8Y2lyY2xlIGN4PSIyNCIgY3k9IjI0IiByPSI1Ii8+CiAgICA8Y2lyY2xlIGN4PSIzMi41IiBjeT0iMzIuNSIgcj0iMi41Ii8+CiAgPC9nPgo8L3N2Zz4=', 'camera', 'Camera', 'Camera in the browser.', 'https://camera.puter.com/index.html', 0, 0, 0, 1, 0, 0, NULL, '2020-01-01 00:00:00'); + +INSERT IGNORE INTO `apps` (`uid`, `owner_user_id`, `icon`, `name`, `title`, `description`, `index_url`, `godmode`, `maximize_on_start`, `background`, `approved_for_listing`, `approved_for_opening_items`, `approved_for_incentive_program`, `tags`, `timestamp`) VALUES ('app-11edfba2-1ed3-4e22-8573-47e88fb87d70', 1, 'data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iaXNvLTg4NTktMSI/Pg0KPCEtLSBHZW5lcmF0b3I6IEFkb2JlIElsbHVzdHJhdG9yIDE5LjAuMCwgU1ZHIEV4cG9ydCBQbHVnLUluIC4gU1ZHIFZlcnNpb246IDYuMDAgQnVpbGQgMCkgIC0tPg0KPHN2ZyB2ZXJzaW9uPSIxLjEiIGlkPSJMYXllcl8xIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB4PSIwcHgiIHk9IjBweCINCgkgdmlld0JveD0iMCAwIDUxMi4wMDEgNTEyLjAwMSIgc3R5bGU9ImVuYWJsZS1iYWNrZ3JvdW5kOm5ldyAwIDAgNTEyLjAwMSA1MTIuMDAxOyIgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+DQo8cGF0aCBzdHlsZT0iZmlsbDojNTE1MDRFOyIgZD0iTTQ5MC42NjUsNDMuNTU3SDIxLjMzM0M5LjU1Miw0My41NTcsMCw1My4xMDgsMCw2NC44OXYzODIuMjJjMCwxMS43ODIsOS41NTIsMjEuMzM0LDIxLjMzMywyMS4zMzQNCgloNDY5LjMzMmMxMS43ODMsMCwyMS4zMzUtOS41NTIsMjEuMzM1LTIxLjMzNFY2NC44OUM1MTIsNTMuMTA4LDUwMi40NDgsNDMuNTU3LDQ5MC42NjUsNDMuNTU3eiBNOTkuMDMsNDI3LjA1MUg1Ni4yNjd2LTM4LjA2OQ0KCUg5OS4wM1Y0MjcuMDUxeiBNOTkuMDMsMTIzLjAxOUg1Ni4yNjd2LTM4LjA3SDk5LjAzVjEyMy4wMTl6IE0xODguMjA2LDQyNy4wNTFoLTQyLjc2M3YtMzguMDY5aDQyLjc2M1Y0MjcuMDUxeiBNMTg4LjIwNiwxMjMuMDE5DQoJaC00Mi43NjN2LTM4LjA3aDQyLjc2M1YxMjMuMDE5eiBNMjc3LjM4Miw0MjcuMDUxaC00Mi43NjR2LTM4LjA2OWg0Mi43NjRWNDI3LjA1MXogTTI3Ny4zODIsMTIzLjAxOWgtNDIuNzY0di0zOC4wN2g0Mi43NjRWMTIzLjAxOQ0KCXogTTM2Ni41NTcsNDI3LjA1MWgtNDIuNzYzdi0zOC4wNjloNDIuNzYzVjQyNy4wNTF6IE0zNjYuNTU3LDEyMy4wMTloLTQyLjc2M3YtMzguMDdoNDIuNzYzVjEyMy4wMTl6IE00NTUuNzMzLDQyNy4wNTFINDEyLjk3DQoJdi0zOC4wNjloNDIuNzY0djM4LjA2OUg0NTUuNzMzeiBNNDU1LjczMywxMjMuMDE5SDQxMi45N3YtMzguMDdoNDIuNzY0djM4LjA3SDQ1NS43MzN6Ii8+DQo8cGF0aCBzdHlsZT0iZmlsbDojNkI2OTY4OyIgZD0iTTQ5MC42NjUsNDMuNTU3SDEzMy44MWMtMTYuMzQzLDM4Ljg3Ny0yNS4zODEsODEuNTgtMjUuMzgxLDEyNi4zOTYNCgljMCwxMzMuMTkyLDc5Ljc4MiwyNDcuNzM0LDE5NC4xNTUsMjk4LjQ5aDE4OC4wODJjMTEuNzgzLDAsMjEuMzM1LTkuNTUyLDIxLjMzNS0yMS4zMzRWNjQuODkNCglDNTEyLDUzLjEwOCw1MDIuNDQ4LDQzLjU1Nyw0OTAuNjY1LDQzLjU1N3ogTTE4OC4yMDYsMTIzLjAxOWgtNDIuNzYzdi0zOC4wN2g0Mi43NjNWMTIzLjAxOXogTTI3Ny4zODIsNDI3LjA1MWgtNDIuNzY0di0zOC4wNjkNCgloNDIuNzY0VjQyNy4wNTF6IE0yNzcuMzgyLDEyMy4wMTloLTQyLjc2NHYtMzguMDdoNDIuNzY0VjEyMy4wMTl6IE0zNjYuNTU3LDQyNy4wNTFoLTQyLjc2M3YtMzguMDY5aDQyLjc2M1Y0MjcuMDUxeg0KCSBNMzY2LjU1NywxMjMuMDE5aC00Mi43NjN2LTM4LjA3aDQyLjc2M1YxMjMuMDE5eiBNNDU1LjczMyw0MjcuMDUxSDQxMi45N3YtMzguMDY5aDQyLjc2NHYzOC4wNjlINDU1LjczM3ogTTQ1NS43MzMsMTIzLjAxOUg0MTIuOTcNCgl2LTM4LjA3aDQyLjc2NHYzOC4wN0g0NTUuNzMzeiIvPg0KPHBhdGggc3R5bGU9ImZpbGw6Izg4RENFNTsiIGQ9Ik0zMTguNjEyLDI0My42NTdsLTExMi44OC01Ni40NGMtOS4xOTEtNC41OTUtMTkuOTc0LDIuMTMtMTkuOTc0LDEyLjM0NlYzMTIuNDQNCgljMCwxMC4yNjcsMTAuODM3LDE2LjkyNywxOS45NzQsMTIuMzQ1bDExMi44OC01Ni40MzljNC42NzQtMi4zMzgsNy42MjgtNy4xMTcsNy42MjgtMTIuMzQ1DQoJQzMyNi4yNCwyNTAuNzc0LDMyMy4yODYsMjQ1Ljk5NSwzMTguNjEyLDI0My42NTd6Ii8+DQo8cGF0aCBzdHlsZT0iZmlsbDojNzRDNEM0OyIgZD0iTTIxMS41MTUsMTk5LjU2MmMwLTIuOTY4LDAuOTU3LTUuODAyLDIuNjUyLTguMTI4bC04LjQzNS00LjIxOA0KCWMtOS4xOTEtNC41OTUtMTkuOTc0LDIuMTMtMTkuOTc0LDEyLjM0NlYzMTIuNDRjMCwxMC4yNjcsMTAuODM3LDE2LjkyNywxOS45NzQsMTIuMzQ1bDguNDMzLTQuMjE3DQoJQzIxMC41MDgsMzE1LjU0NywyMTEuNTE1LDMyMS45NjksMjExLjUxNSwxOTkuNTYyeiIvPg0KPGc+DQo8L2c+DQo8Zz4NCjwvZz4NCjxnPg0KPC9nPg0KPGc+DQo8L2c+DQo8Zz4NCjwvZz4NCjxnPg0KPC9nPg0KPGc+DQo8L2c+DQo8Zz4NCjwvZz4NCjxnPg0KPC9nPg0KPGc+DQo8L2c+DQo8Zz4NCjwvZz4NCjxnPg0KPC9nPg0KPGc+DQo8L2c+DQo8Zz4NCjwvZz4NCjxnPg0KPC9nPg0KPC9zdmc+DQo=', 'player', 'Player', 'A free video player app in the browser.', 'https://player.puter.com/index.html', 0, 0, 0, 1, 0, 0, NULL, '2020-01-01 00:00:00'); + +INSERT IGNORE INTO `apps` (`uid`, `owner_user_id`, `icon`, `name`, `title`, `description`, `index_url`, `godmode`, `maximize_on_start`, `background`, `approved_for_listing`, `approved_for_opening_items`, `approved_for_incentive_program`, `tags`, `timestamp`) VALUES ('app-7bdca1a4-6373-4c98-ad97-03ff2d608ca1', 1, 'data:image/svg+xml;base64,PHN2ZyB2ZXJzaW9uPSIxLjIiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgdmlld0JveD0iMCAwIDUxMiA1MTIiIHdpZHRoPSI1MTIiIGhlaWdodD0iNTEyIj48ZGVmcz48aW1hZ2UgIHdpZHRoPSIzNjEiIGhlaWdodD0iMzYxIiBpZD0iaW1nMSIgaHJlZj0iZGF0YTppbWFnZS9wbmc7YmFzZTY0LGlWQk9SdzBLR2dvQUFBQU5TVWhFVWdBQUFXa0FBQUZwQVFNQUFBQmt0VXNOQUFBQUFYTlNSMElCMmNrc2Z3QUFBQU5RVEZSRi8vLy9wOFFieUFBQUFDZEpSRUZVZUp6dHdRRU5BQUFBd3FEM1QyMFBCeFFBQUFBQUFBQUFBQUFBQUFBQUFBQUFCd1pDUndBQlJ3bDNjZ0FBQUFCSlJVNUVya0pnZ2c9PSIvPjxsaW5lYXJHcmFkaWVudCBpZD0iUCIgZ3JhZGllbnRVbml0cz0idXNlclNwYWNlT25Vc2UiLz48bGluZWFyR3JhZGllbnQgaWQ9ImcxIiB4MT0iMjMiIHkxPSI0ODkiIHgyPSI0ODkiIHkyPSIyMyIgaHJlZj0iI1AiPjxzdG9wIHN0b3AtY29sb3I9IiNmY2M2MGUiLz48c3RvcCBvZmZzZXQ9IjEiIHN0b3AtY29sb3I9IiNlOTJlMjkiLz48L2xpbmVhckdyYWRpZW50PjwvZGVmcz48c3R5bGU+LmF7ZmlsbDp1cmwoI2cxKX08L3N0eWxlPjx1c2UgIGhyZWY9IiNpbWcxIiB4PSI3NSIgeT0iNzYiLz48cGF0aCBmaWxsLXJ1bGU9ImV2ZW5vZGQiIGNsYXNzPSJhIiBkPSJtNTEyIDc4LjR2MzU1LjJjMCA0My4yLTM1LjIgNzguNC03OC40IDc4LjRoLTM1NS4yYy00My4yIDAtNzguNC0zNS4yLTc4LjQtNzguNHYtMzU1LjJjMC00My4yIDM1LjItNzguNCA3OC40LTc4LjRoMzU1LjJjNDMuMiAwIDc4LjQgMzUuMiA3OC40IDc4LjR6bS0zMjQuMyAxNzkuNWMwIDM0LjIgMjcuOSA2MiA2MiA2MmgxMi42YzM0LjEgMCA2Mi0yNy44IDYyLTYydi0xMDEuOWMwLTM0LjItMjcuOS02Mi02Mi02MmgtMTIuNmMtMzQuMSAwLTYyIDI3LjgtNjIgNjJ6bTI0IDB2LTEwMS45YzAtMjEgMTcuMS0zOCAzOC0zOGgxMi42YzIwLjkgMCAzOCAxNyAzOCAzOHYxMDEuOWMwIDIxLTE3LjEgMzgtMzggMzhoLTEyLjZjLTIwLjkgMC0zOC0xNy0zOC0zOHptMTY1LjQtNi4zYzAtNi42LTUuMy0xMi0xMi0xMi02LjYgMC0xMiA1LjQtMTIgMTIgMCA1My42LTQzLjUgOTcuMi05Ny4xIDk3LjItNTMuNiAwLTk3LjEtNDMuNi05Ny4xLTk3LjIgMC02LjYtNS40LTExLjktMTItMTEuOS02LjcgMC0xMiA1LjMtMTIgMTEuOSAwIDYyLjggNDcuOSAxMTQuNSAxMDkuMSAxMjAuNnYzMy44YzAgNi42IDUuNCAxMiAxMiAxMiA2LjYgMCAxMi01LjQgMTItMTJ2LTMzLjhjNjEuMi02LjEgMTA5LjEtNTcuOCAxMDkuMS0xMjAuNnoiLz48L3N2Zz4=', 'recorder', 'Recorder', 'Online voice recorder in the browser with cloud storage. Take voice memos by recording through your mic directly in your web browser on any device.', 'https://recorder.puter.com/index.html', 0, 0, 0, 1, 0, 0, NULL, '2020-01-01 00:00:00'); + +INSERT IGNORE INTO `apps` (`uid`, `owner_user_id`, `icon`, `name`, `title`, `description`, `index_url`, `godmode`, `maximize_on_start`, `background`, `approved_for_listing`, `approved_for_opening_items`, `approved_for_incentive_program`, `tags`, `timestamp`) VALUES ('app-e3ac5486-da8c-42ad-8377-8728086e0980', 1, 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI5MnB0IiBoZWlnaHQ9IjkycHQiIHZpZXdCb3g9IjAgMCA5MiA5MiI+PGRlZnM+PGNsaXBQYXRoIGlkPSJhIj48cGF0aCBkPSJNMCAuMTEzaDkxLjg4N1Y5MkgwWm0wIDAiLz48L2NsaXBQYXRoPjwvZGVmcz48ZyBjbGlwLXBhdGg9InVybCgjYSkiPjxwYXRoIHN0eWxlPSJzdHJva2U6bm9uZTtmaWxsLXJ1bGU6bm9uemVybztmaWxsOiNmMDNjMmU7ZmlsbC1vcGFjaXR5OjEiIGQ9Ik05MC4xNTYgNDEuOTY1IDUwLjAzNiAxLjg0OGE1LjkxOCA1LjkxOCAwIDAgMC04LjM3MiAwbC04LjMyOCA4LjMzMiAxMC41NjYgMTAuNTY2YTcuMDMgNy4wMyAwIDAgMSA3LjIzIDEuNjg0IDcuMDM0IDcuMDM0IDAgMCAxIDEuNjY5IDcuMjc3bDEwLjE4NyAxMC4xODRhNy4wMjggNy4wMjggMCAwIDEgNy4yNzggMS42NzIgNy4wNCA3LjA0IDAgMCAxIDAgOS45NTcgNy4wNSA3LjA1IDAgMCAxLTkuOTY1IDAgNy4wNDQgNy4wNDQgMCAwIDEtMS41MjgtNy42NmwtOS41LTkuNDk3VjU5LjM2YTcuMDQgNy4wNCAwIDAgMSAxLjg2IDExLjI5IDcuMDQgNy4wNCAwIDAgMS05Ljk1NyAwIDcuMDQgNy4wNCAwIDAgMSAwLTkuOTU4IDcuMDYgNy4wNiAwIDAgMSAyLjMwNC0xLjUzOVYzMy45MjZhNy4wNDkgNy4wNDkgMCAwIDEtMy44Mi05LjIzNEwyOS4yNDIgMTQuMjcyIDEuNzMgNDEuNzc3YTUuOTI1IDUuOTI1IDAgMCAwIDAgOC4zNzFMNDEuODUyIDkwLjI3YTUuOTI1IDUuOTI1IDAgMCAwIDguMzcgMGwzOS45MzQtMzkuOTM0YTUuOTI1IDUuOTI1IDAgMCAwIDAtOC4zNzEiLz48L2c+PC9zdmc+', 'git', 'Git', 'Puter Git client', 'https://builtins.namespaces.puter.com/git', 0, 0, 1, 1, 0, 0, 'productivity', '2020-01-01 00:00:00'); + +INSERT IGNORE INTO `apps` (`uid`, `owner_user_id`, `icon`, `name`, `title`, `description`, `index_url`, `godmode`, `maximize_on_start`, `background`, `approved_for_listing`, `approved_for_opening_items`, `approved_for_incentive_program`, `tags`, `timestamp`) VALUES ('app-0b37f054-07d4-4627-8765-11bd23e889d4', 1, 'data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4KPHN2ZyB3aWR0aD0iMTE2IiBoZWlnaHQ9IjEzNiIgdmlld0JveD0iMCAwIDExNiAxMTYiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CiAgPHBhdGggZD0iTSAwLjEyOSA2Mi4wODYgTCAyOC4xMjkgNzQuMDg1IEwgMjguMTI5IDEwOC4wODUgTCAwLjEyOSA5Ni42NDQgTCAwLjEyOSA2Mi4wODYgWiIgc3R5bGU9ImZpbGw6IHJnYigxNjQsIDczLCA3MSk7Ii8+CiAgPHBhdGggZD0iTSAyOS4xMjkgMTA4LjA4NSBMIDU3LjEyOSA5Ni4wODUgTCA1Ny4xMjkgNjIuMDg2IEwgMjkuMTI5IDc0LjA4NSBMIDI5LjEyOSAxMDguMDg1IFoiIHN0eWxlPSJmaWxsOiByZ2IoMTM1LCA1OCwgNTgpOyIvPgogIDxwYXRoIGQ9Ik0gMC4xMjkgNjEuMTc5IEwgMjguNjI5IDczLjA4NSBMIDU3LjI3NiA2MS4xNzkgTCAyOS4xMjkgNTAuMDg2IEwgMC4xMjkgNjEuMTc5IFoiIHN0eWxlPSJmaWxsOiByZ2IoMTk2LCA4NSwgODUpOyIvPgogIDxwYXRoIGQ9Ik0gMjkuMTI5IDE0LjA4NiBMIDU3LjEyOSAyNi4wODYgTCA1Ny4xMjkgNTkuMDg2IEwgMjkuMTI5IDQ4LjA4NiBMIDI5LjEyOSAxNC4wODYgWiIgc3R5bGU9ImZpbGw6IHJnYig0MSwgMTE1LCAyMDIpOyIvPgogIDxwYXRoIGQ9Ik0gNTguMTI5IDU5LjA4NiBMIDg3LjEyOSA0OC4wODYgTCA4Ny4xMjkgMTQuMDg2IEwgNTguMTI5IDI2LjA4NiBMIDU4LjEyOSA1OS4wODYgWiIgc3R5bGU9ImZpbGw6IHJnYigzMiwgODksIDE1OCk7Ii8+CiAgPHBhdGggZD0iTSAyOS4xMjkgMTMuMDg2IEwgNTguMTI5IDI1LjA4NiBMIDg3LjEyOSAxMy4wODYgTCA1OC4xMjkgMS4wODYgTCAyOS4xMjkgMTMuMDg2IFoiIHN0eWxlPSJmaWxsOiByZ2IoNDcsIDEzNCwgMjM2KTsiLz4KICA8cGF0aCBkPSJNIDU5LjEyOSA2Mi4wODYgTCA4Ny4xMjkgNzQuMDg1IEwgODcuMTI5IDEwOC4wODUgTCA1OS4xMjkgOTYuMDg1IEwgNTkuMTI5IDYyLjA4NiBaIiBzdHlsZT0iZmlsbDogcmdiKDM0LCAxNzksIDApOyIvPgogIDxwYXRoIGQ9Ik0gODguMTI5IDEwOC4wODUgTCAxMTYuMTI5IDk2LjE1MSBMIDExNi4xMjkgNjIuMDg2IEwgODguMTI5IDc0LjA4NSBMIDg4LjEyOSAxMDguMDg1IFoiIHN0eWxlPSJmaWxsOiByZ2IoMjYsIDEzNiwgMCk7Ii8+CiAgPHBhdGggZD0iTSA1OS4xMjkgNjEuMDg2IEwgODcuNjI5IDczLjA4NSBMIDExNi4xMjkgNjEuMDg2IEwgODcuMTI5IDUwLjA4NiBMIDU5LjEyOSA2MS4wODYgWiIgc3R5bGU9ImZpbGw6IHJnYig0MCwgMjEzLCAwKTsiLz4KICA8ZGVmcy8+Cjwvc3ZnPg==', 'dev-center', 'Dev Center', 'This is the app that makes apps', 'https://builtins.namespaces.puter.com/dev-center', 1, 1, 0, 1, 1, 0, NULL, '2020-01-01 00:00:00'); + +INSERT IGNORE INTO `apps` (`uid`, `owner_user_id`, `icon`, `name`, `title`, `description`, `index_url`, `godmode`, `maximize_on_start`, `background`, `approved_for_listing`, `approved_for_opening_items`, `approved_for_incentive_program`, `tags`, `timestamp`) VALUES ('app-fbbdb72b-ad08-4cb4-86a1-de0f27cf2e1e', 1, NULL, 'puter-linux', 'Puter Linux', 'Linux emulator for Puter', 'https://builtins.namespaces.puter.com/emulator', 1, 0, 0, 1, 1, 0, NULL, '2020-01-01 00:00:00'); + +/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FK */; diff --git a/src/backend/clients/database/migrations/0001_create-tables.sql b/src/backend/clients/database/migrations/sqlite/0001_create-tables.sql similarity index 96% rename from src/backend/clients/database/migrations/0001_create-tables.sql rename to src/backend/clients/database/migrations/sqlite/0001_create-tables.sql index 3b79df5f8..025a62846 100644 --- a/src/backend/clients/database/migrations/0001_create-tables.sql +++ b/src/backend/clients/database/migrations/sqlite/0001_create-tables.sql @@ -19,7 +19,6 @@ DROP TABLE IF EXISTS `monthly_usage_counts`; DROP TABLE IF EXISTS `access_token_permissions`; -DROP TABLE IF EXISTS `auth_audit`; DROP TABLE IF EXISTS `general_analytics`; DROP TABLE IF EXISTS `audit_user_to_app_permissions`; DROP TABLE IF EXISTS `user_to_app_permissions`; @@ -343,26 +342,6 @@ CREATE TABLE `general_analytics` ( FOREIGN KEY (`app_id`) REFERENCES `apps` (`id`) ON DELETE SET NULL ON UPDATE CASCADE ); --- 0014 - -CREATE TABLE `auth_audit` ( - `id` INTEGER PRIMARY KEY, - `created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, - - `uid` CHAR(40) NOT NULL, - `ip_address` VARCHAR(45) DEFAULT NULL, - `ua_string` VARCHAR(255) DEFAULT NULL, - - `action` VARCHAR(40) DEFAULT NULL, - - `requester` JSON, - `body` JSON, - `extra` JSON, - - `has_parse_error` TINYINT(1) DEFAULT 0 - -); - -- 0017 CREATE TABLE `access_token_permissions` ( diff --git a/src/backend/clients/database/migrations/0002_add-default-apps.sql b/src/backend/clients/database/migrations/sqlite/0002_add-default-apps.sql similarity index 100% rename from src/backend/clients/database/migrations/0002_add-default-apps.sql rename to src/backend/clients/database/migrations/sqlite/0002_add-default-apps.sql diff --git a/src/backend/clients/database/migrations/0003_user-permissions.sql b/src/backend/clients/database/migrations/sqlite/0003_user-permissions.sql similarity index 100% rename from src/backend/clients/database/migrations/0003_user-permissions.sql rename to src/backend/clients/database/migrations/sqlite/0003_user-permissions.sql diff --git a/src/backend/clients/database/migrations/0004_sessions.sql b/src/backend/clients/database/migrations/sqlite/0004_sessions.sql similarity index 100% rename from src/backend/clients/database/migrations/0004_sessions.sql rename to src/backend/clients/database/migrations/sqlite/0004_sessions.sql diff --git a/src/backend/clients/database/migrations/0005_background-apps.sql b/src/backend/clients/database/migrations/sqlite/0005_background-apps.sql similarity index 100% rename from src/backend/clients/database/migrations/0005_background-apps.sql rename to src/backend/clients/database/migrations/sqlite/0005_background-apps.sql diff --git a/src/backend/clients/database/migrations/0006_update-apps.sql b/src/backend/clients/database/migrations/sqlite/0006_update-apps.sql similarity index 100% rename from src/backend/clients/database/migrations/0006_update-apps.sql rename to src/backend/clients/database/migrations/sqlite/0006_update-apps.sql diff --git a/src/backend/clients/database/migrations/0007_sessions.sql b/src/backend/clients/database/migrations/sqlite/0007_sessions.sql similarity index 100% rename from src/backend/clients/database/migrations/0007_sessions.sql rename to src/backend/clients/database/migrations/sqlite/0007_sessions.sql diff --git a/src/backend/clients/database/migrations/0008_otp.sql b/src/backend/clients/database/migrations/sqlite/0008_otp.sql similarity index 100% rename from src/backend/clients/database/migrations/0008_otp.sql rename to src/backend/clients/database/migrations/sqlite/0008_otp.sql diff --git a/src/backend/clients/database/migrations/0009_app-prefix-fix.sql b/src/backend/clients/database/migrations/sqlite/0009_app-prefix-fix.sql similarity index 100% rename from src/backend/clients/database/migrations/0009_app-prefix-fix.sql rename to src/backend/clients/database/migrations/sqlite/0009_app-prefix-fix.sql diff --git a/src/backend/clients/database/migrations/0010_add-git-app.sql b/src/backend/clients/database/migrations/sqlite/0010_add-git-app.sql similarity index 100% rename from src/backend/clients/database/migrations/0010_add-git-app.sql rename to src/backend/clients/database/migrations/sqlite/0010_add-git-app.sql diff --git a/src/backend/clients/database/migrations/0011_notification.sql b/src/backend/clients/database/migrations/sqlite/0011_notification.sql similarity index 100% rename from src/backend/clients/database/migrations/0011_notification.sql rename to src/backend/clients/database/migrations/sqlite/0011_notification.sql diff --git a/src/backend/clients/database/migrations/0012_appmetadata.sql b/src/backend/clients/database/migrations/sqlite/0012_appmetadata.sql similarity index 100% rename from src/backend/clients/database/migrations/0012_appmetadata.sql rename to src/backend/clients/database/migrations/sqlite/0012_appmetadata.sql diff --git a/src/backend/clients/database/migrations/0013_protected-apps.sql b/src/backend/clients/database/migrations/sqlite/0013_protected-apps.sql similarity index 100% rename from src/backend/clients/database/migrations/0013_protected-apps.sql rename to src/backend/clients/database/migrations/sqlite/0013_protected-apps.sql diff --git a/src/backend/clients/database/migrations/0014_share.sql b/src/backend/clients/database/migrations/sqlite/0014_share.sql similarity index 100% rename from src/backend/clients/database/migrations/0014_share.sql rename to src/backend/clients/database/migrations/sqlite/0014_share.sql diff --git a/src/backend/clients/database/migrations/0015_group.sql b/src/backend/clients/database/migrations/sqlite/0015_group.sql similarity index 100% rename from src/backend/clients/database/migrations/0015_group.sql rename to src/backend/clients/database/migrations/sqlite/0015_group.sql diff --git a/src/backend/clients/database/migrations/0016_group-permissions.sql b/src/backend/clients/database/migrations/sqlite/0016_group-permissions.sql similarity index 100% rename from src/backend/clients/database/migrations/0016_group-permissions.sql rename to src/backend/clients/database/migrations/sqlite/0016_group-permissions.sql diff --git a/src/backend/clients/database/migrations/0017_publicdirs.sql b/src/backend/clients/database/migrations/sqlite/0017_publicdirs.sql similarity index 100% rename from src/backend/clients/database/migrations/0017_publicdirs.sql rename to src/backend/clients/database/migrations/sqlite/0017_publicdirs.sql diff --git a/src/backend/clients/database/migrations/0018_fix-0003.sql b/src/backend/clients/database/migrations/sqlite/0018_fix-0003.sql similarity index 100% rename from src/backend/clients/database/migrations/0018_fix-0003.sql rename to src/backend/clients/database/migrations/sqlite/0018_fix-0003.sql diff --git a/src/backend/clients/database/migrations/0019_fix-0016.sql b/src/backend/clients/database/migrations/sqlite/0019_fix-0016.sql similarity index 100% rename from src/backend/clients/database/migrations/0019_fix-0016.sql rename to src/backend/clients/database/migrations/sqlite/0019_fix-0016.sql diff --git a/src/backend/clients/database/migrations/0020_dev-center.sql b/src/backend/clients/database/migrations/sqlite/0020_dev-center.sql similarity index 100% rename from src/backend/clients/database/migrations/0020_dev-center.sql rename to src/backend/clients/database/migrations/sqlite/0020_dev-center.sql diff --git a/src/backend/clients/database/migrations/0021_app-owner-id.sql b/src/backend/clients/database/migrations/sqlite/0021_app-owner-id.sql similarity index 100% rename from src/backend/clients/database/migrations/0021_app-owner-id.sql rename to src/backend/clients/database/migrations/sqlite/0021_app-owner-id.sql diff --git a/src/backend/clients/database/migrations/0022_dev-center-max.sql b/src/backend/clients/database/migrations/sqlite/0022_dev-center-max.sql similarity index 100% rename from src/backend/clients/database/migrations/0022_dev-center-max.sql rename to src/backend/clients/database/migrations/sqlite/0022_dev-center-max.sql diff --git a/src/backend/clients/database/migrations/0023_fix-kv.sql b/src/backend/clients/database/migrations/sqlite/0023_fix-kv.sql similarity index 100% rename from src/backend/clients/database/migrations/0023_fix-kv.sql rename to src/backend/clients/database/migrations/sqlite/0023_fix-kv.sql diff --git a/src/backend/clients/database/migrations/0024_default-groups.sql b/src/backend/clients/database/migrations/sqlite/0024_default-groups.sql similarity index 100% rename from src/backend/clients/database/migrations/0024_default-groups.sql rename to src/backend/clients/database/migrations/sqlite/0024_default-groups.sql diff --git a/src/backend/clients/database/migrations/0025_system-user.dbmig.js b/src/backend/clients/database/migrations/sqlite/0025_system-user.dbmig.js similarity index 100% rename from src/backend/clients/database/migrations/0025_system-user.dbmig.js rename to src/backend/clients/database/migrations/sqlite/0025_system-user.dbmig.js diff --git a/src/backend/clients/database/migrations/0026_user-groups.dbmig.js b/src/backend/clients/database/migrations/sqlite/0026_user-groups.dbmig.js similarity index 100% rename from src/backend/clients/database/migrations/0026_user-groups.dbmig.js rename to src/backend/clients/database/migrations/sqlite/0026_user-groups.dbmig.js diff --git a/src/backend/clients/database/migrations/0027_emulator-app.dbmig.js b/src/backend/clients/database/migrations/sqlite/0027_emulator-app.dbmig.js similarity index 100% rename from src/backend/clients/database/migrations/0027_emulator-app.dbmig.js rename to src/backend/clients/database/migrations/sqlite/0027_emulator-app.dbmig.js diff --git a/src/backend/clients/database/migrations/0028_clean-email.sql b/src/backend/clients/database/migrations/sqlite/0028_clean-email.sql similarity index 100% rename from src/backend/clients/database/migrations/0028_clean-email.sql rename to src/backend/clients/database/migrations/sqlite/0028_clean-email.sql diff --git a/src/backend/clients/database/migrations/0029_emulator_priv.sql b/src/backend/clients/database/migrations/sqlite/0029_emulator_priv.sql similarity index 100% rename from src/backend/clients/database/migrations/0029_emulator_priv.sql rename to src/backend/clients/database/migrations/sqlite/0029_emulator_priv.sql diff --git a/src/backend/clients/database/migrations/0030_comments.sql b/src/backend/clients/database/migrations/sqlite/0030_comments.sql similarity index 100% rename from src/backend/clients/database/migrations/0030_comments.sql rename to src/backend/clients/database/migrations/sqlite/0030_comments.sql diff --git a/src/backend/clients/database/migrations/0031_audit-meta.sql b/src/backend/clients/database/migrations/sqlite/0031_audit-meta.sql similarity index 100% rename from src/backend/clients/database/migrations/0031_audit-meta.sql rename to src/backend/clients/database/migrations/sqlite/0031_audit-meta.sql diff --git a/src/backend/clients/database/migrations/0032_signup_metadata.sql b/src/backend/clients/database/migrations/sqlite/0032_signup_metadata.sql similarity index 100% rename from src/backend/clients/database/migrations/0032_signup_metadata.sql rename to src/backend/clients/database/migrations/sqlite/0032_signup_metadata.sql diff --git a/src/backend/clients/database/migrations/0033_ai-usage.sql b/src/backend/clients/database/migrations/sqlite/0033_ai-usage.sql similarity index 100% rename from src/backend/clients/database/migrations/0033_ai-usage.sql rename to src/backend/clients/database/migrations/sqlite/0033_ai-usage.sql diff --git a/src/backend/clients/database/migrations/0034_app-redirect.sql b/src/backend/clients/database/migrations/sqlite/0034_app-redirect.sql similarity index 100% rename from src/backend/clients/database/migrations/0034_app-redirect.sql rename to src/backend/clients/database/migrations/sqlite/0034_app-redirect.sql diff --git a/src/backend/clients/database/migrations/0035_threads.sql b/src/backend/clients/database/migrations/sqlite/0035_threads.sql similarity index 100% rename from src/backend/clients/database/migrations/0035_threads.sql rename to src/backend/clients/database/migrations/sqlite/0035_threads.sql diff --git a/src/backend/clients/database/migrations/0036_dev-to-app.sql b/src/backend/clients/database/migrations/sqlite/0036_dev-to-app.sql similarity index 100% rename from src/backend/clients/database/migrations/0036_dev-to-app.sql rename to src/backend/clients/database/migrations/sqlite/0036_dev-to-app.sql diff --git a/src/backend/clients/database/migrations/0037_cost.sql b/src/backend/clients/database/migrations/sqlite/0037_cost.sql similarity index 100% rename from src/backend/clients/database/migrations/0037_cost.sql rename to src/backend/clients/database/migrations/sqlite/0037_cost.sql diff --git a/src/backend/clients/database/migrations/0038_custom-domains.sql b/src/backend/clients/database/migrations/sqlite/0038_custom-domains.sql similarity index 100% rename from src/backend/clients/database/migrations/0038_custom-domains.sql rename to src/backend/clients/database/migrations/sqlite/0038_custom-domains.sql diff --git a/src/backend/clients/database/migrations/0039_add-expireAt-to-kv-store.sql b/src/backend/clients/database/migrations/sqlite/0039_add-expireAt-to-kv-store.sql similarity index 100% rename from src/backend/clients/database/migrations/0039_add-expireAt-to-kv-store.sql rename to src/backend/clients/database/migrations/sqlite/0039_add-expireAt-to-kv-store.sql diff --git a/src/backend/clients/database/migrations/0040_add_user_metadata.sql b/src/backend/clients/database/migrations/sqlite/0040_add_user_metadata.sql similarity index 100% rename from src/backend/clients/database/migrations/0040_add_user_metadata.sql rename to src/backend/clients/database/migrations/sqlite/0040_add_user_metadata.sql diff --git a/src/backend/clients/database/migrations/0041_add_unique_constraint_user_uuid.sql b/src/backend/clients/database/migrations/sqlite/0041_add_unique_constraint_user_uuid.sql similarity index 100% rename from src/backend/clients/database/migrations/0041_add_unique_constraint_user_uuid.sql rename to src/backend/clients/database/migrations/sqlite/0041_add_unique_constraint_user_uuid.sql diff --git a/src/backend/clients/database/migrations/0042_add_cloudflare_d1.sql b/src/backend/clients/database/migrations/sqlite/0042_add_cloudflare_d1.sql similarity index 100% rename from src/backend/clients/database/migrations/0042_add_cloudflare_d1.sql rename to src/backend/clients/database/migrations/sqlite/0042_add_cloudflare_d1.sql diff --git a/src/backend/clients/database/migrations/0043_add_dt.sql b/src/backend/clients/database/migrations/sqlite/0043_add_dt.sql similarity index 100% rename from src/backend/clients/database/migrations/0043_add_dt.sql rename to src/backend/clients/database/migrations/sqlite/0043_add_dt.sql diff --git a/src/backend/clients/database/migrations/0044_dev-center-godmode.sql b/src/backend/clients/database/migrations/sqlite/0044_dev-center-godmode.sql similarity index 100% rename from src/backend/clients/database/migrations/0044_dev-center-godmode.sql rename to src/backend/clients/database/migrations/sqlite/0044_dev-center-godmode.sql diff --git a/src/backend/clients/database/migrations/0045_user_oidc_providers.sql b/src/backend/clients/database/migrations/sqlite/0045_user_oidc_providers.sql similarity index 100% rename from src/backend/clients/database/migrations/0045_user_oidc_providers.sql rename to src/backend/clients/database/migrations/sqlite/0045_user_oidc_providers.sql diff --git a/src/backend/clients/database/migrations/0046_is-private-apps.sql b/src/backend/clients/database/migrations/sqlite/0046_is-private-apps.sql similarity index 100% rename from src/backend/clients/database/migrations/0046_is-private-apps.sql rename to src/backend/clients/database/migrations/sqlite/0046_is-private-apps.sql diff --git a/src/backend/clients/database/splitMysqlStatements.test.ts b/src/backend/clients/database/splitMysqlStatements.test.ts new file mode 100644 index 000000000..ced190083 --- /dev/null +++ b/src/backend/clients/database/splitMysqlStatements.test.ts @@ -0,0 +1,137 @@ +/** + * Copyright (C) 2024-present Puter Technologies Inc. + * + * This file is part of Puter. + * + * Puter is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +import { describe, expect, it } from 'vitest'; +import { splitMysqlStatements } from './splitMysqlStatements.js'; + +describe('splitMysqlStatements', () => { + it('splits simple statements on default delimiter', () => { + expect(splitMysqlStatements('SELECT 1; SELECT 2;')).toEqual([ + 'SELECT 1', + 'SELECT 2', + ]); + }); + + it('returns empty array for whitespace-only input', () => { + expect(splitMysqlStatements(' \n\t ')).toEqual([]); + }); + + it('keeps a trailing statement without terminating semicolon', () => { + expect(splitMysqlStatements('SELECT 1;\nSELECT 2')).toEqual([ + 'SELECT 1', + 'SELECT 2', + ]); + }); + + it('ignores semicolons inside single-quoted strings', () => { + expect( + splitMysqlStatements("INSERT INTO t VALUES ('a;b'); SELECT 2;"), + ).toEqual(["INSERT INTO t VALUES ('a;b')", 'SELECT 2']); + }); + + it("handles SQL '' escape inside single-quoted strings", () => { + expect( + splitMysqlStatements("SELECT 'it''s; ok'; SELECT 2;"), + ).toEqual(["SELECT 'it''s; ok'", 'SELECT 2']); + }); + + it('handles backslash escape inside strings', () => { + expect( + splitMysqlStatements("SELECT 'a\\'b;c'; SELECT 2;"), + ).toEqual(["SELECT 'a\\'b;c'", 'SELECT 2']); + }); + + it('ignores semicolons inside backtick identifiers', () => { + expect( + splitMysqlStatements('SELECT `weird;col` FROM t; SELECT 2;'), + ).toEqual(['SELECT `weird;col` FROM t', 'SELECT 2']); + }); + + it('ignores semicolons inside double-quoted strings', () => { + expect(splitMysqlStatements('SELECT "a;b"; SELECT 2;')).toEqual([ + 'SELECT "a;b"', + 'SELECT 2', + ]); + }); + + it('ignores semicolons in line comments', () => { + expect( + splitMysqlStatements( + 'SELECT 1; -- a;b\nSELECT 2; # c;d\nSELECT 3;', + ), + ).toEqual(['SELECT 1', '-- a;b\nSELECT 2', '# c;d\nSELECT 3']); + }); + + it('ignores semicolons in block comments (multi-line)', () => { + expect( + splitMysqlStatements('SELECT 1 /* a;\nb;c */; SELECT 2;'), + ).toEqual(['SELECT 1 /* a;\nb;c */', 'SELECT 2']); + }); + + it('honours DELIMITER directive', () => { + const sql = ` +SELECT 1; +DELIMITER // +CREATE PROCEDURE p() BEGIN SELECT 1; SELECT 2; END// +DELIMITER ; +SELECT 3; +`; + expect(splitMysqlStatements(sql)).toEqual([ + 'SELECT 1', + 'CREATE PROCEDURE p() BEGIN SELECT 1; SELECT 2; END', + 'SELECT 3', + ]); + }); + + it('handles a stored procedure that uses // delimiter end-to-end', () => { + const sql = `DROP PROCEDURE IF EXISTS foo; +DELIMITER // +CREATE PROCEDURE foo(IN x INT) +BEGIN + IF x > 0 THEN + SET @s := 'hi;'; + SELECT @s; + END IF; +END// +DELIMITER ; +DROP PROCEDURE IF EXISTS foo; +`; + const stmts = splitMysqlStatements(sql); + expect(stmts).toHaveLength(3); + expect(stmts[0]).toBe('DROP PROCEDURE IF EXISTS foo'); + expect(stmts[1]).toContain('CREATE PROCEDURE foo'); + expect(stmts[1]).toContain("SET @s := 'hi;';"); + expect(stmts[2]).toBe('DROP PROCEDURE IF EXISTS foo'); + }); + + it('strips DELIMITER lines from output even if no statement follows', () => { + expect(splitMysqlStatements('DELIMITER //\nDELIMITER ;\n')).toEqual( + [], + ); + }); + + it('does not treat -- without trailing whitespace as a comment', () => { + // `--5` is "minus minus 5" (rare in practice but valid SQL). + // MySQL requires whitespace after `--` for it to be a comment. + expect(splitMysqlStatements('SELECT 1--5; SELECT 2;')).toEqual([ + 'SELECT 1--5', + 'SELECT 2', + ]); + }); +}); diff --git a/src/backend/clients/database/splitMysqlStatements.ts b/src/backend/clients/database/splitMysqlStatements.ts new file mode 100644 index 000000000..6fecf48c1 --- /dev/null +++ b/src/backend/clients/database/splitMysqlStatements.ts @@ -0,0 +1,211 @@ +/** + * Copyright (C) 2024-present Puter Technologies Inc. + * + * This file is part of Puter. + * + * Puter is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +/** + * DELIMITER-aware splitter for MySQL dump / migration files. + * + * Splits `sql` into individual statements using the active statement + * delimiter (default `;`). Recognises `DELIMITER X` lines, single-quoted + * strings, double-quoted strings, backtick-quoted identifiers, line + * comments (`-- `, `#`) and block comments (`/* ... *\/`). DELIMITER + * directives are stripped from the output (they're a client-side concept, + * not server SQL). + * + * Returns trimmed, non-empty statements without the trailing delimiter. + */ +export function splitMysqlStatements(sql: string): string[] { + const out: string[] = []; + let buf = ''; + let delim = ';'; + let i = 0; + const n = sql.length; + + // We process the input line-by-line for DELIMITER detection, but track + // multi-line state (strings / block comments) across lines. + type State = + | 'normal' + | 'sq' // single-quoted string + | 'dq' // double-quoted string + | 'bt' // backtick-quoted identifier + | 'block'; // /* ... */ + let state: State = 'normal'; + + const pushStatement = () => { + const trimmed = buf.trim(); + if (trimmed.length > 0) out.push(trimmed); + buf = ''; + }; + + while (i < n) { + // At the start of each line in `normal` state, check for DELIMITER + // and full-line comments. We're at line start iff `i === 0` or the + // previous char was a newline. + const atLineStart = i === 0 || sql[i - 1] === '\n'; + if (atLineStart && state === 'normal') { + // Find end of current line (without consuming). + let lineEnd = sql.indexOf('\n', i); + if (lineEnd === -1) lineEnd = n; + const line = sql.slice(i, lineEnd); + + // DELIMITER directive — only valid when the current statement + // buffer is empty (i.e. between statements). MySQL CLI accepts + // it almost anywhere, but in practice it's always between + // statements; rejecting mid-statement keeps the parser simple + // and predictable. + const delimMatch = /^\s*DELIMITER\s+(\S+)\s*$/i.exec(line); + if (delimMatch && buf.trim() === '') { + delim = delimMatch[1]; + // skip the line including the trailing newline (if any) + i = lineEnd + 1; + buf = ''; + continue; + } + } + + const c = sql[i]; + const next = i + 1 < n ? sql[i + 1] : ''; + + if (state === 'sq') { + buf += c; + if (c === '\\' && i + 1 < n) { + buf += sql[i + 1]; + i += 2; + continue; + } + if (c === "'") { + if (next === "'") { + // SQL-style escaped quote + buf += "'"; + i += 2; + continue; + } + state = 'normal'; + } + i++; + continue; + } + + if (state === 'dq') { + buf += c; + if (c === '\\' && i + 1 < n) { + buf += sql[i + 1]; + i += 2; + continue; + } + if (c === '"') { + if (next === '"') { + buf += '"'; + i += 2; + continue; + } + state = 'normal'; + } + i++; + continue; + } + + if (state === 'bt') { + buf += c; + if (c === '`') { + if (next === '`') { + buf += '`'; + i += 2; + continue; + } + state = 'normal'; + } + i++; + continue; + } + + if (state === 'block') { + buf += c; + if (c === '*' && next === '/') { + buf += '/'; + i += 2; + state = 'normal'; + continue; + } + i++; + continue; + } + + // state === 'normal' + // Line comments: `-- ` or `--\n` or `--$` (MySQL requires whitespace + // or EOL after `--`); also `#` to EOL. + if ( + (c === '-' && + next === '-' && + (sql[i + 2] === undefined || /\s/.test(sql[i + 2]))) || + c === '#' + ) { + // consume to end-of-line; keep the comment in the buffer so the + // statement text remains faithful (mysql server tolerates it) + const lineEnd = sql.indexOf('\n', i); + const end = lineEnd === -1 ? n : lineEnd; + buf += sql.slice(i, end); + i = end; + continue; + } + + if (c === '/' && next === '*') { + buf += '/*'; + i += 2; + state = 'block'; + continue; + } + + if (c === "'") { + buf += c; + i++; + state = 'sq'; + continue; + } + + if (c === '"') { + buf += c; + i++; + state = 'dq'; + continue; + } + + if (c === '`') { + buf += c; + i++; + state = 'bt'; + continue; + } + + // Delimiter match + if (sql.startsWith(delim, i)) { + // emit current buffer (without the delimiter) + pushStatement(); + i += delim.length; + continue; + } + + buf += c; + i++; + } + + // Flush trailing content (no terminating delimiter is allowed for the + // last statement, but we still try) + pushStatement(); + return out; +} diff --git a/src/backend/clients/dynamodb/DDBClient.ts b/src/backend/clients/dynamodb/DDBClient.ts index 76bdd6b6d..68f9935db 100644 --- a/src/backend/clients/dynamodb/DDBClient.ts +++ b/src/backend/clients/dynamodb/DDBClient.ts @@ -401,9 +401,12 @@ export class DDBClient extends PuterClient { params: CreateTableCommandInput, ttlAttribute?: string, ) { - if (this.#ddbConfig.aws) { + // Real-AWS deployments provision tables externally (Terraform / IaC), + // so we no-op there by default. Self-hosters pointing at + // dynamodb-local opt in via `dynamo.bootstrapTables: true`. + if (this.#ddbConfig.aws && !this.#ddbConfig.bootstrapTables) { console.warn( - 'Creating DynamoDB tables in AWS is disabled by default, but if needed, update DDBClient', + 'Creating DynamoDB tables is disabled by default; set `dynamo.bootstrapTables: true` in config to enable (intended for local emulators).', ); return; } diff --git a/src/backend/clients/redis/RedisClient.ts b/src/backend/clients/redis/RedisClient.ts index e40977fe2..a7fe5b62b 100644 --- a/src/backend/clients/redis/RedisClient.ts +++ b/src/backend/clients/redis/RedisClient.ts @@ -78,6 +78,11 @@ const buildCluster = (config: IConfig): Cluster => { ]) as unknown as Cluster; } + // TLS defaults on (matches the existing prod ElastiCache behavior). + // Self-hosters running cluster mode against a plain-TCP Valkey set + // `redis.tls: false` to opt out. + const tlsEnabled = redisConfig.tls !== false; + const cluster = new Redis.Cluster( startupNodes as ConstructorParameters[0], { @@ -90,7 +95,7 @@ const buildCluster = (config: IConfig): Cluster => { slotsRefreshTimeout: redisSlotsRefreshTimeoutMs, enableOfflineQueue: true, redisOptions: { - tls: {}, + ...(tlsEnabled ? { tls: {} } : {}), connectTimeout: redisConnectTimeoutMs, maxRetriesPerRequest: 1, }, diff --git a/src/backend/drivers/apps/AppDriver.js b/src/backend/drivers/apps/AppDriver.js index 1e7183686..1bf376ffb 100644 --- a/src/backend/drivers/apps/AppDriver.js +++ b/src/backend/drivers/apps/AppDriver.js @@ -869,6 +869,50 @@ export class AppDriver extends PuterDriver { return !!this.#extractPuterHostedSubdomain(indexUrl); } + /** + * Read normalized origin-alias groups from config. Each group is a deduped + * list of lowercased, trimmed bare hosts. Malformed entries are skipped so + * a bad config row doesn't brick app create/update for everyone else. + */ + #getOriginAliasGroups() { + const config = this.config ?? {}; + const raw = config.app_origin_aliases; + if (!Array.isArray(raw)) return []; + + const groups = []; + for (const group of raw) { + if (!Array.isArray(group)) continue; + const normalized = [ + ...new Set( + group + .filter((h) => typeof h === 'string') + .map((h) => h.trim().toLowerCase()) + .filter((h) => h.length > 0), + ), + ]; + if (normalized.length > 0) groups.push(normalized); + } + return groups; + } + + /** + * Return the alias group containing this index_url's host, or null when + * the host isn't claimed by any group. + */ + #findOriginAliasGroupForIndexUrl(indexUrl) { + if (typeof indexUrl !== 'string' || !indexUrl) return null; + let hostname; + try { + hostname = new URL(indexUrl).hostname.toLowerCase(); + } catch { + return null; + } + for (const group of this.#getOriginAliasGroups()) { + if (group.includes(hostname)) return group; + } + return null; + } + /** * Generate the set of equivalent index_url strings that should * collide with a given input. We only collapse trailing-slash and @@ -906,13 +950,32 @@ export class AppDriver extends PuterDriver { } async #findIndexUrlConflictRow({ indexUrl, excludeAppId } = {}) { - if (!this.#isPuterHostedIndexUrl(indexUrl)) return null; + const aliasGroup = this.#findOriginAliasGroupForIndexUrl(indexUrl); + if (!this.#isPuterHostedIndexUrl(indexUrl) && !aliasGroup) return null; - const candidates = this.#buildEquivalentIndexUrlCandidates(indexUrl); - if (candidates.length === 0) return null; - if (hasIndexUrlUniquenessExemption(candidates)) return null; + const candidates = new Set( + this.#buildEquivalentIndexUrlCandidates(indexUrl), + ); - return this.appStore.findByIndexUrlCandidates(candidates, { + // For alias-group hosts, treat the group as a host-level reservation: + // any row whose index_url is the root URL of any group member counts + // as a conflict, so a single app owns the whole group. + if (aliasGroup) { + for (const host of aliasGroup) { + for (const proto of ['https', 'http']) { + const base = `${proto}://${host}`; + candidates.add(base); + candidates.add(`${base}/`); + candidates.add(`${base}/index.html`); + } + } + } + + if (candidates.size === 0) return null; + const candidateList = [...candidates]; + if (hasIndexUrlUniquenessExemption(candidateList)) return null; + + return this.appStore.findByIndexUrlCandidates(candidateList, { excludeAppId, }); } diff --git a/src/backend/services/auth/AuthService.ts b/src/backend/services/auth/AuthService.ts index d54c3a87a..55fad8446 100644 --- a/src/backend/services/auth/AuthService.ts +++ b/src/backend/services/auth/AuthService.ts @@ -230,7 +230,11 @@ export class AuthService extends PuterService { legacyCode: 'no_origin_for_app', }); } - const event = { origin: parsed }; + // Aliased hosts collapse to a single canonical representative so the + // event listeners and the UUIDv5 fallback resolve to the same value + // for every member of an alias group. + const aliased = this.#canonicalizeAliasedOrigin(parsed) ?? parsed; + const event = { origin: aliased }; await this.clients.event?.emitAndWait('app.from-origin', event, {}); const canonicalUid = await this.#findCanonicalAppUidForOrigin( @@ -242,6 +246,74 @@ export class AuthService extends PuterService { return `app-${uid}`; } + /** + * Read `app_origin_aliases` from config and return normalized groups — + * each group is a deduped list of lowercased, trimmed host strings. + * Malformed entries are skipped silently so a bad config row doesn't + * brick UID resolution for everyone else. + */ + #getOriginAliasGroups(): string[][] { + const config = this.config as { app_origin_aliases?: unknown }; + const raw = config.app_origin_aliases; + if (!Array.isArray(raw)) return []; + + const groups: string[][] = []; + for (const group of raw) { + if (!Array.isArray(group)) continue; + const normalized = [ + ...new Set( + group + .filter((h): h is string => typeof h === 'string') + .map((h) => h.trim().toLowerCase()) + .filter((h) => h.length > 0), + ), + ]; + if (normalized.length > 0) groups.push(normalized); + } + return groups; + } + + /** + * Find the alias group containing `host` (case-insensitive). Returns the + * normalized group, or null when no group claims this host. + */ + #findOriginAliasGroup(host: string): string[] | null { + const lower = host.trim().toLowerCase(); + if (!lower) return null; + for (const group of this.#getOriginAliasGroups()) { + if (group.includes(lower)) return group; + } + return null; + } + + /** + * If the origin's host belongs to an alias group, swap it for the group's + * canonical representative (alphabetically first member — chosen for + * order-independence so config reordering doesn't shift UUIDs). Returns + * null when the host isn't in any group, so the caller keeps the original. + */ + #canonicalizeAliasedOrigin(origin: string): string | null { + let parsed: URL; + try { + parsed = new URL(origin); + } catch { + return null; + } + const hostRaw = parsed.host.toLowerCase(); + const hostStripped = parsed.hostname.toLowerCase(); + const group = + this.#findOriginAliasGroup(hostRaw) ?? + this.#findOriginAliasGroup(hostStripped); + if (!group) return null; + + const canonical = [...group].sort()[0]; + if (!canonical || canonical === hostRaw || canonical === hostStripped) { + return null; + } + parsed.host = canonical; + return parsed.toString(); + } + /** * Find the real app row whose `index_url` canonically matches `origin`. * @@ -323,6 +395,16 @@ export class AuthService extends PuterService { hostCandidates.add(`${subdomain}.${d}`); } } + // Origin alias group expansion: every host listed alongside the + // request's host in `app_origin_aliases` becomes a lookup candidate, + // so any one of the group's hosts being registered as an `index_url` + // resolves the whole group to that row's UID. + const aliasGroup = + this.#findOriginAliasGroup(hostRaw) ?? + this.#findOriginAliasGroup(hostStripped); + if (aliasGroup) { + for (const h of aliasGroup) hostCandidates.add(h); + } const protocolCandidates = new Set([ parsed.protocol.replace(/:$/, ''), diff --git a/src/backend/stores/systemKv/SystemKVStore.ts b/src/backend/stores/systemKv/SystemKVStore.ts index 7a7e3f14e..539f9e020 100644 --- a/src/backend/stores/systemKv/SystemKVStore.ts +++ b/src/backend/stores/systemKv/SystemKVStore.ts @@ -203,9 +203,12 @@ export class SystemKVStore extends PuterStore { override async onServerStart(): Promise { // For local/dynalite runs we need to create the table up front. - // For real AWS we assume the table already exists. + // Real AWS deployments provision tables externally (Terraform), so + // we skip — unless the operator explicitly opts in via + // `dynamo.bootstrapTables` (e.g. self-hosting against + // dynamodb-local in docker-compose). const ddbConfig = this.config.dynamo ?? {}; - if (ddbConfig.aws) return; + if (ddbConfig.aws && !ddbConfig.bootstrapTables) return; this.initialized = this.clients.dynamo.createTableIfNotExists( { ...PUTER_KV_STORE_TABLE_DEFINITION, TableName: this.tableName }, diff --git a/src/backend/types.ts b/src/backend/types.ts index a7b1af1db..53464f155 100644 --- a/src/backend/types.ts +++ b/src/backend/types.ts @@ -29,6 +29,13 @@ export interface IDynamoConfig { aws?: IAWSCredentials; endpoint?: string; path?: string; + /** + * Create required tables on startup if they don't exist. Off by + * default because real-AWS deployments provision tables externally + * (Terraform / IaC). Set to `true` when pointing at a local + * DynamoDB emulator so self-hosters don't have to bootstrap by hand. + */ + bootstrapTables?: boolean; } export interface IRedisConfig { @@ -36,6 +43,11 @@ export interface IRedisConfig { host: string; port: number; }>; + /** + * Use TLS for cluster connections. Defaults to `true` (matches prod + * ElastiCache). Set `false` for self-host plain-TCP Valkey/Redis. + */ + tls?: boolean; useMock?: boolean; } @@ -265,6 +277,14 @@ export interface IDatabaseConfig { password?: string; database?: string; }; + /** + * Ordered list of directories whose `.sql` files are run sequentially at + * server start (mysql engine only). Files within a directory are sorted + * lexically; directories are processed in array order. Files MUST be + * idempotent — there is no per-file applied-state tracking. + * Relative paths resolve from `process.cwd()`. + */ + migrationPaths?: string[]; } /** @@ -349,6 +369,23 @@ interface IConfigOptional { private_app_hosting_domain: string; /** Alt private app hosting domain. */ private_app_hosting_domain_alt: string; + /** + * Groups of equivalent app index_url hosts. Each group lists hosts that + * should resolve to the same canonical app: `appUidFromOrigin` looks up + * any DB row whose `index_url` is one of the group's hosts and returns + * that row's UID for every host in the group. + * + * Hosts listed here are also reserved — `apps.create` / `apps.update` + * reject any attempt to register a different app under one of these + * hosts, so the group is owned by exactly one app row. + * + * Entries are bare hosts (no scheme), lowercased. Example: + * [ + * ["camera.puter.com", "camera.puter.site", "camera.ca"], + * ["player.puter.com", "player.puter.site"], + * ] + */ + app_origin_aliases?: string[][]; /** When true, accept any Host header value. Dev/testing only. */ allow_all_host_values: boolean; /** When true, accept requests without a Host header. */ diff --git a/tools/extensionSetup.mjs b/tools/extensionSetup.mjs new file mode 100755 index 000000000..9ea1fcc03 --- /dev/null +++ b/tools/extensionSetup.mjs @@ -0,0 +1,59 @@ +#!/usr/bin/env node +// Install dependencies for every subfolder under ./extensions/. +// Runs installs in parallel; uses `npm ci` when a lockfile is present, +// otherwise falls back to `npm install`. Cross-platform replacement for +// the previous bash version. + +import { existsSync, readdirSync, statSync } from 'node:fs'; +import { spawn } from 'node:child_process'; +import { join } from 'node:path'; + +const EXT_DIR = './extensions'; + +if (!existsSync(EXT_DIR)) { + process.exit(0); +} + +const dirs = readdirSync(EXT_DIR) + .map((name) => join(EXT_DIR, name)) + .filter((p) => statSync(p).isDirectory()) + .filter((p) => existsSync(join(p, 'package.json'))); + +if (dirs.length === 0) { + process.exit(0); +} + +const npmCmd = process.platform === 'win32' ? 'npm.cmd' : 'npm'; + +function install(dir) { + return new Promise((resolve, reject) => { + const args = existsSync(join(dir, 'package-lock.json')) ? ['ci'] : ['install']; + console.log(`[${dir}] starting npm ${args.join(' ')}`); + const child = spawn(npmCmd, args, { cwd: dir }); + let out = ''; + child.stdout.on('data', (d) => (out += d)); + child.stderr.on('data', (d) => (out += d)); + child.on('error', reject); + child.on('close', (code) => { + if (out) process.stdout.write(out); + if (code === 0) { + console.log(`[${dir}] done`); + resolve(); + } else { + reject(new Error(`[${dir}] npm ${args.join(' ')} exited with code ${code}`)); + } + }); + }); +} + +const results = await Promise.allSettled(dirs.map(install)); +const failures = results + .map((r, i) => ({ r, dir: dirs[i] })) + .filter(({ r }) => r.status === 'rejected'); + +if (failures.length > 0) { + for (const { r, dir } of failures) { + console.error(`[${dir}] ${r.reason?.message ?? r.reason}`); + } + process.exit(1); +} diff --git a/tools/extensionSetup.sh b/tools/extensionSetup.sh deleted file mode 100755 index 97ec321c5..000000000 --- a/tools/extensionSetup.sh +++ /dev/null @@ -1,8 +0,0 @@ -#~!/bin/bash -# iterate through each folder in extensions/ if they contain a package.json, run npm install -for d in ./extensions/*/ ; do - if [ -f "$d/package.json" ]; then - echo "Installing dependencies for $d" - (cd "$d" && npm install) - fi -done \ No newline at end of file