Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
c612b7e
refactor(backend): pilot arktype validation for private rbac routes
riderx Apr 9, 2026
d5490b0
fix(ci): polyfill Object.groupBy for eslint config
riderx Apr 9, 2026
023453b
Merge branch 'main' into codex/arktype-rbac-pilot
riderx Apr 10, 2026
79a091d
fix(rbac): preserve validation contracts
riderx Apr 10, 2026
c355dd9
fix(rbac): preserve headerless json validation
riderx Apr 10, 2026
120ace7
fix(rbac): preserve invalid json responses
riderx Apr 10, 2026
c3add38
fix(tests): isolate organization apikey setup auth
riderx Apr 10, 2026
aaa196b
fix(rbac): preserve parse error codes
riderx Apr 10, 2026
6f2f052
fix(tests): advance seeded apikey sequence
riderx Apr 10, 2026
8b031ff
fix(tests): isolate audit log apikey coverage
riderx Apr 10, 2026
b1759fb
fix(rbac): check group permissions before validation
riderx Apr 10, 2026
f954d94
fix(rbac): address remaining review feedback
riderx Apr 10, 2026
337b3d6
refactor(validation): remove zod from backend schemas
riderx Apr 11, 2026
74f6e08
fix(stats): avoid arktype range union ambiguity
riderx Apr 11, 2026
3a8f679
fix(validation): reject empty plugin versions
riderx Apr 11, 2026
bc2f981
test(organization): assert stable org fields
riderx Apr 11, 2026
c5e508f
merge: resolve main into codex/arktype-rbac-pilot
riderx Apr 14, 2026
ef309db
fix: restore merge dependency manifests
riderx Apr 14, 2026
c5419da
test: stabilize plan helper auth assertion
riderx Apr 14, 2026
5ed4dbf
fix: guard optional otp type validation
riderx Apr 14, 2026
5814974
fix(db): avoid org user privilege trigger recursion
riderx Apr 14, 2026
e3c10f4
fix(db): restore exact super admin trigger checks
riderx Apr 14, 2026
45fb9e8
test(db): assert admin function execute grants
riderx Apr 14, 2026
fd549b8
test(db): avoid rescind invitation execute crash
riderx Apr 14, 2026
fc3f0e5
test(db): avoid deletion cron permission crash
riderx Apr 14, 2026
c14d4d8
test(db): harden rpc execute grant checks
riderx Apr 14, 2026
9d4d7c9
test(db): avoid 2fa rpc execute crash
riderx Apr 14, 2026
6bc27b8
fix(db): mark billing org rpcs volatile
riderx Apr 14, 2026
074496a
fix(db): clear remaining sql lint warnings
riderx Apr 14, 2026
7800216
fix(db): use positional args in lint cleanup
riderx Apr 14, 2026
da553be
fix(db): recreate app right helpers for lint cleanup
riderx Apr 14, 2026
b1ec62f
fix(db): suppress legacy helper lint warnings
riderx Apr 14, 2026
ffbf447
fix(db): preserve helper signatures for lint pragmas
riderx Apr 14, 2026
bc9a271
fix(db): qualify lint pragma helpers
riderx Apr 14, 2026
97d7783
fix(db): rename legacy app right params
riderx Apr 14, 2026
7342740
fix(db): preserve legacy helper signatures
riderx Apr 14, 2026
aafcfb6
Merge branch 'main' into codex/arktype-rbac-pilot
riderx Apr 16, 2026
d4a961d
test(i18n): fix fallback assertion
riderx Apr 16, 2026
bbc770b
fix(db): preserve legacy org user privilege guards
riderx Apr 16, 2026
a63f0a7
test(cloudflare): stabilize subkey cleanup
riderx Apr 16, 2026
eeb2bff
fix(api): restore webhook jwt auth
riderx Apr 16, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1,544 changes: 1,430 additions & 114 deletions bun.lock

Large diffs are not rendered by default.

46 changes: 34 additions & 12 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -173,38 +173,59 @@
"@capgo/keep-awake": "^8.1.1",
"@capgo/native-audio": "^8.3.11",
"@capgo/native-market": "^8.0.22",
"@capgo/tailwind-capacitor": "^4.0.0",
"@formkit/auto-animate": "0.9.0",
"@formkit/i18n": "^2.0.0",
"@formkit/icons": "^2.0.0",
"@formkit/themes": "2.0.0",
"@formkit/vue": "2.0.0",
"@hono/standard-validator": "^0.2.2",
"@iconify-json/simple-icons": "^1.2.77",
"@intlify/unplugin-vue-i18n": "^11.0.7",
"@logsnag/node": "1.0.1",
"@revenuecat/purchases-capacitor": "12.3.2",
"@std/semver": "npm:@jsr/std__semver@1.0.8",
"@supabase/supabase-js": "2.103.0",
"@tailwindcss/forms": "^0.5.11",
"@types/semver": "^7.7.1",
"@vuepic/vue-datepicker": "^12.1.0",
"@vueuse/components": "^14.2.1",
"@vueuse/core": "14.2.1",
"arktype": "^2.2.0",
"base64-arraybuffer": "1.0.2",
"better-qr": "^0.1.1",
"bun-types": "^1.3.11",
"chart.js": "^4.5.1",
"chartjs-chart-funnel": "^4.2.5",
"country-code-to-flag-emoji": "^2.1.0",
"cron-schedule": "^6.0.0",
"crypto-random-string": "^5.0.0",
"daisyui": "^5.5.19",
"dayjs": "1.11.20",
"dompurify": "^3.3.3",
"drizzle-orm": "^0.45.2",
"dotenv": "^17.4.1",
"drizzle-orm": "1.0.0-beta.21",
"firebase": "12.11.0",
"hono": "4.12.12",
"jose": "^6.2.2",
"konsta": "^5.0.8",
"mime": "^4.1.0",
"pg": "^8.20.0",
"pinia": "3.0.4",
"plausible-tracker": "^0.3.9",
"qrcode": "^1.5.4",
"semver": "^7.7.4",
"stripe": "^22.0.1",
"supabase": "^2.88.1",
"turndown": "^7.2.4",
"vite-plugin-devtools-json": "^1.0.0",
"vue": "3.5.32",
"vue-chartjs": "^5.3.3",
"vue-demi": "0.14.10",
"vue-i18n": "^11.3.2",
"vue-router": "^5.0.4",
"vue-sonner": "^2.0.9",
"vue-turnstile": "^1.0.11",
"zod": "^4.3.6"
"vue-turnstile": "^1.0.11"
},
"devDependencies": {
"@antfu/eslint-config": "8.2.0",
Expand All @@ -215,34 +236,35 @@
"@cloudflare/workers-types": "4.20260409.1",
"@codspeed/vitest-plugin": "^5.2.0",
"@formkit/core": "2.0.0",
"@iconify-json/simple-icons": "^1.2.77",
"@hono/node-server": "^1.19.13",
"@iconify/json": "^2.2.460",
"@intlify/unplugin-vue-i18n": "^11.0.7",
"@playwright/test": "1.59.1",
"@standard-schema/spec": "^1.1.0",
"@tailwindcss/aspect-ratio": "^0.4.2",
"@tailwindcss/container-queries": "^0.1.1",
"@tailwindcss/typography": "^0.5.19",
"@tailwindcss/vite": "^4.2.2",
"@types/adm-zip": "^0.5.8",
"@types/bun": "^1.3.11",
"@types/dompurify": "3.2.0",
"@types/pg": "^8.20.0",
"@types/semver": "^7.7.1",
"@types/qrcode": "^1.5.6",
"@types/turndown": "^5.0.6",
"@vitejs/plugin-vue": "6.0.5",
"@vitejs/plugin-vue-jsx": "^5.1.5",
"@vitest/coverage-v8": "^4.1.4",
"@vitest/ui": "^4.1.4",
"@vue/cli-service": "5.0.9",
"@vue/compiler-sfc": "3.5.32",
"@vue/server-renderer": "3.5.32",
"adm-zip": "^0.5.17",
"bun-types": "^1.3.11",
"daisyui": "^5.5.19",
"cross-env": "^10.1.0",
"discord-api-types": "^0.38.44",
"dotenv": "^17.4.1",
"emulate": "0.4.1",
"eslint": "10.2.0",
"eslint-plugin-format": "^2.0.1",
"jose": "^6.2.2",
"sass": "1.99.0",
"simple-git-hooks": "^2.13.1",
"supabase": "^2.88.1",
"tailwindcss": "^4.2.2",
"typescript": "6.0.2",
"unplugin-auto-import": "^21.0.0",
Expand All @@ -251,8 +273,8 @@
"unplugin-vue-components": "^32.0.0",
"unplugin-vue-macros": "^2.14.5",
"vite": "8.0.8",
"vite-plugin-devtools-json": "^1.0.0",
"vite-plugin-environment": "1.1.3",
"vite-plugin-inspect": "^12.0.0-beta.1",
"vite-plugin-pwa": "1.2.0",
"vite-plugin-vue-devtools": "^8.1.1",
"vite-plugin-vue-layouts": "0.11.0",
Expand Down
57 changes: 11 additions & 46 deletions supabase/functions/_backend/plugins/channel_self.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
// channel self old function
import type { Context } from 'hono'
import type { ZodMiniObject } from 'zod/mini'
import type { StandardSchema } from '../utils/ark_validation.ts'
import type { MiddlewareKeyVariables } from '../utils/hono.ts'
import type { DeviceLink } from '../utils/plugin_parser.ts'
import type { Database } from '../utils/supabase.types.ts'
import { parse } from '@std/semver'
import { Hono } from 'hono/tiny'
import { z } from 'zod/mini'
import { getAppStatus, setAppStatus } from '../utils/appStatus.ts'
import { checkChannelSelfIPRateLimit, isChannelSelfRateLimited, recordChannelSelfIPRequest, recordChannelSelfRequest } from '../utils/channelSelfRateLimit.ts'
import { BRES, parseBody, simpleError200, simpleRateLimit } from '../utils/hono.ts'
Expand All @@ -15,18 +14,17 @@ import { sendNotifOrgCached } from '../utils/notifications.ts'
import { sendNotifToOrgMembersCached } from '../utils/org_email_notifications.ts'
import { closeClient, deleteChannelDevicePg, getAppByIdPg, getAppOwnerPostgres, getChannelByNamePg, getChannelDeviceOverridePg, getChannelsPg, getCompatibleChannelsPg, getDrizzleClient, getMainChannelsPg, getPgClient, setReplicationLagHeader, upsertChannelDevicePg } from '../utils/pg.ts'
import { convertQueryToBody, makeDevice, parsePluginBody } from '../utils/plugin_parser.ts'
import { channelSelfGetRequestSchema, channelSelfRequestSchema, isDevicePlatform } from '../utils/plugin_validation.ts'
import { buildRateLimitInfo } from '../utils/rateLimitInfo.ts'
import { sendStatsAndDevice } from '../utils/stats.ts'
import { backgroundTask, deviceIdRegex, INVALID_STRING_APP_ID, INVALID_STRING_DEVICE_ID, isDeprecatedPluginVersion, isLimited, MISSING_STRING_APP_ID, MISSING_STRING_DEVICE_ID, MISSING_STRING_VERSION_BUILD, MISSING_STRING_VERSION_NAME, NON_STRING_APP_ID, NON_STRING_DEVICE_ID, NON_STRING_VERSION_BUILD, NON_STRING_VERSION_NAME, reverseDomainRegex } from '../utils/utils.ts'
import { backgroundTask, isDeprecatedPluginVersion, isLimited } from '../utils/utils.ts'

// Minimum versions for local channel storage behavior
const CHANNEL_SELF_MIN_V5 = '5.34.0'
const CHANNEL_SELF_MIN_V6 = '6.34.0'
const CHANNEL_SELF_MIN_V7 = '7.34.0'
const CHANNEL_SELF_MIN_V8 = '8.0.0'

z.config(z.locales.en())
const devicePlatformScheme = z.enum(['ios', 'android', 'electron'])
const PLAN_MAU_ACTIONS: Array<'mau'> = ['mau']

async function assertChannelSelfIPRateLimit(c: Context, appId: string) {
Expand Down Expand Up @@ -141,39 +139,6 @@ function isChannelSelfLocalChannelStorageVersion(c: Context, body: DeviceLink, o
}
}

export const jsonRequestSchema = z.looseObject({
app_id: z.string({
error: issue => issue.input === undefined ? MISSING_STRING_APP_ID : NON_STRING_APP_ID,
}).check(z.regex(reverseDomainRegex, { message: INVALID_STRING_APP_ID })),
device_id: z.string({
error: issue => issue.input === undefined ? MISSING_STRING_DEVICE_ID : NON_STRING_DEVICE_ID,
}).check(z.maxLength(36), z.regex(deviceIdRegex, { message: INVALID_STRING_DEVICE_ID })),
version_name: z.string({
error: issue => issue.input === undefined ? MISSING_STRING_VERSION_NAME : NON_STRING_VERSION_NAME,
}),
version_build: z.string({
error: issue => issue.input === undefined ? MISSING_STRING_VERSION_BUILD : NON_STRING_VERSION_BUILD,
}),
is_emulator: z.boolean(),
defaultChannel: z.optional(z.string()),
channel: z.optional(z.string()),
plugin_version: z.optional(z.string()),
is_prod: z.boolean(),
platform: devicePlatformScheme,
key_id: z.optional(z.string().check(z.maxLength(20))),
})

// TODO: delete when all migrated to jsonRequestSchema
export const jsonRequestSchemaGet = z.looseObject({
app_id: z.string({
error: issue => issue.input === undefined ? MISSING_STRING_APP_ID : NON_STRING_APP_ID,
}).check(z.regex(reverseDomainRegex, { message: INVALID_STRING_APP_ID })),
is_emulator: z.boolean(),
is_prod: z.boolean(),
platform: devicePlatformScheme,
key_id: z.optional(z.string().check(z.maxLength(20))),
})

async function post(c: Context, drizzleClient: ReturnType<typeof getDrizzleClient>, body: DeviceLink): Promise<Response> {
cloudlog({ requestId: c.get('requestId'), message: 'post channel self body', body })
const { app_id, device_id, channel } = body
Expand Down Expand Up @@ -395,14 +360,14 @@ async function put(c: Context, drizzleClient: ReturnType<typeof getDrizzleClient
return simpleError200(c, 'channel_not_found', 'Cannot find channel')
}

const devicePlatform = devicePlatformScheme.safeParse(body.platform)
if (!devicePlatform.success) {
if (!isDevicePlatform(body.platform)) {
return simpleError200(c, 'invalid_platform', 'Invalid device platform', { platform: body.platform })
}

const platform = body.platform
const finalChannel = defaultChannel
? dataChannel.find((channel: { name: string }) => channel.name === defaultChannel)
: dataChannel.find((channel: { ios: boolean, android: boolean, electron: boolean }) => channel[devicePlatform.data])
: dataChannel.find((channel: { ios: boolean, android: boolean, electron: boolean }) => channel[platform])

if (!finalChannel) {
return simpleError200(c, 'channel_not_found', 'Cannot find channel')
Expand Down Expand Up @@ -534,7 +499,7 @@ async function parseChannelSelfPluginRequest(
c: Context,
body: DeviceLink,
logMessage: string,
schema: ZodMiniObject,
schema: StandardSchema<DeviceLink>,
requireDevice = true,
): Promise<{ response: Response } | { body: DeviceLink, bodyParsed: DeviceLink }> {
cloudlog({ requestId: c.get('requestId'), message: logMessage, body })
Expand Down Expand Up @@ -573,7 +538,7 @@ export const app = new Hono<MiddlewareKeyVariables>()

app.post('/', async (c) => {
const body = await parseBody<DeviceLink>(c)
const parsed = await parseChannelSelfPluginRequest(c, body, 'post body', jsonRequestSchema)
const parsed = await parseChannelSelfPluginRequest(c, body, 'post body', channelSelfRequestSchema)
if ('response' in parsed) {
return parsed.response
}
Expand Down Expand Up @@ -607,7 +572,7 @@ app.post('/', async (c) => {
app.put('/', async (c) => {
// TODO: Used as get, should be refactor with query param instead
const body = await parseBody<DeviceLink>(c)
const parsed = await parseChannelSelfPluginRequest(c, body, 'put body', jsonRequestSchema)
const parsed = await parseChannelSelfPluginRequest(c, body, 'put body', channelSelfRequestSchema)
if ('response' in parsed) {
return parsed.response
}
Expand Down Expand Up @@ -636,7 +601,7 @@ app.put('/', async (c) => {

app.delete('/', async (c) => {
const body = convertQueryToBody(c.req.query())
const parsed = await parseChannelSelfPluginRequest(c, body, 'delete body', jsonRequestSchema)
const parsed = await parseChannelSelfPluginRequest(c, body, 'delete body', channelSelfRequestSchema)
if ('response' in parsed) {
return parsed.response
}
Expand Down Expand Up @@ -666,7 +631,7 @@ app.delete('/', async (c) => {

app.get('/', async (c) => {
const body = convertQueryToBody(c.req.query())
const parsed = await parseChannelSelfPluginRequest(c, body, 'list compatible channels', jsonRequestSchemaGet, false)
const parsed = await parseChannelSelfPluginRequest(c, body, 'list compatible channels', channelSelfGetRequestSchema as StandardSchema<DeviceLink>, false)
if ('response' in parsed) {
return parsed.response
}
Expand Down
42 changes: 4 additions & 38 deletions supabase/functions/_backend/plugins/stats.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,15 @@ import type { Database } from '../utils/supabase.types.ts'
import type { AppStats, StatsActions } from '../utils/types.ts'
import { greaterOrEqual, parse } from '@std/semver'
import { Hono } from 'hono/tiny'
import { z } from 'zod/mini'
import { getAppStatus, setAppStatus } from '../utils/appStatus.ts'
import { BRES, simpleError, simpleError200, simpleRateLimit } from '../utils/hono.ts'
import { cloudlog } from '../utils/logging.ts'
import { sendNotifOrgCached } from '../utils/notifications.ts'
import { closeClient, ensurePlaceholderVersions, getAppOwnerPostgres, getAppVersionPostgres, getDrizzleClient, getPgClient } from '../utils/pg.ts'
import { makeDevice, parsePluginBody } from '../utils/plugin_parser.ts'
import { statsRequestSchema } from '../utils/plugin_validation.ts'
import { createStatsMau, createStatsVersion, onPremStats, sendStatsAndDevice } from '../utils/stats.ts'
import { backgroundTask, deviceIdRegex, INVALID_STRING_APP_ID, INVALID_STRING_DEVICE_ID, isLimited, MISSING_STRING_APP_ID, MISSING_STRING_DEVICE_ID, MISSING_STRING_PLATFORM, MISSING_STRING_VERSION_NAME, MISSING_STRING_VERSION_OS, NON_STRING_APP_ID, NON_STRING_DEVICE_ID, NON_STRING_PLATFORM, NON_STRING_VERSION_NAME, NON_STRING_VERSION_OS, reverseDomainRegex } from '../utils/utils.ts'
import { ALLOWED_STATS_ACTIONS } from './stats_actions.ts'

z.config(z.locales.en())
import { backgroundTask, INVALID_STRING_APP_ID, isLimited, MISSING_STRING_APP_ID, reverseDomainRegex } from '../utils/utils.ts'

const PLAN_ERROR = 'Cannot send stats, upgrade plan to continue to update'

Expand All @@ -27,37 +24,6 @@ export interface BatchStatsResult {
moreInfo?: Record<string, unknown>
}

export const jsonRequestSchema = z.object({
app_id: z.string({
error: issue => issue.input === undefined ? MISSING_STRING_APP_ID : NON_STRING_APP_ID,
}).check(z.regex(reverseDomainRegex, { message: INVALID_STRING_APP_ID })),
device_id: z.string({
error: issue => issue.input === undefined ? MISSING_STRING_DEVICE_ID : NON_STRING_DEVICE_ID,
}).check(z.maxLength(36), z.regex(deviceIdRegex, { message: INVALID_STRING_DEVICE_ID })),
platform: z.string({
error: issue => issue.input === undefined ? MISSING_STRING_PLATFORM : NON_STRING_PLATFORM,
}),
version_name: z.string({
error: issue => issue.input === undefined ? MISSING_STRING_VERSION_NAME : NON_STRING_VERSION_NAME,
}),
old_version_name: z.optional(z.string({
error: issue => issue.input === undefined ? MISSING_STRING_VERSION_NAME : NON_STRING_VERSION_NAME,
})),
version_os: z.string({
error: issue => issue.input === undefined ? MISSING_STRING_VERSION_OS : NON_STRING_VERSION_OS,
}),
version_code: z.optional(z.string()),
version_build: z.optional(z.string()),
action: z.optional(z.enum(ALLOWED_STATS_ACTIONS)),
custom_id: z.optional(z.string().check(z.maxLength(36))),
channel: z.optional(z.string()),
defaultChannel: z.optional(z.string()),
plugin_version: z.optional(z.string()),
is_emulator: z.boolean(),
is_prod: z.boolean(),
key_id: z.optional(z.string().check(z.maxLength(20))),
})

interface PostResult {
success: boolean
error?: string
Expand Down Expand Up @@ -261,7 +227,7 @@ app.post('/', async (c) => {
try {
// For single event, process directly and let errors propagate for proper status codes
if (!isBatch) {
const bodyParsed = parsePluginBody<AppStats>(c, events[0], jsonRequestSchema)
const bodyParsed = parsePluginBody<AppStats>(c, events[0], statsRequestSchema)
const result = await post(c, drizzleClient, bodyParsed)
if (result.isOnprem) {
return c.json({ error: 'on_premise_app', message: 'On-premise app detected' }, 429)
Expand All @@ -281,7 +247,7 @@ app.post('/', async (c) => {
for (let i = 0; i < events.length; i++) {
const event = events[i]
try {
const bodyParsed = parsePluginBody<AppStats>(c, event, jsonRequestSchema)
const bodyParsed = parsePluginBody<AppStats>(c, event, statsRequestSchema)
const result = await post(c, drizzleClient, bodyParsed)

if (result.isOnprem) {
Expand Down
Loading
Loading