@digitalpaws/starr 0.0.1
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.rst +75 -0
- package/data/.nfs.811bbb0d.43c34.4 +0 -0
- package/data/.nfs.81ba4cd0.ffff87804.4 +0 -0
- package/data/.nfs.82d127f7.fffff5704.4 +0 -0
- package/data/.nfs.82d127fe.fffff5704.4 +0 -0
- package/data/.nfs.82d159a5.fffff5aa4.4 +0 -0
- package/data/.nfs.82d159aa.fffff5aa4.4 +0 -0
- package/data/.nfs.82d15a9c.fffff5bb4.4 +0 -0
- package/data/.nfs.82d15aa1.fffff5bb4.4 +0 -0
- package/data/.nfs.82d15c65.fffff5c24.4 +0 -0
- package/data/.nfs.82d1852c.fffff5e24.4 +0 -0
- package/data/.nfs.82d18536.fffff5e24.4 +0 -0
- package/data/.nfs.82d18dbe.fffff5e44.4 +0 -0
- package/data/.nfs.82d18dc6.fffff5e44.4 +0 -0
- package/data/.nfs.8309a3f5.fffff82f4.4 +0 -0
- package/data/.nfs.8309a3fe.fffff82f4.4 +0 -0
- package/data/.nfs.8314122e.fffffb044.4 +0 -0
- package/data/.nfs.83141236.fffffb044.4 +0 -0
- package/data/.nfs.833c4fe9.fffffbc34.4 +0 -0
- package/data/.nfs.833c4ff3.fffffbc34.4 +0 -0
- package/data/logs/starr.log +66244 -0
- package/data/starr.db +0 -0
- package/data/starr.db-shm +0 -0
- package/data/starr.db-wal +0 -0
- package/data/starr.jwt +1 -0
- package/data/starr.key +1 -0
- package/dist/auth/auth-service.d.ts +42 -0
- package/dist/auth/auth-service.d.ts.map +1 -0
- package/dist/auth/auth-service.js +226 -0
- package/dist/auth/auth-service.js.map +1 -0
- package/dist/auth/http-server.d.ts +16 -0
- package/dist/auth/http-server.d.ts.map +1 -0
- package/dist/auth/http-server.js +207 -0
- package/dist/auth/http-server.js.map +1 -0
- package/dist/auth/jwt-secret.d.ts +6 -0
- package/dist/auth/jwt-secret.d.ts.map +1 -0
- package/dist/auth/jwt-secret.js +51 -0
- package/dist/auth/jwt-secret.js.map +1 -0
- package/dist/auth/oidc/oidc-flow.d.ts +60 -0
- package/dist/auth/oidc/oidc-flow.d.ts.map +1 -0
- package/dist/auth/oidc/oidc-flow.js +201 -0
- package/dist/auth/oidc/oidc-flow.js.map +1 -0
- package/dist/auth/oidc/oidc-provider-service.d.ts +59 -0
- package/dist/auth/oidc/oidc-provider-service.d.ts.map +1 -0
- package/dist/auth/oidc/oidc-provider-service.js +124 -0
- package/dist/auth/oidc/oidc-provider-service.js.map +1 -0
- package/dist/auth/oidc/user-service.d.ts +67 -0
- package/dist/auth/oidc/user-service.d.ts.map +1 -0
- package/dist/auth/oidc/user-service.js +177 -0
- package/dist/auth/oidc/user-service.js.map +1 -0
- package/dist/auth/rate-limiter.d.ts +31 -0
- package/dist/auth/rate-limiter.d.ts.map +1 -0
- package/dist/auth/rate-limiter.js +70 -0
- package/dist/auth/rate-limiter.js.map +1 -0
- package/dist/config/config-service.d.ts +83 -0
- package/dist/config/config-service.d.ts.map +1 -0
- package/dist/config/config-service.js +309 -0
- package/dist/config/config-service.js.map +1 -0
- package/dist/config/validate.d.ts +12 -0
- package/dist/config/validate.d.ts.map +1 -0
- package/dist/config/validate.js +140 -0
- package/dist/config/validate.js.map +1 -0
- package/dist/content-types/registry.d.ts +75 -0
- package/dist/content-types/registry.d.ts.map +1 -0
- package/dist/content-types/registry.js +171 -0
- package/dist/content-types/registry.js.map +1 -0
- package/dist/datastore/db.d.ts +9 -0
- package/dist/datastore/db.d.ts.map +1 -0
- package/dist/datastore/db.js +27 -0
- package/dist/datastore/db.js.map +1 -0
- package/dist/datastore/encrypt.d.ts +9 -0
- package/dist/datastore/encrypt.d.ts.map +1 -0
- package/dist/datastore/encrypt.js +84 -0
- package/dist/datastore/encrypt.js.map +1 -0
- package/dist/datastore/migrations.d.ts +10 -0
- package/dist/datastore/migrations.d.ts.map +1 -0
- package/dist/datastore/migrations.js +340 -0
- package/dist/datastore/migrations.js.map +1 -0
- package/dist/datastore/storage.d.ts +4 -0
- package/dist/datastore/storage.d.ts.map +1 -0
- package/dist/datastore/storage.js +60 -0
- package/dist/datastore/storage.js.map +1 -0
- package/dist/events/event-bus.d.ts +15 -0
- package/dist/events/event-bus.d.ts.map +1 -0
- package/dist/events/event-bus.js +140 -0
- package/dist/events/event-bus.js.map +1 -0
- package/dist/events/spine.d.ts +31 -0
- package/dist/events/spine.d.ts.map +1 -0
- package/dist/events/spine.js +68 -0
- package/dist/events/spine.js.map +1 -0
- package/dist/events/trace.d.ts +24 -0
- package/dist/events/trace.d.ts.map +1 -0
- package/dist/events/trace.js +33 -0
- package/dist/events/trace.js.map +1 -0
- package/dist/host.d.ts +65 -0
- package/dist/host.d.ts.map +1 -0
- package/dist/host.js +94 -0
- package/dist/host.js.map +1 -0
- package/dist/library/importer.d.ts +29 -0
- package/dist/library/importer.d.ts.map +1 -0
- package/dist/library/importer.js +145 -0
- package/dist/library/importer.js.map +1 -0
- package/dist/library/library-cache.d.ts +46 -0
- package/dist/library/library-cache.d.ts.map +1 -0
- package/dist/library/library-cache.js +152 -0
- package/dist/library/library-cache.js.map +1 -0
- package/dist/library/library-config.d.ts +12 -0
- package/dist/library/library-config.d.ts.map +1 -0
- package/dist/library/library-config.js +10 -0
- package/dist/library/library-config.js.map +1 -0
- package/dist/library/library-scanner.d.ts +62 -0
- package/dist/library/library-scanner.d.ts.map +1 -0
- package/dist/library/library-scanner.js +336 -0
- package/dist/library/library-scanner.js.map +1 -0
- package/dist/library/library-service.d.ts +65 -0
- package/dist/library/library-service.d.ts.map +1 -0
- package/dist/library/library-service.js +197 -0
- package/dist/library/library-service.js.map +1 -0
- package/dist/library/metadata-matcher.d.ts +56 -0
- package/dist/library/metadata-matcher.d.ts.map +1 -0
- package/dist/library/metadata-matcher.js +207 -0
- package/dist/library/metadata-matcher.js.map +1 -0
- package/dist/library/naming.d.ts +8 -0
- package/dist/library/naming.d.ts.map +1 -0
- package/dist/library/naming.js +47 -0
- package/dist/library/naming.js.map +1 -0
- package/dist/library/scan-controller.d.ts +95 -0
- package/dist/library/scan-controller.d.ts.map +1 -0
- package/dist/library/scan-controller.js +379 -0
- package/dist/library/scan-controller.js.map +1 -0
- package/dist/library/source-resolver.d.ts +17 -0
- package/dist/library/source-resolver.d.ts.map +1 -0
- package/dist/library/source-resolver.js +75 -0
- package/dist/library/source-resolver.js.map +1 -0
- package/dist/logging/logger.d.ts +3 -0
- package/dist/logging/logger.d.ts.map +1 -0
- package/dist/logging/logger.js +107 -0
- package/dist/logging/logger.js.map +1 -0
- package/dist/main.d.ts +36 -0
- package/dist/main.d.ts.map +1 -0
- package/dist/main.js +229 -0
- package/dist/main.js.map +1 -0
- package/dist/plugins/context.d.ts +10 -0
- package/dist/plugins/context.d.ts.map +1 -0
- package/dist/plugins/context.js +110 -0
- package/dist/plugins/context.js.map +1 -0
- package/dist/plugins/discovery.d.ts +2 -0
- package/dist/plugins/discovery.d.ts.map +1 -0
- package/dist/plugins/discovery.js +32 -0
- package/dist/plugins/discovery.js.map +1 -0
- package/dist/plugins/loader.d.ts +9 -0
- package/dist/plugins/loader.d.ts.map +1 -0
- package/dist/plugins/loader.js +245 -0
- package/dist/plugins/loader.js.map +1 -0
- package/dist/state/request-intake.d.ts +26 -0
- package/dist/state/request-intake.d.ts.map +1 -0
- package/dist/state/request-intake.js +74 -0
- package/dist/state/request-intake.js.map +1 -0
- package/dist/state/state-store.d.ts +146 -0
- package/dist/state/state-store.d.ts.map +1 -0
- package/dist/state/state-store.js +465 -0
- package/dist/state/state-store.js.map +1 -0
- package/dist/state/trace-store.d.ts +12 -0
- package/dist/state/trace-store.d.ts.map +1 -0
- package/dist/state/trace-store.js +24 -0
- package/dist/state/trace-store.js.map +1 -0
- package/dist/types/plugin.d.ts +93 -0
- package/dist/types/plugin.d.ts.map +1 -0
- package/dist/types/plugin.js +4 -0
- package/dist/types/plugin.js.map +1 -0
- package/dist/web/index.d.ts +10 -0
- package/dist/web/index.d.ts.map +1 -0
- package/dist/web/index.js +153 -0
- package/dist/web/index.js.map +1 -0
- package/dist/web/routes/activity.d.ts +6 -0
- package/dist/web/routes/activity.d.ts.map +1 -0
- package/dist/web/routes/activity.js +31 -0
- package/dist/web/routes/activity.js.map +1 -0
- package/dist/web/routes/auth.d.ts +6 -0
- package/dist/web/routes/auth.d.ts.map +1 -0
- package/dist/web/routes/auth.js +142 -0
- package/dist/web/routes/auth.js.map +1 -0
- package/dist/web/routes/config.d.ts +7 -0
- package/dist/web/routes/config.d.ts.map +1 -0
- package/dist/web/routes/config.js +204 -0
- package/dist/web/routes/config.js.map +1 -0
- package/dist/web/routes/content-types.d.ts +6 -0
- package/dist/web/routes/content-types.d.ts.map +1 -0
- package/dist/web/routes/content-types.js +29 -0
- package/dist/web/routes/content-types.js.map +1 -0
- package/dist/web/routes/discovery.d.ts +6 -0
- package/dist/web/routes/discovery.d.ts.map +1 -0
- package/dist/web/routes/discovery.js +111 -0
- package/dist/web/routes/discovery.js.map +1 -0
- package/dist/web/routes/library-import.d.ts +6 -0
- package/dist/web/routes/library-import.d.ts.map +1 -0
- package/dist/web/routes/library-import.js +120 -0
- package/dist/web/routes/library-import.js.map +1 -0
- package/dist/web/routes/library-management.d.ts +6 -0
- package/dist/web/routes/library-management.d.ts.map +1 -0
- package/dist/web/routes/library-management.js +120 -0
- package/dist/web/routes/library-management.js.map +1 -0
- package/dist/web/routes/library.d.ts +6 -0
- package/dist/web/routes/library.d.ts.map +1 -0
- package/dist/web/routes/library.js +28 -0
- package/dist/web/routes/library.js.map +1 -0
- package/dist/web/routes/oidc.d.ts +9 -0
- package/dist/web/routes/oidc.d.ts.map +1 -0
- package/dist/web/routes/oidc.js +203 -0
- package/dist/web/routes/oidc.js.map +1 -0
- package/dist/web/routes/requests.d.ts +6 -0
- package/dist/web/routes/requests.d.ts.map +1 -0
- package/dist/web/routes/requests.js +183 -0
- package/dist/web/routes/requests.js.map +1 -0
- package/dist/web/routes/sse.d.ts +6 -0
- package/dist/web/routes/sse.d.ts.map +1 -0
- package/dist/web/routes/sse.js +54 -0
- package/dist/web/routes/sse.js.map +1 -0
- package/package.json +33 -0
- package/package.json.license +2 -0
- package/src/auth/auth-service.ts +333 -0
- package/src/auth/http-server.ts +249 -0
- package/src/auth/jwt-secret.ts +65 -0
- package/src/auth/oidc/oidc-flow.ts +266 -0
- package/src/auth/oidc/oidc-provider-service.ts +174 -0
- package/src/auth/oidc/user-service.ts +227 -0
- package/src/auth/rate-limiter.ts +85 -0
- package/src/config/config-service.ts +434 -0
- package/src/config/validate.ts +153 -0
- package/src/content-types/registry.ts +194 -0
- package/src/datastore/db.ts +38 -0
- package/src/datastore/encrypt.ts +107 -0
- package/src/datastore/migrations.ts +367 -0
- package/src/datastore/storage.ts +83 -0
- package/src/events/event-bus.ts +180 -0
- package/src/events/spine.ts +87 -0
- package/src/events/trace.ts +38 -0
- package/src/host.ts +130 -0
- package/src/library/importer.ts +174 -0
- package/src/library/library-cache.ts +212 -0
- package/src/library/library-config.ts +22 -0
- package/src/library/library-scanner.ts +404 -0
- package/src/library/library-service.ts +282 -0
- package/src/library/metadata-matcher.ts +285 -0
- package/src/library/naming.ts +59 -0
- package/src/library/scan-controller.ts +490 -0
- package/src/library/source-resolver.ts +92 -0
- package/src/logging/logger.ts +115 -0
- package/src/main.ts +364 -0
- package/src/plugins/context.ts +138 -0
- package/src/plugins/discovery.ts +29 -0
- package/src/plugins/loader.ts +309 -0
- package/src/state/request-intake.ts +101 -0
- package/src/state/state-store.ts +621 -0
- package/src/state/trace-store.ts +27 -0
- package/src/types/plugin.ts +110 -0
- package/src/web/index.ts +182 -0
- package/src/web/routes/activity.ts +41 -0
- package/src/web/routes/auth.ts +170 -0
- package/src/web/routes/config.ts +282 -0
- package/src/web/routes/content-types.ts +42 -0
- package/src/web/routes/discovery.ts +156 -0
- package/src/web/routes/library-import.ts +196 -0
- package/src/web/routes/library-management.ts +140 -0
- package/src/web/routes/library.ts +46 -0
- package/src/web/routes/oidc.ts +260 -0
- package/src/web/routes/requests.ts +250 -0
- package/src/web/routes/sse.ts +92 -0
- package/tests/auth.test.ts +510 -0
- package/tests/config-route.test.ts +345 -0
- package/tests/config.test.ts +612 -0
- package/tests/content-types.test.ts +376 -0
- package/tests/datastore.test.ts +345 -0
- package/tests/e2e.test.ts +430 -0
- package/tests/event-bus.test.ts +925 -0
- package/tests/host.test.ts +449 -0
- package/tests/library-cache.test.ts +171 -0
- package/tests/library-scanner.test.ts +177 -0
- package/tests/library.test.ts +210 -0
- package/tests/loader-context.test.ts +644 -0
- package/tests/logger-jsonl.test.ts +154 -0
- package/tests/metadata-matcher.test.ts +198 -0
- package/tests/oidc.test.ts +218 -0
- package/tests/scan-controller.test.ts +328 -0
- package/tests/source-resolver.test.ts +100 -0
- package/tests/starr-video-integration.test.ts +153 -0
- package/tests/state-store.test.ts +700 -0
- package/tests/trace-history.test.ts +356 -0
- package/tsconfig.json +8 -0
- package/tsconfig.json.license +2 -0
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
// SPDX-License-Identifier: 0BSD
|
|
2
|
+
// SPDX-FileCopyrightText: 2025 Digi <digitalpaws@proton.me>
|
|
3
|
+
|
|
4
|
+
import type { ContentTypeDescriptor } from "@digitalpaws/starr-types-core";
|
|
5
|
+
|
|
6
|
+
export interface PluginMeta {
|
|
7
|
+
id: string;
|
|
8
|
+
version: string;
|
|
9
|
+
starrApiVersion: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface ConnectionTestResult {
|
|
13
|
+
ok: boolean;
|
|
14
|
+
message?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface PluginLifecycle {
|
|
18
|
+
start?(): void | Promise<void>;
|
|
19
|
+
stop?(): void | Promise<void>;
|
|
20
|
+
testConnection?(
|
|
21
|
+
values: Record<string, unknown>,
|
|
22
|
+
): ConnectionTestResult | Promise<ConnectionTestResult>;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface Logger {
|
|
26
|
+
debug(message: string, meta?: Record<string, unknown>): void;
|
|
27
|
+
info(message: string, meta?: Record<string, unknown>): void;
|
|
28
|
+
warn(message: string, meta?: Record<string, unknown>): void;
|
|
29
|
+
error(message: string, meta?: Record<string, unknown>): void;
|
|
30
|
+
child(tag: string): Logger;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface StarrEvent<P = unknown> {
|
|
34
|
+
readonly type: string;
|
|
35
|
+
readonly id: string;
|
|
36
|
+
readonly traceId: string;
|
|
37
|
+
readonly correlationId?: string;
|
|
38
|
+
readonly source: string;
|
|
39
|
+
readonly timestamp: string; // ISO 8601
|
|
40
|
+
readonly schemaVersion: number;
|
|
41
|
+
readonly payload: P;
|
|
42
|
+
/** Convenience: emit a correlated reply. */
|
|
43
|
+
reply<R>(type: string, payload: R): void;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface EmitOpts {
|
|
47
|
+
correlationId?: string;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface RequestOpts {
|
|
51
|
+
responseType: string;
|
|
52
|
+
windowMs: number;
|
|
53
|
+
max?: number;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export type Unsubscribe = () => void;
|
|
57
|
+
|
|
58
|
+
export type EventHandler<P = unknown> = (event: StarrEvent<P>) => void | Promise<void>;
|
|
59
|
+
|
|
60
|
+
export interface EventBus {
|
|
61
|
+
emit<P>(type: string, payload: P, opts?: EmitOpts): void;
|
|
62
|
+
on<P>(type: string, handler: EventHandler<P>): Unsubscribe;
|
|
63
|
+
request<Res>(type: string, payload: unknown, opts: RequestOpts): Promise<StarrEvent<Res>[]>;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export interface PluginStorage {
|
|
67
|
+
get<T>(key: string): Promise<T | undefined>;
|
|
68
|
+
set<T>(key: string, value: T): Promise<void>;
|
|
69
|
+
delete(key: string): Promise<void>;
|
|
70
|
+
/** List all keys (optionally filtered by prefix), sorted lexically. */
|
|
71
|
+
list<T>(prefix?: string): Promise<Array<{ key: string; value: T }>>;
|
|
72
|
+
/** Delete all keys for this plugin's namespace. Used during rollback. */
|
|
73
|
+
deleteAll(): Promise<void>;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export interface ContentTypeRegistry {
|
|
77
|
+
/**
|
|
78
|
+
* Register a content-type descriptor. Throws on:
|
|
79
|
+
* - Malformed descriptor (empty identity, list without itemType,
|
|
80
|
+
* enum without options)
|
|
81
|
+
* - Duplicate id already registered by another plugin
|
|
82
|
+
*/
|
|
83
|
+
register(descriptor: ContentTypeDescriptor): void;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export interface ConfigField {
|
|
87
|
+
type: string;
|
|
88
|
+
label: string;
|
|
89
|
+
required?: boolean;
|
|
90
|
+
default?: unknown;
|
|
91
|
+
options?: string[];
|
|
92
|
+
description?: string;
|
|
93
|
+
group?: string;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export type ConfigSchema<T> = { [K in keyof T]: ConfigField };
|
|
97
|
+
|
|
98
|
+
export interface PluginContext {
|
|
99
|
+
readonly apiVersion: string;
|
|
100
|
+
define(meta: PluginMeta): void;
|
|
101
|
+
config<T>(schema: ConfigSchema<T>): Readonly<T>;
|
|
102
|
+
events: EventBus;
|
|
103
|
+
storage: PluginStorage;
|
|
104
|
+
contentTypes: ContentTypeRegistry;
|
|
105
|
+
logger: Logger;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export type RegisterFn = (
|
|
109
|
+
ctx: PluginContext
|
|
110
|
+
) => void | PluginLifecycle | Promise<void | PluginLifecycle>;
|
package/src/web/index.ts
ADDED
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
// SPDX-License-Identifier: 0BSD
|
|
2
|
+
// SPDX-FileCopyrightText: 2025 Digi <digitalpaws@proton.me>
|
|
3
|
+
|
|
4
|
+
import Fastify from "fastify";
|
|
5
|
+
import fastifyStatic from "@fastify/static";
|
|
6
|
+
import fastifyCookie from "@fastify/cookie";
|
|
7
|
+
import fastifyFormbody from "@fastify/formbody";
|
|
8
|
+
import path from "node:path";
|
|
9
|
+
import { fileURLToPath } from "node:url";
|
|
10
|
+
import type { Logger } from "../types/plugin.js";
|
|
11
|
+
import type { AuthService } from "../auth/auth-service.js";
|
|
12
|
+
import type { OidcProviderService } from "../auth/oidc/oidc-provider-service.js";
|
|
13
|
+
import type { OidcFlow } from "../auth/oidc/oidc-flow.js";
|
|
14
|
+
import type { UserService } from "../auth/oidc/user-service.js";
|
|
15
|
+
|
|
16
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
17
|
+
|
|
18
|
+
// ── Deferred AuthService reference ───────────────────────
|
|
19
|
+
// Set by main.ts via initAuthService() after AuthService is created.
|
|
20
|
+
let authService: AuthService | null = null;
|
|
21
|
+
|
|
22
|
+
export function initAuthService(auth: AuthService): void {
|
|
23
|
+
authService = auth;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// ── Deferred OIDC service references ─────────────────────
|
|
27
|
+
let oidcProviderService: OidcProviderService | null = null;
|
|
28
|
+
let oidcFlow: OidcFlow | null = null;
|
|
29
|
+
let userService: UserService | null = null;
|
|
30
|
+
|
|
31
|
+
export function initOidcServices(
|
|
32
|
+
oidcProviders: OidcProviderService,
|
|
33
|
+
flow: OidcFlow,
|
|
34
|
+
users: UserService,
|
|
35
|
+
): void {
|
|
36
|
+
oidcProviderService = oidcProviders;
|
|
37
|
+
oidcFlow = flow;
|
|
38
|
+
userService = users;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// ── Server factory ───────────────────────────────────────
|
|
42
|
+
|
|
43
|
+
export async function createWebServer(logger: Logger) {
|
|
44
|
+
// forceCloseConnections ensures SSE and other persistent connections
|
|
45
|
+
// are forcefully torn down on close(), preventing shutdown hangs
|
|
46
|
+
// that keep the port bound (the root cause of EADDRINUSE on restart).
|
|
47
|
+
const server = Fastify({ logger: false, forceCloseConnections: true });
|
|
48
|
+
|
|
49
|
+
// Parse cookies (needed for session)
|
|
50
|
+
await server.register(fastifyCookie);
|
|
51
|
+
|
|
52
|
+
// Parse form/multipart bodies (needed for auth, config)
|
|
53
|
+
await server.register(fastifyFormbody);
|
|
54
|
+
|
|
55
|
+
// ---- AUTH HOOK ----
|
|
56
|
+
// All /api/* routes require a valid session, except an explicit
|
|
57
|
+
// allowlist of public endpoints (login, setup, OIDC initiation, etc.).
|
|
58
|
+
// Non-API routes (static files) are always public.
|
|
59
|
+
server.addHook("onRequest", async (request, reply) => {
|
|
60
|
+
// Non-API routes (static files) are always public
|
|
61
|
+
if (!request.url.startsWith("/api/")) {
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Public paths that don't require authentication
|
|
66
|
+
const publicPaths = [
|
|
67
|
+
"/api/auth/login",
|
|
68
|
+
"/api/auth/setup",
|
|
69
|
+
"/api/auth/needs-setup",
|
|
70
|
+
"/api/auth/status",
|
|
71
|
+
"/api/auth/logout", // safe — only clears cookie
|
|
72
|
+
"/api/oidc/providers", // login page needs this before auth
|
|
73
|
+
"/api/oidc/login/", // initiates OIDC flow
|
|
74
|
+
"/api/oidc/callback/", // OIDC provider redirects here
|
|
75
|
+
];
|
|
76
|
+
|
|
77
|
+
for (const pub of publicPaths) {
|
|
78
|
+
if (request.url === pub || request.url.startsWith(pub)) {
|
|
79
|
+
return; // skip auth for public paths
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// All other /api/* routes require a valid session
|
|
84
|
+
const sid = request.cookies["starr_sid"];
|
|
85
|
+
if (!sid) {
|
|
86
|
+
reply.status(401).send({ error: "Unauthorized" });
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (!authService) {
|
|
91
|
+
reply.status(500).send({ error: "auth service not available" });
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const result = authService.validateToken(sid);
|
|
96
|
+
if (!result) {
|
|
97
|
+
reply.status(401).send({ error: "Invalid session" });
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Attach user info including role and id
|
|
102
|
+
(request as any).user = {
|
|
103
|
+
id: result.userId,
|
|
104
|
+
username: result.userId,
|
|
105
|
+
role: result.role ?? "user",
|
|
106
|
+
};
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
// ---- REGISTER API ROUTES ----
|
|
110
|
+
// Each route module is registered as a Fastify plugin.
|
|
111
|
+
// Dynamic import with try/catch means a missing route module won't crash the server.
|
|
112
|
+
|
|
113
|
+
const routeModules = [
|
|
114
|
+
"./routes/auth.js",
|
|
115
|
+
"./routes/discovery.js",
|
|
116
|
+
"./routes/content-types.js",
|
|
117
|
+
"./routes/requests.js",
|
|
118
|
+
"./routes/config.js",
|
|
119
|
+
"./routes/activity.js",
|
|
120
|
+
"./routes/sse.js",
|
|
121
|
+
"./routes/oidc.js",
|
|
122
|
+
"./routes/library.js",
|
|
123
|
+
"./routes/library-management.js",
|
|
124
|
+
"./routes/library-import.js",
|
|
125
|
+
];
|
|
126
|
+
|
|
127
|
+
for (const modPath of routeModules) {
|
|
128
|
+
try {
|
|
129
|
+
const mod = await import(modPath);
|
|
130
|
+
await server.register(mod.default);
|
|
131
|
+
} catch (err: any) {
|
|
132
|
+
if (err.code === "ERR_MODULE_NOT_FOUND") {
|
|
133
|
+
logger.warn(`route module not found (skipping): ${modPath}`);
|
|
134
|
+
} else {
|
|
135
|
+
logger.error(`failed to register route module: ${modPath}`, { error: String(err) });
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// ---- INIT OIDC ROUTES ----
|
|
141
|
+
// OIDC routes need the services hooked up after registration
|
|
142
|
+
try {
|
|
143
|
+
const { initOidcRoutes } = await import("./routes/oidc.js");
|
|
144
|
+
if (authService && oidcProviderService && oidcFlow && userService) {
|
|
145
|
+
initOidcRoutes(authService, oidcProviderService, oidcFlow, userService);
|
|
146
|
+
} else {
|
|
147
|
+
logger.warn("OIDC services not fully initialized — skipping OIDC route init");
|
|
148
|
+
}
|
|
149
|
+
} catch (err: any) {
|
|
150
|
+
logger.error("failed to init OIDC routes", { error: String(err) });
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// ---- STATIC FILE SERVING (SPA) ----
|
|
154
|
+
// In production, the Svelte build output is at ../../web-ui/dist
|
|
155
|
+
// In development, the Vite dev server proxies to the host; static files aren't served.
|
|
156
|
+
const spaDist = path.resolve(__dirname, "../../../web-ui/dist");
|
|
157
|
+
try {
|
|
158
|
+
await server.register(fastifyStatic, {
|
|
159
|
+
root: spaDist,
|
|
160
|
+
prefix: "/",
|
|
161
|
+
wildcard: false,
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
// SPA fallback: serve index.html for any non-API, non-file route
|
|
165
|
+
server.setNotFoundHandler(async (request, reply) => {
|
|
166
|
+
if (request.url.startsWith("/api/")) {
|
|
167
|
+
reply.status(404).send({ error: "not found" });
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
// Serve index.html for client-side routing
|
|
171
|
+
await reply.sendFile("index.html");
|
|
172
|
+
});
|
|
173
|
+
} catch {
|
|
174
|
+
logger.warn(
|
|
175
|
+
"SPA static files not found (web-ui/dist missing); " +
|
|
176
|
+
"run `pnpm --filter starr-web-ui build` first"
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
logger.info("web server created");
|
|
181
|
+
return server;
|
|
182
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
// SPDX-License-Identifier: 0BSD
|
|
2
|
+
// SPDX-FileCopyrightText: 2025 Digi <digitalpaws@proton.me>
|
|
3
|
+
|
|
4
|
+
import type { FastifyPluginAsync } from "fastify";
|
|
5
|
+
import type { Starr } from "../../host.js";
|
|
6
|
+
|
|
7
|
+
let starrInstance: Starr | null = null;
|
|
8
|
+
|
|
9
|
+
export function initActivityRoutes(starr: Starr): void {
|
|
10
|
+
starrInstance = starr;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const activityRoutes: FastifyPluginAsync = async (server) => {
|
|
14
|
+
server.get<{ Params: { id: string } }>(
|
|
15
|
+
"/api/requests/:id/activity",
|
|
16
|
+
async (request, reply) => {
|
|
17
|
+
if (!starrInstance) {
|
|
18
|
+
reply.status(500).send({ error: "host not available" });
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const req = starrInstance.getStateStore().getByRequestId(request.params.id);
|
|
23
|
+
if (!req) {
|
|
24
|
+
reply.status(404).send({ error: "request not found" });
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Use the trace-store wrapper module (Task 020 data surface).
|
|
29
|
+
// Gracefully degrades to empty array if the wrapper isn't wired yet.
|
|
30
|
+
try {
|
|
31
|
+
const { getTraceEvents } = await import("../../state/trace-store.js");
|
|
32
|
+
const events = getTraceEvents(req.requestId);
|
|
33
|
+
return { requestId: req.requestId, events };
|
|
34
|
+
} catch {
|
|
35
|
+
return { requestId: req.requestId, events: [] };
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
);
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
export default activityRoutes;
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
// SPDX-License-Identifier: 0BSD
|
|
2
|
+
// SPDX-FileCopyrightText: 2025 Digi <digitalpaws@proton.me>
|
|
3
|
+
|
|
4
|
+
import type { FastifyPluginAsync } from "fastify";
|
|
5
|
+
import type { AuthService } from "../../auth/auth-service.js";
|
|
6
|
+
import { rateLimit } from "../../auth/rate-limiter.js";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* This module needs access to the AuthService.
|
|
10
|
+
* We provide it via a module-level setter called from main.ts.
|
|
11
|
+
*/
|
|
12
|
+
let authService: AuthService | null = null;
|
|
13
|
+
|
|
14
|
+
export function initAuthRoutes(auth: AuthService): void {
|
|
15
|
+
authService = auth;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const SESSION_COOKIE = "starr_sid";
|
|
19
|
+
const COOKIE_OPTS = {
|
|
20
|
+
httpOnly: true,
|
|
21
|
+
sameSite: "strict" as const,
|
|
22
|
+
path: "/",
|
|
23
|
+
secure: false,
|
|
24
|
+
// TTL from env or default 7 days (matches AuthService default)
|
|
25
|
+
maxAge: parseInt(process.env.STARR_SESSION_TTL_HOURS ?? "168", 10) * 3600,
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const authRoutes: FastifyPluginAsync = async (server) => {
|
|
29
|
+
// GET /api/auth/needs-setup — check if admin account exists
|
|
30
|
+
server.get("/api/auth/needs-setup", async (_request, _reply) => {
|
|
31
|
+
const needsSetup = authService?.needsSetup() ?? true;
|
|
32
|
+
return { needsSetup };
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
// GET /api/auth/status — check if logged in
|
|
36
|
+
server.get("/api/auth/status", async (request, reply) => {
|
|
37
|
+
const sid = request.cookies[SESSION_COOKIE];
|
|
38
|
+
if (!sid || !authService) {
|
|
39
|
+
reply.status(401).send({ error: "Unauthorized" });
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
const result = authService.validateToken(sid);
|
|
43
|
+
if (!result) {
|
|
44
|
+
reply.status(401).send({ error: "Invalid session" });
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
// Return richer user info (display name, avatar, role)
|
|
48
|
+
const userInfo = authService.getUserInfo(result.userId);
|
|
49
|
+
return {
|
|
50
|
+
user: result.userId,
|
|
51
|
+
displayName: userInfo?.displayName ?? result.userId,
|
|
52
|
+
avatarUrl: userInfo?.avatarUrl ?? null,
|
|
53
|
+
role: result.role ?? "user",
|
|
54
|
+
};
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
// POST /api/auth/setup — first-run admin creation
|
|
58
|
+
server.post<{ Body: { username?: string; password?: string } }>(
|
|
59
|
+
"/api/auth/setup",
|
|
60
|
+
async (request, reply) => {
|
|
61
|
+
if (!authService) {
|
|
62
|
+
reply.status(500).send({ error: "auth service not available" });
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
const { username, password } = request.body;
|
|
66
|
+
if (!username || !password) {
|
|
67
|
+
reply.status(400).send({ error: "username and password are required" });
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
try {
|
|
72
|
+
await authService.setup(username, password);
|
|
73
|
+
} catch (err: unknown) {
|
|
74
|
+
const msg = err instanceof Error ? err.message : "Setup failed";
|
|
75
|
+
if (msg.includes("already exists")) {
|
|
76
|
+
reply.status(403).send({ error: msg });
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
reply.status(400).send({ error: msg });
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Log in immediately after setup
|
|
84
|
+
const jwt = await authService.login(username, password);
|
|
85
|
+
if (jwt) {
|
|
86
|
+
reply.setCookie(SESSION_COOKIE, jwt, COOKIE_OPTS);
|
|
87
|
+
return { ok: true };
|
|
88
|
+
}
|
|
89
|
+
reply.status(500).send({ error: "Account created but login failed" });
|
|
90
|
+
},
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
// POST /api/auth/login — rate-limited (5 attempts per minute per IP)
|
|
94
|
+
server.post<{ Body: { username?: string; password?: string } }>(
|
|
95
|
+
"/api/auth/login",
|
|
96
|
+
{ preHandler: rateLimit(5, 1) },
|
|
97
|
+
async (request, reply) => {
|
|
98
|
+
if (!authService) {
|
|
99
|
+
reply.status(500).send({ error: "auth service not available" });
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
const { username, password } = request.body;
|
|
103
|
+
if (!username || !password) {
|
|
104
|
+
reply.status(400).send({ error: "username and password are required" });
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const jwt = await authService.login(username, password);
|
|
109
|
+
if (!jwt) {
|
|
110
|
+
reply.status(401).send({ error: "Invalid credentials" });
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
reply.setCookie(SESSION_COOKIE, jwt, COOKIE_OPTS);
|
|
115
|
+
return { ok: true };
|
|
116
|
+
},
|
|
117
|
+
);
|
|
118
|
+
|
|
119
|
+
// POST /api/auth/logout
|
|
120
|
+
server.post("/api/auth/logout", async (request, reply) => {
|
|
121
|
+
const sid = request.cookies[SESSION_COOKIE];
|
|
122
|
+
if (sid && authService) {
|
|
123
|
+
const result = authService.validateToken(sid);
|
|
124
|
+
if (result) {
|
|
125
|
+
authService.logout(result.sessionId);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
reply.clearCookie(SESSION_COOKIE, { path: "/" });
|
|
129
|
+
return { ok: true };
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
// POST /api/auth/change-password
|
|
133
|
+
server.post<{ Body: { currentPassword?: string; newPassword?: string } }>(
|
|
134
|
+
"/api/auth/change-password",
|
|
135
|
+
async (request, reply) => {
|
|
136
|
+
if (!authService) {
|
|
137
|
+
reply.status(500).send({ error: "auth service not available" });
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const { currentPassword, newPassword } = request.body;
|
|
142
|
+
if (!currentPassword || !newPassword) {
|
|
143
|
+
reply.status(400).send({ error: "currentPassword and newPassword are required" });
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const user = (request as any).user;
|
|
148
|
+
const username = user?.username;
|
|
149
|
+
|
|
150
|
+
if (!username) {
|
|
151
|
+
reply.status(401).send({ error: "Unauthorized" });
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
try {
|
|
156
|
+
await authService.changePassword(username, currentPassword, newPassword);
|
|
157
|
+
// Create a new session with the new password context
|
|
158
|
+
const newJwt = await authService.login(username, newPassword);
|
|
159
|
+
if (newJwt) {
|
|
160
|
+
reply.setCookie(SESSION_COOKIE, newJwt, COOKIE_OPTS);
|
|
161
|
+
}
|
|
162
|
+
return { ok: true };
|
|
163
|
+
} catch (err: unknown) {
|
|
164
|
+
reply.status(400).send({ error: err instanceof Error ? err.message : "Password change failed" });
|
|
165
|
+
}
|
|
166
|
+
},
|
|
167
|
+
);
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
export default authRoutes;
|