@qlever-llc/trellis-svelte 0.5.1 → 0.6.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.md +2 -2
- package/package.json +3 -7
- package/src/components/TrellisProvider.svelte +82 -70
- package/src/context.svelte.ts +60 -42
- package/src/index.ts +5 -2
- package/src/portal_flow.svelte.ts +101 -0
- package/src/state/auth.svelte.ts +162 -39
- package/src/state/nats.svelte.ts +44 -31
- package/src/state/trellis.svelte.ts +26 -20
package/README.md
CHANGED
|
@@ -4,6 +4,6 @@ Svelte integration for Trellis browser applications.
|
|
|
4
4
|
|
|
5
5
|
Provides `TrellisProvider` for app-level wiring, along with reactive helpers for auth state, NATS connection state, and Trellis client state.
|
|
6
6
|
|
|
7
|
-
Prefer `
|
|
7
|
+
Prefer `createTrellisApp(...)` for app-scoped auth creation and typed `app.getTrellis()` access without passing the contract around at every call site.
|
|
8
8
|
|
|
9
|
-
Uses the contract/runtime model from `@qlever-llc/trellis
|
|
9
|
+
Uses the contract/runtime model from `@qlever-llc/trellis/contracts` and `@qlever-llc/trellis`.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@qlever-llc/trellis-svelte",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Svelte components and state helpers for Trellis browser applications.",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
},
|
|
11
11
|
"repository": {
|
|
12
12
|
"type": "git",
|
|
13
|
-
"url": "https://github.com/Qlever-LLC/trellis"
|
|
13
|
+
"url": "git+https://github.com/Qlever-LLC/trellis.git"
|
|
14
14
|
},
|
|
15
15
|
"publishConfig": {
|
|
16
16
|
"access": "public"
|
|
@@ -28,11 +28,7 @@
|
|
|
28
28
|
},
|
|
29
29
|
"dependencies": {
|
|
30
30
|
"@nats-io/nats-core": "^3.3.1",
|
|
31
|
-
"@qlever-llc/trellis
|
|
32
|
-
"@qlever-llc/trellis-contracts": "^0.5.1",
|
|
33
|
-
"@qlever-llc/trellis-result": "^0.5.1",
|
|
34
|
-
"@qlever-llc/trellis-sdk-auth": "^0.5.1",
|
|
35
|
-
"@qlever-llc/trellis": "^0.5.1",
|
|
31
|
+
"@qlever-llc/trellis": "^0.6.1",
|
|
36
32
|
"typebox": "^1.0.15"
|
|
37
33
|
},
|
|
38
34
|
"peerDependencies": {
|
|
@@ -1,29 +1,27 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import type { NatsConnection } from "@nats-io/nats-core";
|
|
3
|
-
import { AsyncResult } from "@qlever-llc/
|
|
4
|
-
import type { Snippet } from "svelte";
|
|
3
|
+
import { AsyncResult } from "@qlever-llc/result";
|
|
5
4
|
import { onDestroy } from "svelte";
|
|
5
|
+
import type { Snippet } from "svelte";
|
|
6
6
|
import {
|
|
7
|
+
setAppContext,
|
|
7
8
|
setAuthContext,
|
|
8
9
|
setNatsStateContext,
|
|
9
10
|
setTrellisContext,
|
|
10
11
|
} from "../context.svelte.ts";
|
|
11
|
-
import {
|
|
12
|
+
import type { AuthState, BindErrorResult } from "../state/auth.svelte.ts";
|
|
12
13
|
import { createNatsState, type NatsState } from "../state/nats.svelte.ts";
|
|
13
|
-
import {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
}
|
|
14
|
+
import { createTrellisState, type TrellisState } from "../state/trellis.svelte.ts";
|
|
15
|
+
|
|
16
|
+
type TrellisApp = {
|
|
17
|
+
auth: AuthState;
|
|
18
|
+
};
|
|
18
19
|
|
|
19
20
|
type Props = {
|
|
20
21
|
children: Snippet;
|
|
21
22
|
loading?: Snippet;
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
serviceName?: string;
|
|
25
|
-
contract?: TrellisClientContract;
|
|
26
|
-
loginPath?: string;
|
|
23
|
+
bindError?: Snippet<[BindErrorResult]>;
|
|
24
|
+
app: TrellisApp;
|
|
27
25
|
onAuthExpired?: () => void;
|
|
28
26
|
onAuthFailed?: (error: unknown) => void;
|
|
29
27
|
onAuthRequired?: (redirectTo: string) => void;
|
|
@@ -36,14 +34,11 @@
|
|
|
36
34
|
onNatsError?: (error: Error) => void;
|
|
37
35
|
};
|
|
38
36
|
|
|
39
|
-
|
|
37
|
+
let {
|
|
40
38
|
children,
|
|
41
39
|
loading,
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
serviceName = "app",
|
|
45
|
-
contract,
|
|
46
|
-
loginPath = "/login",
|
|
40
|
+
bindError,
|
|
41
|
+
app,
|
|
47
42
|
onAuthExpired,
|
|
48
43
|
onAuthFailed,
|
|
49
44
|
onAuthRequired,
|
|
@@ -56,63 +51,59 @@
|
|
|
56
51
|
onNatsError,
|
|
57
52
|
}: Props = $props();
|
|
58
53
|
|
|
54
|
+
let bindErrorResult = $state<BindErrorResult | null>(null);
|
|
55
|
+
|
|
59
56
|
type InitContext = {
|
|
60
57
|
auth: AuthState;
|
|
61
58
|
nats: NatsState;
|
|
62
59
|
trellis: TrellisState;
|
|
63
60
|
};
|
|
61
|
+
const isBrowser = typeof window !== "undefined";
|
|
64
62
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
63
|
+
function getRedirectTo(): string {
|
|
64
|
+
if (typeof window === "undefined") {
|
|
65
|
+
return "/";
|
|
68
66
|
}
|
|
69
|
-
}
|
|
70
67
|
|
|
71
|
-
|
|
72
|
-
constructor() {
|
|
73
|
-
super("Bind failed");
|
|
74
|
-
}
|
|
68
|
+
return window.location.pathname + window.location.search;
|
|
75
69
|
}
|
|
76
70
|
|
|
77
|
-
function
|
|
78
|
-
|
|
79
|
-
authUrl,
|
|
80
|
-
loginPath,
|
|
81
|
-
contract,
|
|
82
|
-
};
|
|
71
|
+
function redirectToLogin(redirectTo: string): void {
|
|
72
|
+
if (typeof window === "undefined") return;
|
|
83
73
|
|
|
84
|
-
|
|
74
|
+
const url = new URL(app.auth.loginPath, window.location.origin);
|
|
75
|
+
url.searchParams.set("redirectTo", redirectTo);
|
|
76
|
+
window.location.href = url.toString();
|
|
85
77
|
}
|
|
86
78
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
if (
|
|
91
|
-
|
|
79
|
+
function handleAuthRequired(): void {
|
|
80
|
+
const redirectTo = getRedirectTo();
|
|
81
|
+
onAuthRequired?.(redirectTo);
|
|
82
|
+
if (!onAuthRequired) {
|
|
83
|
+
redirectToLogin(redirectTo);
|
|
92
84
|
}
|
|
93
|
-
|
|
94
|
-
return window.location.pathname + window.location.search;
|
|
95
85
|
}
|
|
96
86
|
|
|
97
|
-
async function initialize(): Promise<InitContext> {
|
|
87
|
+
async function initialize(): Promise<InitContext | null> {
|
|
98
88
|
const result = await AsyncResult.try(async () => {
|
|
89
|
+
const authState = app.auth;
|
|
90
|
+
|
|
99
91
|
await authState.init();
|
|
100
92
|
const bindResult = await authState.handleCallback();
|
|
101
93
|
authState.cleanupCallbackUrl();
|
|
102
94
|
|
|
103
95
|
if (bindResult !== null && bindResult.status !== "bound") {
|
|
96
|
+
bindErrorResult = bindResult;
|
|
104
97
|
onBindError?.(bindResult);
|
|
105
|
-
|
|
98
|
+
return null;
|
|
106
99
|
}
|
|
107
100
|
|
|
108
101
|
if (!authState.isAuthenticated) {
|
|
109
|
-
|
|
110
|
-
|
|
102
|
+
handleAuthRequired();
|
|
103
|
+
return null;
|
|
111
104
|
}
|
|
112
105
|
|
|
113
|
-
const effectiveNatsServers = authState.natsServers ?? natsServers;
|
|
114
106
|
const natsState = await createNatsState(authState, {
|
|
115
|
-
servers: effectiveNatsServers,
|
|
116
107
|
onConnecting: onNatsConnecting,
|
|
117
108
|
onConnected: onNatsConnected,
|
|
118
109
|
onDisconnect: onNatsDisconnect,
|
|
@@ -120,13 +111,13 @@
|
|
|
120
111
|
onReconnect: onNatsReconnect,
|
|
121
112
|
onError: onNatsError,
|
|
122
113
|
onAuthRequired: () => {
|
|
123
|
-
|
|
114
|
+
onAuthExpired?.();
|
|
115
|
+
handleAuthRequired();
|
|
124
116
|
},
|
|
125
117
|
});
|
|
126
118
|
|
|
127
119
|
const trellisState = await createTrellisState(authState, natsState, {
|
|
128
|
-
|
|
129
|
-
contract,
|
|
120
|
+
contract: authState.contract,
|
|
130
121
|
});
|
|
131
122
|
|
|
132
123
|
return {
|
|
@@ -138,10 +129,7 @@
|
|
|
138
129
|
|
|
139
130
|
if (result.isErr()) {
|
|
140
131
|
const error = result.error;
|
|
141
|
-
|
|
142
|
-
throw error;
|
|
143
|
-
}
|
|
144
|
-
authState.clearAuth();
|
|
132
|
+
app.auth.clearAuth();
|
|
145
133
|
onAuthFailed?.(error);
|
|
146
134
|
throw error;
|
|
147
135
|
}
|
|
@@ -154,30 +142,54 @@
|
|
|
154
142
|
});
|
|
155
143
|
}
|
|
156
144
|
|
|
157
|
-
const initPromise = initialize();
|
|
158
|
-
const
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
145
|
+
const initPromise = isBrowser ? initialize() : null;
|
|
146
|
+
const readyPromise: Promise<InitContext> | null = initPromise?.then((ctx) => {
|
|
147
|
+
if (!ctx) {
|
|
148
|
+
throw new Error("Trellis context is not available");
|
|
149
|
+
}
|
|
150
|
+
return ctx;
|
|
151
|
+
}) ?? null;
|
|
152
|
+
const natsStatePromise: Promise<NatsState> | null = readyPromise?.then((ctx) => ctx.nats) ?? null;
|
|
153
|
+
const trellisPromise: Promise<TrellisState["trellis"]> | null = readyPromise?.then((ctx) => ctx.trellis.trellis) ?? null;
|
|
154
|
+
const natsPromise: Promise<NatsConnection> | null = readyPromise?.then((ctx) => ctx.nats.nc) ?? null;
|
|
155
|
+
|
|
156
|
+
if (readyPromise && natsStatePromise && trellisPromise && natsPromise) {
|
|
157
|
+
void readyPromise.catch(() => {});
|
|
158
|
+
setAppContext(() => app);
|
|
159
|
+
setAuthContext(() => app.auth);
|
|
160
|
+
setNatsStateContext(natsStatePromise);
|
|
161
|
+
setTrellisContext({
|
|
162
|
+
trellis: trellisPromise,
|
|
163
|
+
nats: natsPromise,
|
|
164
|
+
});
|
|
165
|
+
}
|
|
168
166
|
|
|
169
167
|
onDestroy(() => {
|
|
170
|
-
|
|
168
|
+
if (!readyPromise) return;
|
|
169
|
+
|
|
170
|
+
void readyPromise.then((ctx) => {
|
|
171
171
|
ctx.trellis.stop();
|
|
172
172
|
void ctx.nats.disconnect();
|
|
173
173
|
});
|
|
174
174
|
});
|
|
175
175
|
</script>
|
|
176
176
|
|
|
177
|
-
{#
|
|
177
|
+
{#if !isBrowser}
|
|
178
178
|
{#if loading}
|
|
179
179
|
{@render loading()}
|
|
180
180
|
{/if}
|
|
181
|
-
{:
|
|
182
|
-
{
|
|
183
|
-
{
|
|
181
|
+
{:else}
|
|
182
|
+
{#await initPromise}
|
|
183
|
+
{#if loading}
|
|
184
|
+
{@render loading()}
|
|
185
|
+
{/if}
|
|
186
|
+
{:then ctx}
|
|
187
|
+
{#if bindErrorResult}
|
|
188
|
+
{#if bindError}
|
|
189
|
+
{@render bindError(bindErrorResult)}
|
|
190
|
+
{/if}
|
|
191
|
+
{:else if ctx}
|
|
192
|
+
{@render children()}
|
|
193
|
+
{/if}
|
|
194
|
+
{/await}
|
|
195
|
+
{/if}
|
package/src/context.svelte.ts
CHANGED
|
@@ -1,17 +1,12 @@
|
|
|
1
|
+
import type { BaseError, Result } from "@qlever-llc/result";
|
|
1
2
|
import type { NatsConnection } from "@nats-io/nats-core";
|
|
2
|
-
import type { Trellis } from "@qlever-llc/trellis";
|
|
3
|
-
import
|
|
4
|
-
import {
|
|
5
|
-
import type { AuthState } from "./state/auth.svelte.ts";
|
|
3
|
+
import type { InferSchemaType, Trellis, TrellisAPI } from "@qlever-llc/trellis";
|
|
4
|
+
import { createContext } from "svelte";
|
|
5
|
+
import { createAuthState, type AuthState, type AuthStateConfig, type SignInOptions } from "./state/auth.svelte.ts";
|
|
6
6
|
import type { NatsState } from "./state/nats.svelte.ts";
|
|
7
7
|
|
|
8
|
-
const TRELLIS_KEY = Symbol("trellis");
|
|
9
|
-
const NATS_KEY = Symbol("nats");
|
|
10
|
-
const NATS_STATE_KEY = Symbol("nats-state");
|
|
11
|
-
const AUTH_KEY = Symbol("auth");
|
|
12
|
-
|
|
13
8
|
type TrellisContext = {
|
|
14
|
-
trellis: Promise<
|
|
9
|
+
trellis: Promise<Trellis<TrellisAPI>>;
|
|
15
10
|
nats: Promise<NatsConnection>;
|
|
16
11
|
};
|
|
17
12
|
|
|
@@ -25,60 +20,83 @@ type RequestOpts = {
|
|
|
25
20
|
timeout?: number;
|
|
26
21
|
};
|
|
27
22
|
|
|
28
|
-
type
|
|
29
|
-
|
|
30
|
-
input: InferSchemaType<TA["rpc"][M]["input"]>;
|
|
31
|
-
output: InferSchemaType<TA["rpc"][M]["output"]>;
|
|
32
|
-
};
|
|
33
|
-
};
|
|
34
|
-
|
|
35
|
-
type TypedRequestSurface<TA extends TrellisAPI> = {
|
|
36
|
-
requestOrThrow<M extends keyof RpcMapFor<TA> & string>(
|
|
23
|
+
type TypedTrellis<TA extends TrellisAPI> = Omit<Trellis<TrellisAPI>, "request" | "requestOrThrow"> & {
|
|
24
|
+
request<M extends keyof TA["rpc"] & string>(
|
|
37
25
|
method: M,
|
|
38
|
-
input:
|
|
26
|
+
input: InferSchemaType<TA["rpc"][M]["input"]>,
|
|
39
27
|
opts?: RequestOpts,
|
|
40
|
-
): Promise<
|
|
28
|
+
): Promise<Result<InferSchemaType<TA["rpc"][M]["output"]>, BaseError>>;
|
|
29
|
+
requestOrThrow<M extends keyof TA["rpc"] & string>(
|
|
30
|
+
method: M,
|
|
31
|
+
input: InferSchemaType<TA["rpc"][M]["input"]>,
|
|
32
|
+
opts?: RequestOpts,
|
|
33
|
+
): Promise<InferSchemaType<TA["rpc"][M]["output"]>>;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
function createTypedTrellis<TA extends TrellisAPI>(trellis: Trellis<TrellisAPI>): TypedTrellis<TA> {
|
|
37
|
+
return trellis as TypedTrellis<TA>;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export type BoundTrellisApp<TContract extends TrellisContractLike> = {
|
|
41
|
+
auth: AuthState;
|
|
42
|
+
signIn: (options?: SignInOptions) => Promise<never>;
|
|
43
|
+
getTrellis: () => Promise<TypedTrellis<TContract["API"]["trellis"]>>;
|
|
41
44
|
};
|
|
42
45
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
+
const [getTrellisContext, setTrellisContextValue] = createContext<TrellisContext>();
|
|
47
|
+
const [getNatsStateContext, setNatsStateContextValue] = createContext<Promise<NatsState>>();
|
|
48
|
+
const [getAuthContext, setAuthContextValue] = createContext<() => AuthState>();
|
|
49
|
+
const [getAppContext, setAppContextValue] = createContext<() => unknown>();
|
|
46
50
|
|
|
47
|
-
export function setTrellisContext
|
|
48
|
-
ctx:
|
|
51
|
+
export function setTrellisContext(
|
|
52
|
+
ctx: TrellisContext,
|
|
49
53
|
): void {
|
|
50
|
-
|
|
51
|
-
setContext(NATS_KEY, ctx.nats);
|
|
54
|
+
setTrellisContextValue(ctx);
|
|
52
55
|
}
|
|
53
56
|
|
|
54
57
|
export function setNatsStateContext(natsState: Promise<NatsState>): void {
|
|
55
|
-
|
|
58
|
+
setNatsStateContextValue(natsState);
|
|
56
59
|
}
|
|
57
60
|
|
|
58
|
-
export function setAuthContext(
|
|
59
|
-
|
|
61
|
+
export function setAuthContext(getAuth: () => AuthState): void {
|
|
62
|
+
setAuthContextValue(getAuth);
|
|
60
63
|
}
|
|
61
64
|
|
|
62
|
-
export function
|
|
63
|
-
|
|
65
|
+
export function setAppContext(getApp: () => unknown): void {
|
|
66
|
+
setAppContextValue(getApp);
|
|
64
67
|
}
|
|
65
68
|
|
|
66
|
-
export function
|
|
67
|
-
|
|
68
|
-
):
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
>;
|
|
69
|
+
export function createTrellisApp<TContract extends TrellisContractLike>(
|
|
70
|
+
config: AuthStateConfig & { contract: TContract },
|
|
71
|
+
): BoundTrellisApp<TContract> {
|
|
72
|
+
const auth = createAuthState(config);
|
|
73
|
+
|
|
74
|
+
let app!: BoundTrellisApp<TContract>;
|
|
75
|
+
app = {
|
|
76
|
+
auth,
|
|
77
|
+
signIn: (options) => auth.signIn(options),
|
|
78
|
+
getTrellis: () => {
|
|
79
|
+
if (getAppContext()() !== app) {
|
|
80
|
+
throw new Error("getTrellis() was called outside the matching TrellisProvider");
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return getTrellisContext().trellis.then((trellis) =>
|
|
84
|
+
createTypedTrellis<TContract["API"]["trellis"]>(trellis)
|
|
85
|
+
);
|
|
86
|
+
},
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
return app;
|
|
72
90
|
}
|
|
73
91
|
|
|
74
92
|
export function getNats(): Promise<NatsConnection> {
|
|
75
|
-
return
|
|
93
|
+
return getTrellisContext().nats;
|
|
76
94
|
}
|
|
77
95
|
|
|
78
96
|
export function getNatsState(): Promise<NatsState> {
|
|
79
|
-
return
|
|
97
|
+
return getNatsStateContext();
|
|
80
98
|
}
|
|
81
99
|
|
|
82
100
|
export function getAuth(): AuthState {
|
|
83
|
-
return
|
|
101
|
+
return getAuthContext()();
|
|
84
102
|
}
|
package/src/index.ts
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
export { default as TrellisProvider } from "./components/TrellisProvider.svelte";
|
|
2
|
-
export { getAuth, getNats, getNatsState,
|
|
3
|
-
export
|
|
2
|
+
export { createTrellisApp, getAuth, getNats, getNatsState, setTrellisContext } from "./context.svelte.ts";
|
|
3
|
+
export type { BoundTrellisApp } from "./context.svelte.ts";
|
|
4
|
+
export { createPortalFlow, PortalFlowController, type CreatePortalFlowConfig } from "./portal_flow.svelte.ts";
|
|
5
|
+
export { AuthState, type BindErrorResult, type BindResult, createAuthState, type SignInOptions } from "./state/auth.svelte.ts";
|
|
4
6
|
export type { NatsStateConfig, Status as NatsStatus } from "./state/nats.svelte.ts";
|
|
5
7
|
export { createNatsState, NatsState } from "./state/nats.svelte.ts";
|
|
8
|
+
export type { TrellisClientContract, TrellisStateConfig } from "./state/trellis.svelte.ts";
|
|
6
9
|
export { createTrellisState, TrellisState } from "./state/trellis.svelte.ts";
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import {
|
|
2
|
+
fetchPortalFlowState,
|
|
3
|
+
portalFlowIdFromUrl,
|
|
4
|
+
portalProviderLoginUrl,
|
|
5
|
+
type PortalFlowState,
|
|
6
|
+
submitPortalApproval,
|
|
7
|
+
type AuthConfig,
|
|
8
|
+
} from "@qlever-llc/trellis/auth";
|
|
9
|
+
|
|
10
|
+
export type CreatePortalFlowConfig = AuthConfig & {
|
|
11
|
+
getUrl?: () => URL;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
function errorMessage(error: unknown): string {
|
|
15
|
+
return error instanceof Error ? error.message : String(error);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function defaultGetUrl(): URL {
|
|
19
|
+
return new URL(globalThis.location.href);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export class PortalFlowController {
|
|
23
|
+
flowId: string | null = $state(null);
|
|
24
|
+
state: PortalFlowState | null = $state(null);
|
|
25
|
+
loading = $state(false);
|
|
26
|
+
error: string | null = $state(null);
|
|
27
|
+
|
|
28
|
+
#config: AuthConfig;
|
|
29
|
+
#getUrl: () => URL;
|
|
30
|
+
|
|
31
|
+
constructor(config: CreatePortalFlowConfig) {
|
|
32
|
+
this.#config = { authUrl: config.authUrl };
|
|
33
|
+
this.#getUrl = config.getUrl ?? defaultGetUrl;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async load(): Promise<PortalFlowState | null> {
|
|
37
|
+
this.loading = true;
|
|
38
|
+
this.error = null;
|
|
39
|
+
this.state = null;
|
|
40
|
+
|
|
41
|
+
try {
|
|
42
|
+
const flowId = portalFlowIdFromUrl(this.#getUrl());
|
|
43
|
+
this.flowId = flowId;
|
|
44
|
+
if (!flowId) {
|
|
45
|
+
this.error = "Missing flow id.";
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const state = await fetchPortalFlowState(this.#config, flowId);
|
|
50
|
+
this.state = state;
|
|
51
|
+
return state;
|
|
52
|
+
} catch (error) {
|
|
53
|
+
this.error = errorMessage(error);
|
|
54
|
+
this.state = null;
|
|
55
|
+
return null;
|
|
56
|
+
} finally {
|
|
57
|
+
this.loading = false;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
providerUrl(providerId: string): string {
|
|
62
|
+
if (!this.flowId) {
|
|
63
|
+
throw new Error("Missing flow id.");
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return portalProviderLoginUrl(this.#config, providerId, this.flowId);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async approve(): Promise<PortalFlowState | null> {
|
|
70
|
+
return this.#submit("approved");
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async deny(): Promise<PortalFlowState | null> {
|
|
74
|
+
return this.#submit("denied");
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async #submit(decision: "approved" | "denied"): Promise<PortalFlowState | null> {
|
|
78
|
+
if (!this.flowId) {
|
|
79
|
+
this.error = "Missing flow id.";
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
this.loading = true;
|
|
84
|
+
this.error = null;
|
|
85
|
+
|
|
86
|
+
try {
|
|
87
|
+
const state = await submitPortalApproval(this.#config, this.flowId, decision);
|
|
88
|
+
this.state = state;
|
|
89
|
+
return state;
|
|
90
|
+
} catch (error) {
|
|
91
|
+
this.error = errorMessage(error);
|
|
92
|
+
return null;
|
|
93
|
+
} finally {
|
|
94
|
+
this.loading = false;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function createPortalFlow(config: CreatePortalFlowConfig): PortalFlowController {
|
|
100
|
+
return new PortalFlowController(config);
|
|
101
|
+
}
|
package/src/state/auth.svelte.ts
CHANGED
|
@@ -1,20 +1,22 @@
|
|
|
1
1
|
import {
|
|
2
2
|
type BindResponse,
|
|
3
3
|
type BindSuccessResponse,
|
|
4
|
+
bindFlow,
|
|
4
5
|
bindSession,
|
|
5
|
-
buildLoginUrl,
|
|
6
6
|
clearSessionKey,
|
|
7
|
-
extractAuthErrorFromFragment,
|
|
8
|
-
extractAuthTokenFromFragment,
|
|
9
7
|
getOrCreateSessionKey,
|
|
10
8
|
getPublicSessionKey,
|
|
11
9
|
type SentinelCreds,
|
|
12
10
|
type SessionKeyHandle,
|
|
13
|
-
} from "@qlever-llc/trellis
|
|
14
|
-
import {
|
|
11
|
+
} from "@qlever-llc/trellis/auth";
|
|
12
|
+
import { canonicalizeJsonValue } from "../../../auth/utils.ts";
|
|
13
|
+
import { oauthInitSig } from "../../../auth/browser/session.ts";
|
|
14
|
+
import { Result } from "@qlever-llc/result";
|
|
15
15
|
import { SvelteDate } from "svelte/reactivity";
|
|
16
|
+
import type { TrellisContractV1 } from "@qlever-llc/trellis";
|
|
16
17
|
import { Type } from "typebox";
|
|
17
18
|
import { Value } from "typebox/value";
|
|
19
|
+
import type { TrellisClientContract } from "./trellis.svelte.ts";
|
|
18
20
|
|
|
19
21
|
export type BindErrorResult =
|
|
20
22
|
| { status: "insufficient_capabilities"; missingCapabilities: string[] }
|
|
@@ -24,15 +26,8 @@ export type BindErrorResult =
|
|
|
24
26
|
|
|
25
27
|
export type BindResult = { status: "bound" } | BindErrorResult;
|
|
26
28
|
|
|
27
|
-
const buildLoginHref = buildLoginUrl as unknown as (
|
|
28
|
-
config: { authUrl: string },
|
|
29
|
-
provider: string | undefined,
|
|
30
|
-
redirectTo: string,
|
|
31
|
-
handle: SessionKeyHandle,
|
|
32
|
-
contract: Record<string, unknown>,
|
|
33
|
-
) => Promise<string>;
|
|
34
|
-
|
|
35
29
|
const STORAGE_KEY = "trellis_auth";
|
|
30
|
+
const AUTH_URL_STORAGE_KEY = "trellis_auth_url";
|
|
36
31
|
|
|
37
32
|
type AuthStateData = {
|
|
38
33
|
handle: SessionKeyHandle | null;
|
|
@@ -104,11 +99,88 @@ function clearPersistedAuth(): void {
|
|
|
104
99
|
}
|
|
105
100
|
|
|
106
101
|
export type AuthStateConfig = {
|
|
107
|
-
authUrl
|
|
102
|
+
authUrl?: string; // https://auth.example.com
|
|
108
103
|
loginPath?: string;
|
|
109
|
-
contract?:
|
|
104
|
+
contract?: TrellisClientContract;
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
export type SignInOptions = {
|
|
108
|
+
authUrl?: string;
|
|
109
|
+
redirectTo?: string;
|
|
110
|
+
landingPath?: string;
|
|
111
|
+
context?: unknown;
|
|
110
112
|
};
|
|
111
113
|
|
|
114
|
+
function normalizeAuthUrl(authUrl: string): string {
|
|
115
|
+
return new URL(authUrl).toString().replace(/\/$/, "");
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function encodeJsonForQuery(value: unknown): string {
|
|
119
|
+
const json = canonicalizeJsonValue(value);
|
|
120
|
+
const bytes = new TextEncoder().encode(json);
|
|
121
|
+
let binary = "";
|
|
122
|
+
for (const byte of bytes) binary += String.fromCharCode(byte);
|
|
123
|
+
return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/g, "");
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
async function buildLoginUrl(options: {
|
|
127
|
+
authUrl: string;
|
|
128
|
+
redirectTo: string;
|
|
129
|
+
handle: SessionKeyHandle;
|
|
130
|
+
contract: Record<string, unknown>;
|
|
131
|
+
context?: unknown;
|
|
132
|
+
}): Promise<string> {
|
|
133
|
+
const sessionKey = getPublicSessionKey(options.handle);
|
|
134
|
+
const sig = await oauthInitSig(options.handle, options.redirectTo, options.context);
|
|
135
|
+
const url = new URL(`${options.authUrl}/auth/login`);
|
|
136
|
+
|
|
137
|
+
url.searchParams.set("redirectTo", options.redirectTo);
|
|
138
|
+
url.searchParams.set("sessionKey", sessionKey);
|
|
139
|
+
url.searchParams.set("sig", sig);
|
|
140
|
+
url.searchParams.set("contract", encodeJsonForQuery(options.contract));
|
|
141
|
+
if (options.context !== undefined) {
|
|
142
|
+
url.searchParams.set("context", encodeJsonForQuery(options.context));
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return url.href;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function loadPersistedAuthUrl(): string | null {
|
|
149
|
+
if (typeof localStorage === "undefined") return null;
|
|
150
|
+
const stored = localStorage.getItem(AUTH_URL_STORAGE_KEY);
|
|
151
|
+
if (!stored) return null;
|
|
152
|
+
|
|
153
|
+
return Result.try(() => normalizeAuthUrl(stored)).unwrapOr(null);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function persistAuthUrl(authUrl: string): string {
|
|
157
|
+
const normalized = normalizeAuthUrl(authUrl);
|
|
158
|
+
if (typeof localStorage !== "undefined") {
|
|
159
|
+
localStorage.setItem(AUTH_URL_STORAGE_KEY, normalized);
|
|
160
|
+
}
|
|
161
|
+
return normalized;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function resolveRedirectTo(
|
|
165
|
+
options: SignInOptions,
|
|
166
|
+
currentUrl: URL,
|
|
167
|
+
): string {
|
|
168
|
+
if (options.redirectTo) {
|
|
169
|
+
return new URL(options.redirectTo, currentUrl.origin).toString();
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const queryRedirect = currentUrl.searchParams.get("redirectTo");
|
|
173
|
+
if (queryRedirect) {
|
|
174
|
+
return new URL(queryRedirect, currentUrl.origin).toString();
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if (options.landingPath) {
|
|
178
|
+
return new URL(options.landingPath, currentUrl.origin).toString();
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
return currentUrl.toString();
|
|
182
|
+
}
|
|
183
|
+
|
|
112
184
|
/**
|
|
113
185
|
* Svelte 5 runes-based reactive authentication state.
|
|
114
186
|
*
|
|
@@ -132,12 +204,48 @@ export class AuthState {
|
|
|
132
204
|
#bindingInProgress: Promise<BindResult> | null = null;
|
|
133
205
|
|
|
134
206
|
constructor(config: AuthStateConfig) {
|
|
135
|
-
this.#config =
|
|
207
|
+
this.#config = {
|
|
208
|
+
...config,
|
|
209
|
+
authUrl: config.authUrl ? normalizeAuthUrl(config.authUrl) : undefined,
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
#getConfiguredAuthUrl(): string | null {
|
|
214
|
+
if (this.#config.authUrl) return this.#config.authUrl;
|
|
215
|
+
|
|
216
|
+
const persisted = loadPersistedAuthUrl();
|
|
217
|
+
if (!persisted) return null;
|
|
218
|
+
|
|
219
|
+
this.#config.authUrl = persisted;
|
|
220
|
+
return persisted;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
#requireAuthUrl(): string {
|
|
224
|
+
const authUrl = this.#getConfiguredAuthUrl();
|
|
225
|
+
if (!authUrl) {
|
|
226
|
+
throw new Error("Auth URL is not configured");
|
|
227
|
+
}
|
|
228
|
+
return authUrl;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
setAuthUrl(authUrl: string): string {
|
|
232
|
+
const normalized = persistAuthUrl(authUrl);
|
|
233
|
+
this.#config.authUrl = normalized;
|
|
234
|
+
return normalized;
|
|
136
235
|
}
|
|
137
236
|
|
|
138
237
|
get handle(): SessionKeyHandle | null {
|
|
139
238
|
return this.#state.handle;
|
|
140
239
|
}
|
|
240
|
+
get authUrl(): string | null {
|
|
241
|
+
return this.#getConfiguredAuthUrl();
|
|
242
|
+
}
|
|
243
|
+
get loginPath(): string {
|
|
244
|
+
return this.#config.loginPath ?? "/login";
|
|
245
|
+
}
|
|
246
|
+
get contract(): TrellisClientContract | undefined {
|
|
247
|
+
return this.#config.contract;
|
|
248
|
+
}
|
|
141
249
|
get sessionKey(): string | null {
|
|
142
250
|
return this.#state.handle ? getPublicSessionKey(this.#state.handle) : null;
|
|
143
251
|
}
|
|
@@ -169,6 +277,8 @@ export class AuthState {
|
|
|
169
277
|
async init(): Promise<SessionKeyHandle> {
|
|
170
278
|
if (this.#state.handle) return this.#state.handle;
|
|
171
279
|
|
|
280
|
+
this.#getConfiguredAuthUrl();
|
|
281
|
+
|
|
172
282
|
const handle = await getOrCreateSessionKey();
|
|
173
283
|
this.#state.handle = handle;
|
|
174
284
|
|
|
@@ -188,30 +298,28 @@ export class AuthState {
|
|
|
188
298
|
* Initiate OAuth sign-in flow by redirecting to the auth provider.
|
|
189
299
|
* This method does not return - it redirects the browser.
|
|
190
300
|
*/
|
|
191
|
-
async signIn(
|
|
301
|
+
async signIn(options: SignInOptions = {}): Promise<never> {
|
|
302
|
+
const authUrl = options.authUrl ? this.setAuthUrl(options.authUrl) : this.#requireAuthUrl();
|
|
192
303
|
const handle = await this.init();
|
|
193
|
-
const
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
redirectTo,
|
|
304
|
+
const currentUrl = new URL(window.location.href);
|
|
305
|
+
const url = await buildLoginUrl({
|
|
306
|
+
authUrl,
|
|
307
|
+
redirectTo: resolveRedirectTo(options, currentUrl),
|
|
197
308
|
handle,
|
|
198
|
-
this.#config.contract?.CONTRACT ?? {},
|
|
199
|
-
|
|
309
|
+
contract: this.#config.contract?.CONTRACT ?? {},
|
|
310
|
+
context: options.context,
|
|
311
|
+
});
|
|
200
312
|
window.location.href = url;
|
|
201
|
-
throw new Error(
|
|
313
|
+
throw new Error("Redirecting to auth for provider selection");
|
|
202
314
|
}
|
|
203
315
|
|
|
204
316
|
async handleCallback(url: string = window.location.href): Promise<BindResult | null> {
|
|
205
317
|
if (this.#bindingInProgress) return this.#bindingInProgress;
|
|
206
318
|
|
|
207
|
-
const
|
|
208
|
-
if (
|
|
209
|
-
if (authError) return { status: "error", message: authError };
|
|
210
|
-
|
|
211
|
-
const authToken = extractAuthTokenFromFragment(url);
|
|
212
|
-
if (!authToken) return null;
|
|
319
|
+
const flowId = new URL(url).searchParams.get("flowId");
|
|
320
|
+
if (!flowId) return null;
|
|
213
321
|
|
|
214
|
-
this.#bindingInProgress = this.#resolveCallback(
|
|
322
|
+
this.#bindingInProgress = this.#resolveCallback(flowId);
|
|
215
323
|
try {
|
|
216
324
|
return await this.#bindingInProgress;
|
|
217
325
|
} finally {
|
|
@@ -219,9 +327,9 @@ export class AuthState {
|
|
|
219
327
|
}
|
|
220
328
|
}
|
|
221
329
|
|
|
222
|
-
async #resolveCallback(
|
|
330
|
+
async #resolveCallback(flowId: string): Promise<BindResult> {
|
|
223
331
|
try {
|
|
224
|
-
const response = await this.
|
|
332
|
+
const response = await this.bindFlow(flowId);
|
|
225
333
|
return response.status === "bound"
|
|
226
334
|
? { status: "bound" }
|
|
227
335
|
: { status: "insufficient_capabilities", missingCapabilities: response.missingCapabilities };
|
|
@@ -234,12 +342,13 @@ export class AuthState {
|
|
|
234
342
|
}
|
|
235
343
|
|
|
236
344
|
/**
|
|
237
|
-
* Clean up the callback URL by removing the
|
|
345
|
+
* Clean up the callback URL by removing the auth flow query params.
|
|
238
346
|
*/
|
|
239
347
|
cleanupCallbackUrl(url: string = window.location.href): void {
|
|
240
348
|
const parsed = new URL(url);
|
|
241
|
-
if (parsed.
|
|
242
|
-
parsed.
|
|
349
|
+
if (parsed.searchParams.has("flowId") || parsed.searchParams.has("authError")) {
|
|
350
|
+
parsed.searchParams.delete("flowId");
|
|
351
|
+
parsed.searchParams.delete("authError");
|
|
243
352
|
window.history.replaceState({}, "", parsed.pathname + parsed.search);
|
|
244
353
|
}
|
|
245
354
|
}
|
|
@@ -255,7 +364,7 @@ export class AuthState {
|
|
|
255
364
|
async #bind(authToken: string): Promise<BindResponse> {
|
|
256
365
|
const handle = await this.init();
|
|
257
366
|
const response = await bindSession(
|
|
258
|
-
{ authUrl: this.#
|
|
367
|
+
{ authUrl: this.#requireAuthUrl() },
|
|
259
368
|
handle,
|
|
260
369
|
authToken,
|
|
261
370
|
);
|
|
@@ -267,6 +376,21 @@ export class AuthState {
|
|
|
267
376
|
return response;
|
|
268
377
|
}
|
|
269
378
|
|
|
379
|
+
async bindFlow(flowId: string): Promise<BindResponse> {
|
|
380
|
+
const handle = await this.init();
|
|
381
|
+
const response = await bindFlow(
|
|
382
|
+
{ authUrl: this.#requireAuthUrl() },
|
|
383
|
+
handle,
|
|
384
|
+
flowId,
|
|
385
|
+
);
|
|
386
|
+
|
|
387
|
+
if (response.status === "bound") {
|
|
388
|
+
this.setBindingToken(response);
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
return response;
|
|
392
|
+
}
|
|
393
|
+
|
|
270
394
|
setBindingToken(
|
|
271
395
|
response: Pick<BindSuccessResponse, "bindingToken" | "inboxPrefix" | "expires"> & {
|
|
272
396
|
sentinel?: SentinelCreds;
|
|
@@ -326,8 +450,7 @@ export class AuthState {
|
|
|
326
450
|
this.clearAuth();
|
|
327
451
|
this.#state.handle = null;
|
|
328
452
|
|
|
329
|
-
|
|
330
|
-
window.location.href = loginPath;
|
|
453
|
+
window.location.href = this.loginPath;
|
|
331
454
|
throw new Error("Signed out, redirecting to login");
|
|
332
455
|
}
|
|
333
456
|
}
|
package/src/state/nats.svelte.ts
CHANGED
|
@@ -3,20 +3,20 @@ import {
|
|
|
3
3
|
type NatsConnection,
|
|
4
4
|
wsconnect,
|
|
5
5
|
} from "@nats-io/nats-core";
|
|
6
|
-
import { createClient } from "@qlever-llc/trellis";
|
|
6
|
+
import { createClient, type Trellis } from "@qlever-llc/trellis";
|
|
7
7
|
import {
|
|
8
8
|
getPublicSessionKey,
|
|
9
9
|
natsConnectSigForBindingToken,
|
|
10
10
|
type SentinelCreds,
|
|
11
11
|
type SessionKeyHandle,
|
|
12
12
|
signBytes,
|
|
13
|
-
} from "@qlever-llc/trellis
|
|
14
|
-
import { AsyncResult,
|
|
13
|
+
} from "@qlever-llc/trellis/auth";
|
|
14
|
+
import { AsyncResult, UnexpectedError } from "@qlever-llc/result";
|
|
15
15
|
import {
|
|
16
16
|
API as AUTH_API,
|
|
17
17
|
type AuthRenewBindingTokenInput,
|
|
18
18
|
type AuthRenewBindingTokenOutput,
|
|
19
|
-
} from "@qlever-llc/trellis
|
|
19
|
+
} from "@qlever-llc/trellis/sdk/auth";
|
|
20
20
|
import type { AuthState } from "./auth.svelte.ts";
|
|
21
21
|
|
|
22
22
|
const AUTH_RENEW_API = {
|
|
@@ -25,19 +25,34 @@ const AUTH_RENEW_API = {
|
|
|
25
25
|
},
|
|
26
26
|
events: {},
|
|
27
27
|
subjects: {},
|
|
28
|
+
operations: {},
|
|
28
29
|
} as const;
|
|
29
30
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
31
|
+
const AUTH_RENEW_CONTRACT = {
|
|
32
|
+
API: {
|
|
33
|
+
trellis: AUTH_RENEW_API,
|
|
34
|
+
},
|
|
35
|
+
} as const;
|
|
36
|
+
|
|
37
|
+
function createAuthRenewClient(
|
|
38
|
+
nc: NatsConnection,
|
|
39
|
+
handle: SessionKeyHandle,
|
|
40
|
+
): Trellis<typeof AUTH_RENEW_API> {
|
|
41
|
+
return createClient<typeof AUTH_RENEW_API>(
|
|
42
|
+
AUTH_RENEW_CONTRACT,
|
|
43
|
+
nc,
|
|
44
|
+
{
|
|
45
|
+
sessionKey: getPublicSessionKey(handle),
|
|
46
|
+
sign: (data: Uint8Array) => signBytes(handle, data),
|
|
47
|
+
},
|
|
48
|
+
{ name: "auth-renew" },
|
|
49
|
+
);
|
|
35
50
|
};
|
|
36
51
|
|
|
37
52
|
export type Status = "disconnected" | "connecting" | "connected" | "error";
|
|
38
53
|
|
|
39
54
|
export type NatsStateConfig = {
|
|
40
|
-
servers
|
|
55
|
+
servers?: string[];
|
|
41
56
|
onConnecting?: () => void;
|
|
42
57
|
onConnected?: () => void;
|
|
43
58
|
onDisconnect?: () => void;
|
|
@@ -63,19 +78,15 @@ function requireBrowserAuth(authState: AuthState): {
|
|
|
63
78
|
return { handle, bindingToken, sentinel };
|
|
64
79
|
}
|
|
65
80
|
|
|
66
|
-
function
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
)
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
sign: (data: Uint8Array) => signBytes(handle, data),
|
|
76
|
-
},
|
|
77
|
-
{ name: "auth-renew" },
|
|
78
|
-
) as unknown as AuthRenewClient;
|
|
81
|
+
function resolveServers(
|
|
82
|
+
authState: AuthState,
|
|
83
|
+
config: NatsStateConfig,
|
|
84
|
+
): string[] {
|
|
85
|
+
const servers = config.servers ?? authState.natsServers;
|
|
86
|
+
if (!servers || servers.length === 0) {
|
|
87
|
+
throw new Error("Not authenticated: missing natsServers from auth state");
|
|
88
|
+
}
|
|
89
|
+
return servers;
|
|
79
90
|
}
|
|
80
91
|
|
|
81
92
|
async function buildNatsAuthToken(
|
|
@@ -105,7 +116,7 @@ export class NatsState {
|
|
|
105
116
|
#handle: SessionKeyHandle;
|
|
106
117
|
#tokenRef: { value: string };
|
|
107
118
|
#sentinel: SentinelCreds;
|
|
108
|
-
#trellis:
|
|
119
|
+
#trellis: ReturnType<typeof createAuthRenewClient>;
|
|
109
120
|
#renewTimer: ReturnType<typeof setTimeout> | undefined;
|
|
110
121
|
|
|
111
122
|
private constructor(
|
|
@@ -151,6 +162,7 @@ export class NatsState {
|
|
|
151
162
|
}
|
|
152
163
|
|
|
153
164
|
const { handle, bindingToken, sentinel } = requireBrowserAuth(authState);
|
|
165
|
+
const servers = resolveServers(authState, config);
|
|
154
166
|
const inboxPrefix = authState.inboxPrefix ?? undefined;
|
|
155
167
|
const tokenRef = { value: await buildNatsAuthToken(handle, bindingToken) };
|
|
156
168
|
|
|
@@ -161,7 +173,7 @@ export class NatsState {
|
|
|
161
173
|
);
|
|
162
174
|
|
|
163
175
|
const nc = await wsconnect({
|
|
164
|
-
servers
|
|
176
|
+
servers,
|
|
165
177
|
authenticator,
|
|
166
178
|
token: tokenRef.value, // auth_token for auth callout
|
|
167
179
|
reconnect: true,
|
|
@@ -175,7 +187,7 @@ export class NatsState {
|
|
|
175
187
|
const state = new NatsState(
|
|
176
188
|
nc,
|
|
177
189
|
"connected",
|
|
178
|
-
|
|
190
|
+
servers,
|
|
179
191
|
authState,
|
|
180
192
|
config,
|
|
181
193
|
handle,
|
|
@@ -258,13 +270,14 @@ export class NatsState {
|
|
|
258
270
|
async #renewBindingToken(): Promise<void> {
|
|
259
271
|
const renew = async () => {
|
|
260
272
|
if (this.status !== "connected") return;
|
|
261
|
-
const
|
|
273
|
+
const requestOrThrow = this.#trellis.requestOrThrow.bind(this.#trellis) as (
|
|
274
|
+
method: string,
|
|
275
|
+
input: unknown,
|
|
276
|
+
) => Promise<unknown>;
|
|
277
|
+
const binding = await requestOrThrow(
|
|
262
278
|
"Auth.RenewBindingToken",
|
|
263
279
|
{} satisfies AuthRenewBindingTokenInput,
|
|
264
|
-
);
|
|
265
|
-
const v = res.take();
|
|
266
|
-
if (isErr(v)) return;
|
|
267
|
-
const binding = v as AuthRenewBindingTokenOutput;
|
|
280
|
+
) as AuthRenewBindingTokenOutput;
|
|
268
281
|
this.#authState.setBindingToken(binding);
|
|
269
282
|
this.#tokenRef.value = await buildNatsAuthToken(
|
|
270
283
|
this.#handle,
|
|
@@ -1,12 +1,16 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
1
|
+
import {
|
|
2
|
+
createClient,
|
|
3
|
+
defineContract,
|
|
4
|
+
type Trellis,
|
|
5
|
+
type TrellisAPI,
|
|
6
|
+
type TrellisContractV1,
|
|
7
|
+
} from "@qlever-llc/trellis";
|
|
8
|
+
import { getPublicSessionKey, signBytes } from "@qlever-llc/trellis/auth";
|
|
5
9
|
import type { AuthState } from "./auth.svelte.ts";
|
|
6
10
|
import type { NatsState } from "./nats.svelte.ts";
|
|
7
11
|
|
|
8
12
|
export type TrellisClientContract<TApi extends TrellisAPI = TrellisAPI> = {
|
|
9
|
-
CONTRACT:
|
|
13
|
+
CONTRACT: TrellisContractV1;
|
|
10
14
|
CONTRACT_DIGEST: string;
|
|
11
15
|
API: {
|
|
12
16
|
trellis: TApi;
|
|
@@ -14,7 +18,6 @@ export type TrellisClientContract<TApi extends TrellisAPI = TrellisAPI> = {
|
|
|
14
18
|
};
|
|
15
19
|
|
|
16
20
|
export type TrellisStateConfig<TApi extends TrellisAPI = TrellisAPI> = {
|
|
17
|
-
serviceName: string;
|
|
18
21
|
contract?: TrellisClientContract<TApi>;
|
|
19
22
|
};
|
|
20
23
|
|
|
@@ -22,8 +25,8 @@ const DEFAULT_TRELLIS_CONTRACT = defineContract({
|
|
|
22
25
|
id: "trellis.svelte.browser@v1",
|
|
23
26
|
displayName: "Trellis Svelte Browser Client",
|
|
24
27
|
description: "Represent a browser client that only uses its locally declared Trellis APIs.",
|
|
25
|
-
kind: "
|
|
26
|
-
})
|
|
28
|
+
kind: "app",
|
|
29
|
+
}) satisfies TrellisClientContract<TrellisAPI>;
|
|
27
30
|
|
|
28
31
|
/**
|
|
29
32
|
* Svelte 5 wrapper for Trellis client.
|
|
@@ -32,33 +35,36 @@ const DEFAULT_TRELLIS_CONTRACT = defineContract({
|
|
|
32
35
|
* - Trellis client instance
|
|
33
36
|
* - Session-key based request signing
|
|
34
37
|
*/
|
|
35
|
-
export class TrellisState {
|
|
36
|
-
readonly trellis: Trellis<
|
|
38
|
+
export class TrellisState<TApi extends TrellisAPI = TrellisAPI> {
|
|
39
|
+
readonly trellis: Trellis<TApi>;
|
|
37
40
|
|
|
38
|
-
private constructor(trellis: Trellis<
|
|
41
|
+
private constructor(trellis: Trellis<TApi>) {
|
|
39
42
|
this.trellis = trellis;
|
|
40
43
|
}
|
|
41
44
|
|
|
42
45
|
/**
|
|
43
46
|
* Create a TrellisState instance with proper authentication.
|
|
44
47
|
*/
|
|
45
|
-
static async create<TApi extends TrellisAPI
|
|
48
|
+
static async create<TApi extends TrellisAPI>(
|
|
46
49
|
authState: AuthState,
|
|
47
50
|
natsState: NatsState,
|
|
48
51
|
config: TrellisStateConfig<TApi>,
|
|
49
|
-
): Promise<TrellisState
|
|
52
|
+
): Promise<TrellisState<TApi>> {
|
|
50
53
|
const handle = await authState.init();
|
|
51
54
|
const contract = (config.contract ?? DEFAULT_TRELLIS_CONTRACT) as TrellisClientContract<TApi>;
|
|
52
|
-
const
|
|
55
|
+
const clientName = typeof contract.CONTRACT.id === "string" && contract.CONTRACT.id.length > 0
|
|
56
|
+
? contract.CONTRACT.id
|
|
57
|
+
: "client";
|
|
58
|
+
const trellis = createClient<TApi>(
|
|
53
59
|
contract,
|
|
54
60
|
natsState.nc,
|
|
55
61
|
{
|
|
56
62
|
sessionKey: getPublicSessionKey(handle),
|
|
57
63
|
sign: (data: Uint8Array) => signBytes(handle, data),
|
|
58
64
|
},
|
|
59
|
-
{ name:
|
|
60
|
-
)
|
|
61
|
-
return new TrellisState(trellis);
|
|
65
|
+
{ name: clientName },
|
|
66
|
+
);
|
|
67
|
+
return new TrellisState<TApi>(trellis);
|
|
62
68
|
}
|
|
63
69
|
|
|
64
70
|
stop(): void {
|
|
@@ -69,10 +75,10 @@ export class TrellisState {
|
|
|
69
75
|
/**
|
|
70
76
|
* Factory function to create a TrellisState instance.
|
|
71
77
|
*/
|
|
72
|
-
export async function createTrellisState<TApi extends TrellisAPI
|
|
78
|
+
export async function createTrellisState<TApi extends TrellisAPI>(
|
|
73
79
|
authState: AuthState,
|
|
74
80
|
natsState: NatsState,
|
|
75
81
|
config: TrellisStateConfig<TApi>,
|
|
76
|
-
): Promise<TrellisState
|
|
77
|
-
return TrellisState.create(authState, natsState, config);
|
|
82
|
+
): Promise<TrellisState<TApi>> {
|
|
83
|
+
return TrellisState.create<TApi>(authState, natsState, config);
|
|
78
84
|
}
|