@percepta/create 3.4.2 → 3.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +13 -0
- package/dist/index.js +135 -106
- package/dist/index.js.map +1 -1
- package/dist/{init-CtCp7Tv2.js → init-sI9aIrkU.js} +2 -2
- package/dist/init-sI9aIrkU.js.map +1 -0
- package/dist/{upstream-D-LH_1z4.js → upstream-gUHLWSR1.js} +2 -2
- package/dist/upstream-gUHLWSR1.js.map +1 -0
- package/package.json +1 -1
- package/template-versions.json +1 -0
- package/templates/monorepo/README.md +8 -5
- package/templates/monorepo/package.json.template +1 -0
- package/templates/webapp/.claude/commands/upstream.md +1 -1
- package/templates/webapp/AGENTS.md +1 -1
- package/templates/webapp/agent-skills/access-control.md +24 -1
- package/templates/webapp/agent-skills/inngest.md +5 -5
- package/templates/webapp/agent-skills/langfuse.md +4 -4
- package/templates/webapp/agent-skills/llm.md +1 -1
- package/templates/webapp/drizzle.config.ts +1 -1
- package/templates/webapp/package.json.template +10 -17
- package/templates/webapp/src/app/api/inngest/route.ts +12 -22
- package/templates/webapp/src/drizzle/db.ts +1 -2
- package/templates/webapp/src/instrumentation.ts +2 -63
- package/templates/webapp/src/server/trpc.ts +6 -18
- package/templates/webapp/src/services/AuthContextService.ts +7 -59
- package/templates/webapp/src/services/inngest/InngestService.ts +14 -62
- package/templates/webapp/src/services/langfuse/LangfuseService.ts +9 -77
- package/templates/webapp/src/services/llm/LLMService.ts +10 -88
- package/templates/webapp/src/services/logger/AppLogger.ts +3 -48
- package/templates/webapp/src/utils/syncInngestApp.ts +4 -56
- package/dist/init-CtCp7Tv2.js.map +0 -1
- package/dist/upstream-D-LH_1z4.js.map +0 -1
- package/templates/webapp/scripts/generate-migrations.ts +0 -28
- package/templates/webapp/scripts/migrate.ts +0 -21
- package/templates/webapp/scripts/setup-database.ts +0 -78
- package/templates/webapp/scripts/setup-readonly-user.ts +0 -193
- package/templates/webapp/src/drizzle/__tests__/migrationSql.test.ts +0 -24
- package/templates/webapp/src/drizzle/migrationSql.ts +0 -8
- package/templates/webapp/src/drizzle/searchPath.test.ts +0 -21
- package/templates/webapp/src/drizzle/searchPath.ts +0 -16
- package/templates/webapp/src/drizzle/ssl.ts +0 -5
- package/templates/webapp/src/services/inngest/InngestFunctionCollection.ts +0 -5
- package/templates/webapp/src/services/llm/LlmProviderService.ts +0 -85
|
@@ -140,7 +140,7 @@ If `access:reconcile` should repair missing system links for a resource type, pr
|
|
|
140
140
|
|
|
141
141
|
## Permissioned Postgres Tables
|
|
142
142
|
|
|
143
|
-
When a Drizzle table backs a SpiceDB resource, bind the table to the resource manifest once. Keep Zed as the permission source of truth; the table binding
|
|
143
|
+
When a Drizzle table backs a SpiceDB resource, bind the table to the resource manifest once. Keep Zed as the permission source of truth; the table binding records object identity, relationship columns, and which field groups require which permissions. It does not automatically write to SpiceDB.
|
|
144
144
|
|
|
145
145
|
```ts
|
|
146
146
|
import {
|
|
@@ -192,6 +192,29 @@ const checks = createColumnPermissionChecks(employeeAccess, {
|
|
|
192
192
|
const [canReadSalary] = await getAccessControl().permissions.canMany(checks);
|
|
193
193
|
```
|
|
194
194
|
|
|
195
|
+
Use a lifecycle sync wrapper in repositories/services that create, update, or delete rows with SpiceDB-backed relationships. Configure it once with either direct SpiceDB writes for authorization changes that must be visible immediately, or outbox enqueueing for transactional retry.
|
|
196
|
+
|
|
197
|
+
```ts
|
|
198
|
+
import {
|
|
199
|
+
createPermissionedResourceLifecycleSync,
|
|
200
|
+
} from "@percepta/access-control/drizzle";
|
|
201
|
+
|
|
202
|
+
const employeeLifecycle = createPermissionedResourceLifecycleSync({
|
|
203
|
+
apply: {
|
|
204
|
+
client: getAccessControl().client,
|
|
205
|
+
mode: "direct",
|
|
206
|
+
},
|
|
207
|
+
binding: employeeAccess,
|
|
208
|
+
system: accessManifest.system.ref(),
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
await employeeLifecycle.afterInsert(createdEmployee);
|
|
212
|
+
await employeeLifecycle.afterUpdate(existingEmployee, updatedEmployee);
|
|
213
|
+
await employeeLifecycle.afterDelete(deletedEmployee);
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
For transactional outbox sync, pass `apply: { mode: "outbox", enqueue }`; the queued event contains either relationship mutations or a resource delete filter. Hard deletes use resource-filter cleanup by default so relationships not represented by local columns are removed too. Pass `{ cleanup: "derived-mutations" }` to `afterDelete()` only when the resource should keep out-of-band relationships.
|
|
217
|
+
|
|
195
218
|
Do not model ordinary Postgres columns as SpiceDB objects. Model business permissions in Zed, group columns under those permissions in the table binding, then enforce the binding at the API/data boundary.
|
|
196
219
|
|
|
197
220
|
## App Code
|
|
@@ -43,14 +43,14 @@ schemas validate `event.data`, so do not wrap payload schemas in another
|
|
|
43
43
|
|
|
44
44
|
### 1. Create a function collection
|
|
45
45
|
|
|
46
|
-
Group related functions into a collection class that implements `
|
|
46
|
+
Group related functions into a collection class that implements `PerceptaInngestFunctionCollection`:
|
|
47
47
|
|
|
48
48
|
```typescript
|
|
49
|
+
import { type PerceptaInngestFunctionCollection } from "@percepta/inngest";
|
|
49
50
|
import { type InngestFunction } from "inngest";
|
|
50
|
-
import { type InngestFunctionCollection } from "../InngestFunctionCollection";
|
|
51
51
|
import { InngestService } from "../InngestService";
|
|
52
52
|
|
|
53
|
-
export class DocumentFunctions implements
|
|
53
|
+
export class DocumentFunctions implements PerceptaInngestFunctionCollection {
|
|
54
54
|
private inngestService = InngestService.create();
|
|
55
55
|
|
|
56
56
|
public get functions(): InngestFunction.Like[] {
|
|
@@ -85,9 +85,9 @@ export class DocumentFunctions implements InngestFunctionCollection {
|
|
|
85
85
|
Add your collection to the `functionCollections` array in the Inngest serve endpoint:
|
|
86
86
|
|
|
87
87
|
```typescript
|
|
88
|
-
const functionCollections:
|
|
88
|
+
const functionCollections: PerceptaInngestFunctionCollection[] = [
|
|
89
89
|
new DocumentFunctions(),
|
|
90
|
-
]
|
|
90
|
+
];
|
|
91
91
|
```
|
|
92
92
|
|
|
93
93
|
## Sending Events
|
|
@@ -21,14 +21,14 @@ Langfuse is an open-source LLM observability platform. It captures traces, spans
|
|
|
21
21
|
The template uses Next.js's instrumentation hook (called on server startup) to bootstrap OTEL with both the environment collector and optional Langfuse:
|
|
22
22
|
|
|
23
23
|
```typescript
|
|
24
|
-
import { LangfuseSpanProcessor } from "@langfuse/otel";
|
|
25
|
-
import { NodeSDK, tracing } from "@opentelemetry/sdk-node";
|
|
26
24
|
import { getNodeAutoInstrumentations } from "@opentelemetry/auto-instrumentations-node";
|
|
25
|
+
import { NodeSDK, tracing } from "@opentelemetry/sdk-node";
|
|
26
|
+
import { createLangfuseSpanProcessor } from "@percepta/ai";
|
|
27
27
|
|
|
28
28
|
const sdk = new NodeSDK({
|
|
29
29
|
spanProcessors: [
|
|
30
30
|
new tracing.BatchSpanProcessor(otlpTraceExporter),
|
|
31
|
-
|
|
31
|
+
createLangfuseSpanProcessor(env, logger),
|
|
32
32
|
],
|
|
33
33
|
instrumentations: [getNodeAutoInstrumentations()],
|
|
34
34
|
});
|
|
@@ -37,7 +37,7 @@ sdk.start();
|
|
|
37
37
|
|
|
38
38
|
- `getNodeAutoInstrumentations()` automatically instruments HTTP calls, database queries, and other standard Node.js operations.
|
|
39
39
|
- The OTLP span processor forwards general server traces to the environment collector.
|
|
40
|
-
-
|
|
40
|
+
- The shared Langfuse span processor forwards only AI SDK spans to Langfuse when all `LANGFUSE_*` variables are set. General HTTP, DB, and server spans stay in the OTEL/LGTM pipeline and are not sent to Langfuse by default.
|
|
41
41
|
|
|
42
42
|
**You do not need to manually instrument LLM calls.** Use `LLMService` from `src/services/llm/LLMService.ts`; it calls the Vercel AI SDK with telemetry enabled and provider/model metadata attached.
|
|
43
43
|
|
|
@@ -30,7 +30,7 @@ const stream = LLMService.create().streamText({
|
|
|
30
30
|
|
|
31
31
|
## Providers
|
|
32
32
|
|
|
33
|
-
`
|
|
33
|
+
The shared `@percepta/ai` provider helper chooses a provider at call time:
|
|
34
34
|
|
|
35
35
|
1. `LLM_PROVIDER`, when explicitly set.
|
|
36
36
|
2. Anthropic when `ANTHROPIC_API_KEY` is available.
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { loadEnvConfig } from "@next/env";
|
|
2
|
+
import { getPgSearchPathOption } from "@percepta/database";
|
|
2
3
|
import type { Config } from "drizzle-kit";
|
|
3
4
|
import { getEnvConfig } from "./src/config/getEnvConfig";
|
|
4
|
-
import { getPgSearchPathOption } from "./src/drizzle/searchPath";
|
|
5
5
|
|
|
6
6
|
loadEnvConfig(process.cwd());
|
|
7
7
|
|
|
@@ -17,11 +17,11 @@
|
|
|
17
17
|
"access:apply-local": "pnpm --dir ../.. run access:apply-local",
|
|
18
18
|
"auth:db:setup-and-migrate": "pnpm --dir ../.. run auth:db:setup-and-migrate",
|
|
19
19
|
"inngest:dev": "pnpm dlx inngest-cli@latest dev -u http://localhost:3000/api/inngest",
|
|
20
|
-
"db:generate": "
|
|
21
|
-
"db:migrate": "
|
|
22
|
-
"db:setup": "
|
|
20
|
+
"db:generate": "percepta-db generate-migrations",
|
|
21
|
+
"db:migrate": "percepta-db migrate --database __DB_NAME__",
|
|
22
|
+
"db:setup": "percepta-db setup --database __DB_NAME__",
|
|
23
23
|
"db:setup-and-migrate": "pnpm db:setup && pnpm db:migrate",
|
|
24
|
-
"db:setup-readonly": "
|
|
24
|
+
"db:setup-readonly": "percepta-db setup-readonly --database __DB_NAME__",
|
|
25
25
|
"db:studio": "pnpm db:setup-and-migrate && drizzle-kit studio",
|
|
26
26
|
"db:seed": "tsx ./scripts/seed.ts",
|
|
27
27
|
"deploy:percepta-test": "percepta-deploy percepta-test --app __APP_NAME__ --repo __REPO_NAME__",
|
|
@@ -33,10 +33,7 @@
|
|
|
33
33
|
"test:watch": "vitest"
|
|
34
34
|
},
|
|
35
35
|
"dependencies": {
|
|
36
|
-
"@ai-sdk/anthropic": "^2.0.23",
|
|
37
|
-
"@ai-sdk/openai": "^2.0.23",
|
|
38
36
|
"@aws-sdk/client-s3": "^3.888.0",
|
|
39
|
-
"@aws-sdk/client-secrets-manager": "^3.914.0",
|
|
40
37
|
"@aws-sdk/client-sts": "^3.913.0",
|
|
41
38
|
"@aws-sdk/credential-providers": "^3.913.0",
|
|
42
39
|
"@aws-sdk/s3-request-presigner": "^3.891.0",
|
|
@@ -48,9 +45,6 @@
|
|
|
48
45
|
"@grafana/faro-web-sdk": "^1.14.0",
|
|
49
46
|
"@grafana/faro-web-tracing": "^1.14.0",
|
|
50
47
|
"@hookform/resolvers": "^5.2.2",
|
|
51
|
-
"@inngest/middleware-validation": "^0.0.5",
|
|
52
|
-
"@langfuse/otel": "^4.0.0",
|
|
53
|
-
"@langfuse/tracing": "^4.0.0",
|
|
54
48
|
"@mantine/hooks": "^8.3.1",
|
|
55
49
|
"@next/env": "^15.3.5",
|
|
56
50
|
"@opentelemetry/api": "^1.9.0",
|
|
@@ -58,10 +52,13 @@
|
|
|
58
52
|
"@opentelemetry/exporter-trace-otlp-proto": "^0.203.0",
|
|
59
53
|
"@opentelemetry/sdk-node": "^0.203.0",
|
|
60
54
|
"@__REPO_NAME__/auth": "workspace:*",
|
|
61
|
-
"@percepta/access-control": "0.
|
|
55
|
+
"@percepta/access-control": "0.7.0",
|
|
56
|
+
"@percepta/ai": "0.1.0",
|
|
57
|
+
"@percepta/database": "0.1.0",
|
|
62
58
|
"@percepta/design": "0.3.2",
|
|
63
|
-
"@percepta/
|
|
64
|
-
"@percepta/
|
|
59
|
+
"@percepta/inngest": "0.1.0",
|
|
60
|
+
"@percepta/logger": "0.1.0",
|
|
61
|
+
"@percepta/next-utils": "0.2.0",
|
|
65
62
|
"@percepta/utils": "0.1.10",
|
|
66
63
|
"@radix-ui/react-slot": "^1.2.3",
|
|
67
64
|
"@tanstack/react-query": "^5.81.5",
|
|
@@ -69,7 +66,6 @@
|
|
|
69
66
|
"@trpc/client": "^11.4.3",
|
|
70
67
|
"@trpc/server": "^11.4.3",
|
|
71
68
|
"@trpc/tanstack-react-query": "^11.4.3",
|
|
72
|
-
"ai": "^5.0.24",
|
|
73
69
|
"better-auth": "^1.6.4",
|
|
74
70
|
"clsx": "^2.1.1",
|
|
75
71
|
"date-fns": "^4.1.0",
|
|
@@ -80,15 +76,12 @@
|
|
|
80
76
|
"import-in-the-middle": "^1.14.2",
|
|
81
77
|
"inngest": "^3.44.3",
|
|
82
78
|
"keyed-type-union": "^0.1.0",
|
|
83
|
-
"langfuse": "^3.38.6",
|
|
84
79
|
"lodash-es": "^4.17.21",
|
|
85
80
|
"lucide-react": "^0.542.0",
|
|
86
81
|
"mime-types": "^3.0.1",
|
|
87
82
|
"next": "^15.3.5",
|
|
88
83
|
"numeral": "^2.0.6",
|
|
89
84
|
"pg": "^8.16.3",
|
|
90
|
-
"pino": "^10.1.0",
|
|
91
|
-
"pino-pretty": "^13.1.3",
|
|
92
85
|
"pluralize": "^8.0.0",
|
|
93
86
|
"react": "^19.0.0",
|
|
94
87
|
"react-dom": "^19.0.0",
|
|
@@ -1,31 +1,21 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
import {
|
|
2
|
+
createPerceptaInngestNextRouteHandlers,
|
|
3
|
+
type PerceptaInngestFunctionCollection,
|
|
4
|
+
} from "@percepta/inngest";
|
|
4
5
|
import { InngestService } from "../../../services/inngest/InngestService";
|
|
5
6
|
|
|
6
7
|
export const dynamic = "force-dynamic";
|
|
7
8
|
|
|
8
|
-
const functionCollections:
|
|
9
|
+
const functionCollections: PerceptaInngestFunctionCollection[] = [];
|
|
9
10
|
|
|
10
11
|
// InngestService.create() reads env vars and throws if INNGEST_BASE_URL is
|
|
11
12
|
// unset. Defer initialization until request time so `next build` can compile
|
|
12
13
|
// this route without those vars present.
|
|
13
|
-
|
|
14
|
+
const inngestHandlers = createPerceptaInngestNextRouteHandlers({
|
|
15
|
+
getService: () => InngestService.create(),
|
|
16
|
+
getFunctions: () => functionCollections.flatMap(({ functions }) => functions),
|
|
17
|
+
});
|
|
14
18
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
if (cachedHandlers == null) {
|
|
19
|
-
const inngestService = InngestService.create();
|
|
20
|
-
cachedHandlers = serve({
|
|
21
|
-
client: inngestService.client,
|
|
22
|
-
functions: functionCollections.flatMap(({ functions }) => functions),
|
|
23
|
-
signingKey: inngestService.signingKey,
|
|
24
|
-
});
|
|
25
|
-
}
|
|
26
|
-
return cachedHandlers;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
export const GET: Handlers["GET"] = (...args) => getHandlers().GET(...args);
|
|
30
|
-
export const POST: Handlers["POST"] = (...args) => getHandlers().POST(...args);
|
|
31
|
-
export const PUT: Handlers["PUT"] = (...args) => getHandlers().PUT(...args);
|
|
19
|
+
export const GET = inngestHandlers.GET;
|
|
20
|
+
export const POST = inngestHandlers.POST;
|
|
21
|
+
export const PUT = inngestHandlers.PUT;
|
|
@@ -1,8 +1,7 @@
|
|
|
1
|
+
import { getPgSearchPathOption, getPgSslConfig } from "@percepta/database";
|
|
1
2
|
import { type NodePgDatabase, drizzle } from "drizzle-orm/node-postgres";
|
|
2
3
|
import { Pool } from "pg";
|
|
3
4
|
import { getEnvConfig } from "../config/getEnvConfig";
|
|
4
|
-
import { getPgSearchPathOption } from "./searchPath";
|
|
5
|
-
import { getPgSslConfig } from "./ssl";
|
|
6
5
|
|
|
7
6
|
export const { client, db } = createDb();
|
|
8
7
|
|
|
@@ -1,52 +1,12 @@
|
|
|
1
|
-
import { LangfuseSpanProcessor } from "@langfuse/otel";
|
|
2
1
|
import { getNodeAutoInstrumentations } from "@opentelemetry/auto-instrumentations-node";
|
|
3
2
|
import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-proto";
|
|
4
3
|
import { NodeSDK, tracing } from "@opentelemetry/sdk-node";
|
|
4
|
+
import { createLangfuseSpanProcessor } from "@percepta/ai";
|
|
5
5
|
import { compact } from "lodash-es";
|
|
6
6
|
import { getEnvConfig } from "./config/getEnvConfig";
|
|
7
7
|
import { getLogger } from "./services/logger/AppLogger";
|
|
8
8
|
|
|
9
9
|
type SpanProcessor = tracing.SpanProcessor;
|
|
10
|
-
type ReadableSpan = Parameters<SpanProcessor["onEnd"]>[0];
|
|
11
|
-
type OnStartArgs = Parameters<SpanProcessor["onStart"]>;
|
|
12
|
-
|
|
13
|
-
class FilteringSpanProcessor implements SpanProcessor {
|
|
14
|
-
public constructor(
|
|
15
|
-
private delegate: SpanProcessor,
|
|
16
|
-
private shouldExport: (span: ReadableSpan) => boolean,
|
|
17
|
-
) {}
|
|
18
|
-
|
|
19
|
-
public forceFlush(): Promise<void> {
|
|
20
|
-
return this.delegate.forceFlush();
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
public onStart(...args: OnStartArgs): void {
|
|
24
|
-
this.delegate.onStart(...args);
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
public onEnd(span: ReadableSpan): void {
|
|
28
|
-
if (this.shouldExport(span)) {
|
|
29
|
-
this.delegate.onEnd(span);
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
public shutdown(): Promise<void> {
|
|
34
|
-
return this.delegate.shutdown();
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
function isAiSdkSpan(span: ReadableSpan): boolean {
|
|
39
|
-
const operationId = span.attributes["ai.operationId"];
|
|
40
|
-
if (typeof operationId === "string" && operationId.startsWith("ai.")) {
|
|
41
|
-
return true;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
if (span.name.startsWith("ai.")) {
|
|
45
|
-
return true;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
return Object.keys(span.attributes).some((key) => key.startsWith("gen_ai."));
|
|
49
|
-
}
|
|
50
10
|
|
|
51
11
|
function getOtlpTracesEndpoint(): string | undefined {
|
|
52
12
|
const {
|
|
@@ -90,28 +50,7 @@ function getOtlpSpanProcessor(): tracing.BatchSpanProcessor | undefined {
|
|
|
90
50
|
}
|
|
91
51
|
|
|
92
52
|
function getLangfuseSpanProcessor(): SpanProcessor | undefined {
|
|
93
|
-
|
|
94
|
-
LANGFUSE_BASE_URL: baseUrl,
|
|
95
|
-
LANGFUSE_PUBLIC_KEY: publicKey,
|
|
96
|
-
LANGFUSE_SECRET_KEY: secretKey,
|
|
97
|
-
} = getEnvConfig();
|
|
98
|
-
if (!baseUrl || !publicKey || !secretKey) {
|
|
99
|
-
getLogger().debug(
|
|
100
|
-
undefined,
|
|
101
|
-
"Langfuse environment is incomplete. Skipping Langfuse OpenTelemetry.",
|
|
102
|
-
);
|
|
103
|
-
return undefined;
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
getLogger().debug(undefined, "Registering Langfuse OpenTelemetry.");
|
|
107
|
-
return new FilteringSpanProcessor(
|
|
108
|
-
new LangfuseSpanProcessor({
|
|
109
|
-
baseUrl,
|
|
110
|
-
publicKey,
|
|
111
|
-
secretKey,
|
|
112
|
-
}),
|
|
113
|
-
isAiSdkSpan,
|
|
114
|
-
);
|
|
53
|
+
return createLangfuseSpanProcessor(getEnvConfig(), getLogger());
|
|
115
54
|
}
|
|
116
55
|
|
|
117
56
|
const spanProcessors: tracing.SpanProcessor[] = compact([
|
|
@@ -1,8 +1,5 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
import {
|
|
3
|
-
createRequireApplicationAccess,
|
|
4
|
-
createRequirePermission,
|
|
5
|
-
} from "@percepta/access-control/trpc";
|
|
1
|
+
import type { SubjectRef } from "@percepta/access-control";
|
|
2
|
+
import { createAppAccessControlMiddlewares } from "@percepta/access-control/trpc";
|
|
6
3
|
import { TRPCError, initTRPC } from "@trpc/server";
|
|
7
4
|
import superjson from "superjson";
|
|
8
5
|
import { accessManifest } from "../access/access.manifest";
|
|
@@ -25,12 +22,6 @@ export interface ProtectedContext extends Context {
|
|
|
25
22
|
session: BetterAuthSession;
|
|
26
23
|
}
|
|
27
24
|
|
|
28
|
-
const lazyPermissionClient = {
|
|
29
|
-
can(check) {
|
|
30
|
-
return getAccessControl().permissions.can(check);
|
|
31
|
-
},
|
|
32
|
-
} satisfies Pick<PermissionClient, "can">;
|
|
33
|
-
|
|
34
25
|
function getCurrentSubject(ctx: Context): SubjectRef | null {
|
|
35
26
|
const userId = ctx.session?.user.id;
|
|
36
27
|
return userId == null ? null : toUserSubject(userId);
|
|
@@ -76,17 +67,14 @@ const requireAuthenticatedUser = procedure.use(
|
|
|
76
67
|
},
|
|
77
68
|
);
|
|
78
69
|
|
|
79
|
-
const requireApplicationAccess =
|
|
80
|
-
|
|
81
|
-
|
|
70
|
+
const { requireApplicationAccess, requirePermission } =
|
|
71
|
+
createAppAccessControlMiddlewares<ProtectedContext>({
|
|
72
|
+
getAccessControl,
|
|
82
73
|
getSubject: getCurrentSubject,
|
|
83
74
|
manifest: accessManifest,
|
|
84
75
|
});
|
|
85
76
|
|
|
86
|
-
export
|
|
87
|
-
accessControl: lazyPermissionClient,
|
|
88
|
-
getSubject: getCurrentSubject,
|
|
89
|
-
});
|
|
77
|
+
export { requirePermission };
|
|
90
78
|
|
|
91
79
|
export const protectedProcedure = requireAuthenticatedUser;
|
|
92
80
|
|
|
@@ -1,63 +1,11 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
1
|
+
import {
|
|
2
|
+
createAuthContextServiceFactory,
|
|
3
|
+
type AuthContextService as PerceptaAuthContextService,
|
|
4
|
+
} from "@percepta/next-utils";
|
|
3
5
|
import type { BetterAuthSession } from "../lib/auth";
|
|
4
6
|
|
|
5
7
|
export type AuthSession = BetterAuthSession;
|
|
6
8
|
|
|
7
|
-
export
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
if (AuthContextService.SINGLETON == null) {
|
|
11
|
-
AuthContextService.SINGLETON = new AuthContextService();
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
return AuthContextService.SINGLETON;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
private authAsyncLocalStorage = new AsyncLocalStorage<LocalStorage>();
|
|
18
|
-
|
|
19
|
-
private constructor() {}
|
|
20
|
-
|
|
21
|
-
public withSession<TReturn>(
|
|
22
|
-
session: AuthSession,
|
|
23
|
-
callback: () => TReturn,
|
|
24
|
-
): TReturn {
|
|
25
|
-
// Set user email on the active OpenTelemetry span
|
|
26
|
-
const activeSpan = trace.getActiveSpan();
|
|
27
|
-
if (
|
|
28
|
-
activeSpan != null &&
|
|
29
|
-
session.user.email != null &&
|
|
30
|
-
session.user.id != null
|
|
31
|
-
) {
|
|
32
|
-
activeSpan.setAttribute("user.email", session.user.email);
|
|
33
|
-
activeSpan.setAttribute("user.id", session.user.id);
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
return this.authAsyncLocalStorage.run<TReturn>({ session }, () =>
|
|
37
|
-
callback(),
|
|
38
|
-
);
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
public getSession(): AuthSession | undefined {
|
|
42
|
-
const context = this.authAsyncLocalStorage.getStore();
|
|
43
|
-
if (context == null) {
|
|
44
|
-
return undefined;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
const { session } = context;
|
|
48
|
-
return session;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
public getSessionOrThrow(): AuthSession {
|
|
52
|
-
const session = this.getSession();
|
|
53
|
-
if (session == null) {
|
|
54
|
-
throw new TRPCError({ code: "UNAUTHORIZED" });
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
return session;
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
interface LocalStorage {
|
|
62
|
-
session: AuthSession;
|
|
63
|
-
}
|
|
9
|
+
export const AuthContextService =
|
|
10
|
+
createAuthContextServiceFactory<AuthSession>();
|
|
11
|
+
export type AuthContextService = PerceptaAuthContextService<AuthSession>;
|
|
@@ -1,5 +1,9 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
1
|
+
import {
|
|
2
|
+
createPerceptaInngestServiceFactory,
|
|
3
|
+
type PerceptaInngest,
|
|
4
|
+
type PerceptaInngestService,
|
|
5
|
+
} from "@percepta/inngest";
|
|
6
|
+
import { EventSchemas } from "inngest";
|
|
3
7
|
import { getEnvConfig } from "../../config/getEnvConfig";
|
|
4
8
|
import { AppEvents } from "./events/AppEvents";
|
|
5
9
|
|
|
@@ -8,64 +12,12 @@ export interface PollOpts {
|
|
|
8
12
|
delayMs: number;
|
|
9
13
|
}
|
|
10
14
|
|
|
11
|
-
export interface AppInngest extends AppInngestInternal {
|
|
12
|
-
// Externally, we should never call createFunction on the client interface.
|
|
13
|
-
createFunction: never;
|
|
14
|
-
}
|
|
15
|
-
type AppInngestInternal = Inngest<{
|
|
16
|
-
id: string;
|
|
17
|
-
schemas: typeof EVENT_SCHEMAS;
|
|
18
|
-
}>;
|
|
19
|
-
|
|
20
|
-
export class InngestService {
|
|
21
|
-
private static SINGLETON: InngestService | undefined;
|
|
22
|
-
public static create(): InngestService {
|
|
23
|
-
if (InngestService.SINGLETON == null) {
|
|
24
|
-
InngestService.SINGLETON = new InngestService();
|
|
25
|
-
}
|
|
26
|
-
return InngestService.SINGLETON;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
private _signingKey: string | undefined;
|
|
30
|
-
private inngest: AppInngestInternal;
|
|
31
|
-
|
|
32
|
-
private constructor() {
|
|
33
|
-
const {
|
|
34
|
-
INNGEST_BASE_URL: baseUrl,
|
|
35
|
-
INNGEST_SIGNING_KEY: signingKey,
|
|
36
|
-
INNGEST_EVENT_KEY: eventKey,
|
|
37
|
-
} = getEnvConfig();
|
|
38
|
-
|
|
39
|
-
if (baseUrl == null) {
|
|
40
|
-
throw new Error("INNGEST_BASE_URL is not set.");
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
this._signingKey = signingKey;
|
|
44
|
-
this.inngest = new Inngest({
|
|
45
|
-
id: "__APP_NAME__",
|
|
46
|
-
eventKey,
|
|
47
|
-
baseUrl,
|
|
48
|
-
schemas: EVENT_SCHEMAS,
|
|
49
|
-
middleware: [validationMiddleware()],
|
|
50
|
-
});
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
public get client(): AppInngest {
|
|
54
|
-
return this.inngest as never;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
public get signingKey(): string | undefined {
|
|
58
|
-
return this._signingKey;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
public createFunction: Inngest.CreateFunction<AppInngest> = (
|
|
62
|
-
options,
|
|
63
|
-
trigger,
|
|
64
|
-
handler,
|
|
65
|
-
) => {
|
|
66
|
-
// TODO: Add context plumbing here.
|
|
67
|
-
return this.inngest.createFunction(options, trigger, handler) as never;
|
|
68
|
-
};
|
|
69
|
-
}
|
|
70
|
-
|
|
71
15
|
const EVENT_SCHEMAS = new EventSchemas().fromSchema(AppEvents);
|
|
16
|
+
|
|
17
|
+
export type AppInngest = PerceptaInngest<typeof EVENT_SCHEMAS>;
|
|
18
|
+
export const InngestService = createPerceptaInngestServiceFactory({
|
|
19
|
+
appId: "__APP_NAME__",
|
|
20
|
+
getEnv: getEnvConfig,
|
|
21
|
+
schemas: EVENT_SCHEMAS,
|
|
22
|
+
});
|
|
23
|
+
export type InngestService = PerceptaInngestService<typeof EVENT_SCHEMAS>;
|
|
@@ -1,80 +1,12 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
createPerceptaLangfuseServiceFactory,
|
|
3
|
+
type PerceptaLangfuseService,
|
|
4
|
+
} from "@percepta/ai";
|
|
2
5
|
import { getEnvConfig } from "../../config/getEnvConfig";
|
|
3
6
|
import { getLogger } from "../logger/AppLogger";
|
|
4
7
|
|
|
5
|
-
export
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
LangfuseService.SINGLETON = new LangfuseService();
|
|
11
|
-
}
|
|
12
|
-
return LangfuseService.SINGLETON;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
private readonly client: Langfuse | undefined;
|
|
16
|
-
|
|
17
|
-
private constructor() {
|
|
18
|
-
const config = getEnvConfig();
|
|
19
|
-
const baseUrl = config.LANGFUSE_BASE_URL;
|
|
20
|
-
const publicKey = config.LANGFUSE_PUBLIC_KEY;
|
|
21
|
-
const secretKey = config.LANGFUSE_SECRET_KEY;
|
|
22
|
-
|
|
23
|
-
if (baseUrl && publicKey && secretKey) {
|
|
24
|
-
this.client = new Langfuse({
|
|
25
|
-
baseUrl,
|
|
26
|
-
publicKey,
|
|
27
|
-
secretKey,
|
|
28
|
-
});
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
/**
|
|
33
|
-
* Check if Langfuse is configured.
|
|
34
|
-
*/
|
|
35
|
-
public isConfigured(): boolean {
|
|
36
|
-
return this.client != null;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
/**
|
|
40
|
-
* Update a Langfuse trace with metadata.
|
|
41
|
-
* Uses trace.update() which merges with existing metadata.
|
|
42
|
-
*
|
|
43
|
-
* @param traceId - The trace ID (same as documentPipelineRuns.id)
|
|
44
|
-
* @param metadata - The metadata to save (must be JSON-serializable)
|
|
45
|
-
*/
|
|
46
|
-
public async updateTraceMetadata<T extends object>(
|
|
47
|
-
traceId: string,
|
|
48
|
-
metadata: T,
|
|
49
|
-
): Promise<void> {
|
|
50
|
-
if (!this.isConfigured()) {
|
|
51
|
-
getLogger().warn(
|
|
52
|
-
{ safe: { traceId } },
|
|
53
|
-
"[LangfuseService] Langfuse not configured, skipping trace update",
|
|
54
|
-
);
|
|
55
|
-
return;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
// Capture client after isConfigured() guard for type narrowing
|
|
59
|
-
const client = this.client!;
|
|
60
|
-
|
|
61
|
-
try {
|
|
62
|
-
// Attach to existing trace by ID and update metadata
|
|
63
|
-
const trace = client.trace({ id: traceId });
|
|
64
|
-
trace.update({ metadata });
|
|
65
|
-
|
|
66
|
-
// Flush to ensure the update is sent
|
|
67
|
-
await client.flushAsync();
|
|
68
|
-
|
|
69
|
-
getLogger().info(
|
|
70
|
-
{ safe: { traceId } },
|
|
71
|
-
"[LangfuseService] Updated trace with metadata",
|
|
72
|
-
);
|
|
73
|
-
} catch (error) {
|
|
74
|
-
getLogger().error(
|
|
75
|
-
{ safe: { traceId }, unsafe: { error: String(error) } },
|
|
76
|
-
"[LangfuseService] Failed to update trace metadata",
|
|
77
|
-
);
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
}
|
|
8
|
+
export const LangfuseService = createPerceptaLangfuseServiceFactory({
|
|
9
|
+
getConfig: getEnvConfig,
|
|
10
|
+
getLogger,
|
|
11
|
+
});
|
|
12
|
+
export type LangfuseService = PerceptaLangfuseService;
|