@qlever-llc/trellis-svelte 0.6.1 → 0.7.0-rc.3

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 CHANGED
@@ -4,6 +4,10 @@ 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 `createTrellisApp(...)` for app-scoped auth creation and typed `app.getTrellis()` access without passing the contract around at every call site.
7
+ Use `TrellisProvider` as the primary integration surface:
8
+
9
+ - pass `trellisUrl`, `contract`, and `loginPath`
10
+ - read the live client with `getTrellis<typeof contract>()`
11
+ - use `createAuthState(...)` only for lower-level sign-in flows when needed
8
12
 
9
13
  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.6.1",
3
+ "version": "0.7.0-rc.3",
4
4
  "type": "module",
5
5
  "description": "Svelte components and state helpers for Trellis browser applications.",
6
6
  "license": "Apache-2.0",
@@ -28,7 +28,7 @@
28
28
  },
29
29
  "dependencies": {
30
30
  "@nats-io/nats-core": "^3.3.1",
31
- "@qlever-llc/trellis": "^0.6.1",
31
+ "@qlever-llc/trellis": "^0.7.0-rc.3",
32
32
  "typebox": "^1.0.15"
33
33
  },
34
34
  "peerDependencies": {
@@ -1,27 +1,28 @@
1
1
  <script lang="ts">
2
2
  import type { NatsConnection } from "@nats-io/nats-core";
3
3
  import { AsyncResult } from "@qlever-llc/result";
4
+ import type { Trellis, TrellisAPI } from "@qlever-llc/trellis";
4
5
  import { onDestroy } from "svelte";
5
6
  import type { Snippet } from "svelte";
6
7
  import {
7
- setAppContext,
8
8
  setAuthContext,
9
9
  setNatsStateContext,
10
10
  setTrellisContext,
11
11
  } from "../context.svelte.ts";
12
- import type { AuthState, BindErrorResult } from "../state/auth.svelte.ts";
13
- import { createNatsState, type NatsState } from "../state/nats.svelte.ts";
14
- import { createTrellisState, type TrellisState } from "../state/trellis.svelte.ts";
15
-
16
- type TrellisApp = {
17
- auth: AuthState;
18
- };
12
+ import { createAuthState, type BindErrorResult } from "../state/auth.svelte.ts";
13
+ import { createConnectedNatsState, type NatsState } from "../state/nats.svelte.ts";
14
+ import {
15
+ type TrellisClientContract,
16
+ } from "../state/trellis.svelte.ts";
17
+ import { TrellisClient } from "@qlever-llc/trellis";
19
18
 
20
19
  type Props = {
21
20
  children: Snippet;
22
21
  loading?: Snippet;
23
22
  bindError?: Snippet<[BindErrorResult]>;
24
- app: TrellisApp;
23
+ trellisUrl: string;
24
+ loginPath?: string;
25
+ contract: TrellisClientContract;
25
26
  onAuthExpired?: () => void;
26
27
  onAuthFailed?: (error: unknown) => void;
27
28
  onAuthRequired?: (redirectTo: string) => void;
@@ -38,7 +39,9 @@
38
39
  children,
39
40
  loading,
40
41
  bindError,
41
- app,
42
+ trellisUrl,
43
+ loginPath,
44
+ contract,
42
45
  onAuthExpired,
43
46
  onAuthFailed,
44
47
  onAuthRequired,
@@ -52,14 +55,35 @@
52
55
  }: Props = $props();
53
56
 
54
57
  let bindErrorResult = $state<BindErrorResult | null>(null);
58
+ let providerAuth = $state<ReturnType<typeof createAuthState> | null>(null);
59
+
60
+ function getProviderAuth() {
61
+ if (providerAuth === null) {
62
+ providerAuth = createAuthState({ authUrl: trellisUrl, loginPath, contract });
63
+ }
64
+
65
+ providerAuth.setAuthUrl(trellisUrl);
66
+
67
+ return providerAuth;
68
+ }
55
69
 
56
70
  type InitContext = {
57
- auth: AuthState;
71
+ auth: ReturnType<typeof createAuthState>;
58
72
  nats: NatsState;
59
- trellis: TrellisState;
73
+ trellis: Trellis<TrellisAPI>;
60
74
  };
61
75
  const isBrowser = typeof window !== "undefined";
62
76
 
77
+ function requireTrellisUrl(): string {
78
+ const auth = getProviderAuth();
79
+ const resolvedTrellisUrl = auth.authUrl;
80
+ if (!resolvedTrellisUrl) {
81
+ throw new Error("Trellis URL is not configured");
82
+ }
83
+
84
+ return resolvedTrellisUrl;
85
+ }
86
+
63
87
  function getRedirectTo(): string {
64
88
  if (typeof window === "undefined") {
65
89
  return "/";
@@ -71,7 +95,8 @@
71
95
  function redirectToLogin(redirectTo: string): void {
72
96
  if (typeof window === "undefined") return;
73
97
 
74
- const url = new URL(app.auth.loginPath, window.location.origin);
98
+ const auth = getProviderAuth();
99
+ const url = new URL(auth.loginPath, window.location.origin);
75
100
  url.searchParams.set("redirectTo", redirectTo);
76
101
  window.location.href = url.toString();
77
102
  }
@@ -86,11 +111,10 @@
86
111
 
87
112
  async function initialize(): Promise<InitContext | null> {
88
113
  const result = await AsyncResult.try(async () => {
89
- const authState = app.auth;
114
+ const auth = getProviderAuth();
90
115
 
91
- await authState.init();
92
- const bindResult = await authState.handleCallback();
93
- authState.cleanupCallbackUrl();
116
+ await auth.init();
117
+ const bindResult = await auth.handleCallback();
94
118
 
95
119
  if (bindResult !== null && bindResult.status !== "bound") {
96
120
  bindErrorResult = bindResult;
@@ -98,12 +122,28 @@
98
122
  return null;
99
123
  }
100
124
 
101
- if (!authState.isAuthenticated) {
125
+ if (!auth.isAuthenticated) {
102
126
  handleAuthRequired();
103
127
  return null;
104
128
  }
105
129
 
106
- const natsState = await createNatsState(authState, {
130
+ const trellis = await TrellisClient.connect({
131
+ trellisUrl: requireTrellisUrl(),
132
+ contract,
133
+ auth: {
134
+ handle: auth.handle ?? undefined,
135
+ },
136
+ onAuthRequired: ({ loginUrl }) => {
137
+ onAuthExpired?.();
138
+ const redirectTo = getRedirectTo();
139
+ onAuthRequired?.(redirectTo);
140
+ if (!onAuthRequired && typeof window !== "undefined") {
141
+ window.location.href = loginUrl;
142
+ }
143
+ },
144
+ });
145
+
146
+ const natsState = await createConnectedNatsState(trellis.natsConnection, auth, {
107
147
  onConnecting: onNatsConnecting,
108
148
  onConnected: onNatsConnected,
109
149
  onDisconnect: onNatsDisconnect,
@@ -116,20 +156,16 @@
116
156
  },
117
157
  });
118
158
 
119
- const trellisState = await createTrellisState(authState, natsState, {
120
- contract: authState.contract,
121
- });
122
-
123
159
  return {
124
- auth: authState,
160
+ auth,
125
161
  nats: natsState,
126
- trellis: trellisState,
162
+ trellis,
127
163
  };
128
164
  });
129
165
 
130
166
  if (result.isErr()) {
131
167
  const error = result.error;
132
- app.auth.clearAuth();
168
+ getProviderAuth().clearAuth();
133
169
  onAuthFailed?.(error);
134
170
  throw error;
135
171
  }
@@ -150,13 +186,12 @@
150
186
  return ctx;
151
187
  }) ?? null;
152
188
  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;
189
+ const trellisPromise: Promise<Trellis<TrellisAPI>> | null = readyPromise?.then((ctx) => ctx.trellis) ?? null;
154
190
  const natsPromise: Promise<NatsConnection> | null = readyPromise?.then((ctx) => ctx.nats.nc) ?? null;
155
191
 
156
192
  if (readyPromise && natsStatePromise && trellisPromise && natsPromise) {
157
193
  void readyPromise.catch(() => {});
158
- setAppContext(() => app);
159
- setAuthContext(() => app.auth);
194
+ setAuthContext(() => getProviderAuth());
160
195
  setNatsStateContext(natsStatePromise);
161
196
  setTrellisContext({
162
197
  trellis: trellisPromise,
@@ -168,7 +203,6 @@
168
203
  if (!readyPromise) return;
169
204
 
170
205
  void readyPromise.then((ctx) => {
171
- ctx.trellis.stop();
172
206
  void ctx.nats.disconnect();
173
207
  });
174
208
  });
@@ -1,52 +1,23 @@
1
- import type { BaseError, Result } from "@qlever-llc/result";
2
1
  import type { NatsConnection } from "@nats-io/nats-core";
3
- import type { InferSchemaType, Trellis, TrellisAPI } from "@qlever-llc/trellis";
2
+ import type { TrellisAPI } from "@qlever-llc/trellis";
4
3
  import { createContext } from "svelte";
5
- import { createAuthState, type AuthState, type AuthStateConfig, type SignInOptions } from "./state/auth.svelte.ts";
4
+ import type { AuthState } from "./state/auth.svelte.ts";
6
5
  import type { NatsState } from "./state/nats.svelte.ts";
7
6
 
8
7
  type TrellisContext = {
9
- trellis: Promise<Trellis<TrellisAPI>>;
8
+ trellis: Promise<unknown>;
10
9
  nats: Promise<NatsConnection>;
11
10
  };
12
11
 
13
- type TrellisContractLike<TA extends TrellisAPI = TrellisAPI> = {
12
+ export type TrellisContractLike<TA extends TrellisAPI = TrellisAPI> = {
14
13
  API: {
15
14
  trellis: TA;
16
15
  };
17
16
  };
18
17
 
19
- type RequestOpts = {
20
- timeout?: number;
21
- };
22
-
23
- type TypedTrellis<TA extends TrellisAPI> = Omit<Trellis<TrellisAPI>, "request" | "requestOrThrow"> & {
24
- request<M extends keyof TA["rpc"] & string>(
25
- method: M,
26
- input: InferSchemaType<TA["rpc"][M]["input"]>,
27
- opts?: RequestOpts,
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"]>>;
44
- };
45
-
46
18
  const [getTrellisContext, setTrellisContextValue] = createContext<TrellisContext>();
47
19
  const [getNatsStateContext, setNatsStateContextValue] = createContext<Promise<NatsState>>();
48
20
  const [getAuthContext, setAuthContextValue] = createContext<() => AuthState>();
49
- const [getAppContext, setAppContextValue] = createContext<() => unknown>();
50
21
 
51
22
  export function setTrellisContext(
52
23
  ctx: TrellisContext,
@@ -62,31 +33,8 @@ export function setAuthContext(getAuth: () => AuthState): void {
62
33
  setAuthContextValue(getAuth);
63
34
  }
64
35
 
65
- export function setAppContext(getApp: () => unknown): void {
66
- setAppContextValue(getApp);
67
- }
68
-
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;
36
+ export function getTrellis<T = unknown>(): Promise<T> {
37
+ return getTrellisContext().trellis as Promise<T>;
90
38
  }
91
39
 
92
40
  export function getNats(): Promise<NatsConnection> {
package/src/index.ts CHANGED
@@ -1,9 +1,9 @@
1
1
  export { default as TrellisProvider } from "./components/TrellisProvider.svelte";
2
- export { createTrellisApp, getAuth, getNats, getNatsState, setTrellisContext } from "./context.svelte.ts";
3
- export type { BoundTrellisApp } from "./context.svelte.ts";
2
+ export { getAuth, getNats, getNatsState, getTrellis } from "./context.svelte.ts";
3
+ export type { TrellisContractLike } from "./context.svelte.ts";
4
4
  export { createPortalFlow, PortalFlowController, type CreatePortalFlowConfig } from "./portal_flow.svelte.ts";
5
5
  export { AuthState, type BindErrorResult, type BindResult, createAuthState, type SignInOptions } from "./state/auth.svelte.ts";
6
6
  export type { NatsStateConfig, Status as NatsStatus } from "./state/nats.svelte.ts";
7
- export { createNatsState, NatsState } from "./state/nats.svelte.ts";
7
+ export { createConnectedNatsState, createNatsState, NatsState } from "./state/nats.svelte.ts";
8
8
  export type { TrellisClientContract, TrellisStateConfig } from "./state/trellis.svelte.ts";
9
9
  export { createTrellisState, TrellisState } from "./state/trellis.svelte.ts";
@@ -344,13 +344,15 @@ export class AuthState {
344
344
  /**
345
345
  * Clean up the callback URL by removing the auth flow query params.
346
346
  */
347
- cleanupCallbackUrl(url: string = window.location.href): void {
347
+ cleanupCallbackUrl(url: string = window.location.href): string | null {
348
348
  const parsed = new URL(url);
349
349
  if (parsed.searchParams.has("flowId") || parsed.searchParams.has("authError")) {
350
350
  parsed.searchParams.delete("flowId");
351
351
  parsed.searchParams.delete("authError");
352
- window.history.replaceState({}, "", parsed.pathname + parsed.search);
352
+ return `${parsed.pathname}${parsed.search}${parsed.hash}`;
353
353
  }
354
+
355
+ return null;
354
356
  }
355
357
 
356
358
  /**
@@ -3,7 +3,7 @@ import {
3
3
  type NatsConnection,
4
4
  wsconnect,
5
5
  } from "@nats-io/nats-core";
6
- import { createClient, type Trellis } from "@qlever-llc/trellis";
6
+ import type { Trellis } from "@qlever-llc/trellis";
7
7
  import {
8
8
  getPublicSessionKey,
9
9
  natsConnectSigForBindingToken,
@@ -18,6 +18,7 @@ import {
18
18
  type AuthRenewBindingTokenOutput,
19
19
  } from "@qlever-llc/trellis/sdk/auth";
20
20
  import type { AuthState } from "./auth.svelte.ts";
21
+ import { createClient } from "../../../trellis/client.ts";
21
22
 
22
23
  const AUTH_RENEW_API = {
23
24
  rpc: {
@@ -202,6 +203,44 @@ export class NatsState {
202
203
  return state;
203
204
  }
204
205
 
206
+ static async fromConnection(
207
+ nc: NatsConnection,
208
+ authState: AuthState,
209
+ config: NatsStateConfig,
210
+ ): Promise<NatsState> {
211
+ config.onConnecting?.();
212
+
213
+ await authState.init();
214
+
215
+ if (!authState.isAuthenticated) {
216
+ config.onAuthRequired?.();
217
+ throw new Error("Not authenticated: missing binding token or sentinel");
218
+ }
219
+
220
+ const { handle, sentinel } = requireBrowserAuth(authState);
221
+ const servers = resolveServers(authState, config);
222
+ const bindingToken = authState.bindingToken;
223
+ if (!bindingToken) {
224
+ throw new Error("Not authenticated: missing binding token or sentinel");
225
+ }
226
+
227
+ const state = new NatsState(
228
+ nc,
229
+ "connected",
230
+ servers,
231
+ authState,
232
+ config,
233
+ handle,
234
+ { value: await buildNatsAuthToken(handle, bindingToken) },
235
+ sentinel,
236
+ );
237
+
238
+ config.onConnected?.();
239
+ await state.#renewBindingToken();
240
+
241
+ return state;
242
+ }
243
+
205
244
  /**
206
245
  * Reconnect to NATS with the existing binding token (if still valid).
207
246
  * Uses stored sentinel credentials with jwtAuthenticator per ADR.
@@ -361,3 +400,11 @@ export async function createNatsState(
361
400
  ): Promise<NatsState> {
362
401
  return NatsState.connect(authState, config);
363
402
  }
403
+
404
+ export async function createConnectedNatsState(
405
+ nc: NatsConnection,
406
+ authState: AuthState,
407
+ config: NatsStateConfig,
408
+ ): Promise<NatsState> {
409
+ return NatsState.fromConnection(nc, authState, config);
410
+ }
@@ -1,10 +1,11 @@
1
1
  import {
2
- createClient,
3
2
  defineContract,
4
3
  type Trellis,
5
4
  type TrellisAPI,
6
5
  type TrellisContractV1,
7
6
  } from "@qlever-llc/trellis";
7
+ import { TrellisClient } from "../../../trellis/client_connect.ts";
8
+ import { createClient } from "../../../trellis/client.ts";
8
9
  import { getPublicSessionKey, signBytes } from "@qlever-llc/trellis/auth";
9
10
  import type { AuthState } from "./auth.svelte.ts";
10
11
  import type { NatsState } from "./nats.svelte.ts";
@@ -67,6 +68,10 @@ export class TrellisState<TApi extends TrellisAPI = TrellisAPI> {
67
68
  return new TrellisState<TApi>(trellis);
68
69
  }
69
70
 
71
+ static fromTrellis<TApi extends TrellisAPI>(trellis: Trellis<TApi>): TrellisState<TApi> {
72
+ return new TrellisState<TApi>(trellis);
73
+ }
74
+
70
75
  stop(): void {
71
76
  // no-op (kept for convenience)
72
77
  }