{"record":{"id":"c5e146c8d0f80bf4","repo":"HeyPuter/puter","slug":"invalid-orderby-allowed-allowed-order-by-join","errorCode":null,"errorMessage":"Invalid orderBy. Allowed: ${ALLOWED_ORDER_BY.join(', ')}","messagePattern":"Invalid orderBy\\. Allowed: (.+?)","errorType":"http","errorClass":"HttpError","httpStatus":400,"severity":"error","filePath":"extensions/installedApps.ts","lineNumber":33,"sourceCode":"] as const;\nconst ORDER_BY_FIELD_MAP: Record<string, string> = {\n    id: 'apps.id',\n    name: 'apps.name',\n    uid: 'apps.uid',\n    title: 'apps.title',\n    installed_at: 'installed_at',\n};\n\nexport const handleInstalledApps = async (\n    req: Request,\n    res: Response,\n): Promise<void> => {\n    const actor = Context.get('actor');\n    if (!actor?.user?.id) throw new HttpError(401, 'Authentication required');\n\n    const orderBy = String(req.query.orderBy ?? 'installed_at');\n    if (!(ALLOWED_ORDER_BY as readonly string[]).includes(orderBy)) {\n        throw new HttpError(\n            400,\n            `Invalid orderBy. Allowed: ${ALLOWED_ORDER_BY.join(', ')}`,\n        );\n    }\n\n    const page = Math.max(Number(req.query.page) || 1, 1);\n    const limit = Math.min(Math.max(Number(req.query.limit) || 100, 1), 100);\n    const offset = (page - 1) * limit;\n    const orderByField = ORDER_BY_FIELD_MAP[orderBy];\n    const sortDirection = req.query.desc ? 'DESC' : 'ASC';\n\n    const installedApps = (await clients.db.read(\n        `SELECT\n            apps.name,\n            apps.uid,\n            apps.title,\n            apps.description,\n            apps.icon,","sourceCodeStart":15,"sourceCodeEnd":51,"githubUrl":"https://github.com/HeyPuter/puter/blob/908ec23eda38526170322c3edf71ba45ecb1ca95/extensions/installedApps.ts#L15-L51","documentation":"TogetherVideoProvider reads `Context.get('actor')` before the credit check. The actor is required both to call hasEnoughCredits and later to incrementUsage on the actor. If the actor is absent, the request reached the provider without authentication context and the driver throws 401 unauthorized rather than producing unbilled output.","triggerScenarios":"The Together generate route is hit without the auth gate, or the provider is invoked from a path (background job, test, miswired extension) that never populated Context.actor.","commonSituations":"Extension route missing `auth: true`; refactor that relocated the generate call outside the authenticated controller; test harness that instantiates the provider directly.","solutions":["Route generation through an authenticated controller/driver method.","Assert Context.actor is set before invoking the provider; fail fast with 401 if not.","Add a regression test asserting unauthenticated calls return 401.","Mirror the auth/subdomain RouteOptions used by the other video providers."],"exampleFix":"// before — driver route without auth\nextension.post('/drivers/ai-video/together', (req, res) => together.generate(req.body));\n\n// after — auth gate populates Context.actor\nextension.post('/drivers/ai-video/together', { auth: true }, (req, res) => together.generate(req.body));","handlingStrategy":"try-catch","validationCode":"// client side\nif (!puter.auth.isSignedIn()) { redirectToLogin(); return; }\nawait puter.ai.txt2video({ prompt, model: 'together/...' });","typeGuard":null,"tryCatchPattern":"try { await puter.ai.txt2video(params); }\ncatch (e) {\n  if (e?.code === 'unauthorized') { await puter.auth.signIn(); return; }\n  throw e;\n}","preventionTips":["Register Together video routes with the auth gate.","Add a regression test for unauthenticated 401.","Never invoke the provider from a Context-less background path."],"tags":["ai-video","together","auth","metering"],"backgroundTag":null,"analyzedSha":"908ec23eda38526170322c3edf71ba45ecb1ca95","analyzedAt":"2026-08-12T20:53:15.911Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}