@qlever-llc/trellis-svelte 0.8.0 → 0.8.2

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.
@@ -0,0 +1,24 @@
1
+ <script lang="ts">
2
+ import type { Snippet } from "svelte";
3
+ import {
4
+ provideConnectedTrellisContext,
5
+ type TrellisAppOwner,
6
+ type TrellisContextClient,
7
+ } from "../context.js";
8
+
9
+ type Props = {
10
+ trellisApp: TrellisAppOwner;
11
+ trellis: TrellisContextClient;
12
+ children: Snippet;
13
+ };
14
+
15
+ const { trellisApp, trellis, children }: Props = $props();
16
+
17
+ function installContext(): void {
18
+ provideConnectedTrellisContext(trellisApp, trellis);
19
+ }
20
+
21
+ installContext();
22
+ </script>
23
+
24
+ {@render children()}
@@ -0,0 +1,144 @@
1
+ <script lang="ts" generics="TContract extends TrellisContractLike">
2
+ import {
3
+ ClientAuthHandledError,
4
+ TrellisClient,
5
+ type ClientAuthOptions,
6
+ type ConnectedTrellisClient,
7
+ } from "@qlever-llc/trellis";
8
+ import { onMount } from "svelte";
9
+ import type { TrellisContractLike } from "../context.js";
10
+ import { resolveTrellisAppUrl } from "../context.js";
11
+ import TrellisContextProvider from "./TrellisContextProvider.svelte";
12
+ import type { TrellisProviderProps } from "./TrellisProvider.types.js";
13
+
14
+ const {
15
+ trellisApp,
16
+ auth,
17
+ client,
18
+ children,
19
+ loading,
20
+ error: errorSnippet,
21
+ onAuthRequired,
22
+ }: TrellisProviderProps<TContract> = $props();
23
+
24
+ let trellis = $state<ConnectedTrellisClient<TContract> | null>(null);
25
+ let connectError = $state<unknown>(null);
26
+
27
+ type SerializableTrellisError = {
28
+ message?: unknown;
29
+ code?: unknown;
30
+ hint?: unknown;
31
+ context?: unknown;
32
+ };
33
+
34
+ function maybeSerializableError(
35
+ value: unknown,
36
+ ): SerializableTrellisError | undefined {
37
+ if (!value || typeof value !== "object" || !("toSerializable" in value)) {
38
+ return undefined;
39
+ }
40
+ const serialize = value.toSerializable;
41
+ if (typeof serialize !== "function") return undefined;
42
+ const serialized = serialize.call(value);
43
+ return serialized && typeof serialized === "object"
44
+ ? (serialized as SerializableTrellisError)
45
+ : undefined;
46
+ }
47
+
48
+ function contextRecord(value: unknown): Record<string, unknown> | undefined {
49
+ return value && typeof value === "object" && !Array.isArray(value)
50
+ ? (value as Record<string, unknown>)
51
+ : undefined;
52
+ }
53
+
54
+ function logConnectionError(error: unknown): void {
55
+ const serialized = maybeSerializableError(error);
56
+ const context = contextRecord(serialized?.context);
57
+ const causeMessage =
58
+ typeof context?.causeMessage === "string"
59
+ ? context.causeMessage
60
+ : undefined;
61
+ const message =
62
+ typeof serialized?.message === "string"
63
+ ? serialized.message
64
+ : error instanceof Error
65
+ ? error.message
66
+ : String(error);
67
+
68
+ console.error("Error:", error);
69
+ }
70
+
71
+ onMount(() => {
72
+ let active = true;
73
+
74
+ function withBrowserAuthDefaults(
75
+ authOptions: ClientAuthOptions | undefined,
76
+ ): ClientAuthOptions {
77
+ if (authOptions?.mode === "session_key") {
78
+ return authOptions;
79
+ }
80
+
81
+ return {
82
+ ...authOptions,
83
+ currentUrl:
84
+ authOptions?.currentUrl ?? (() => new URL(window.location.href)),
85
+ };
86
+ }
87
+
88
+ const connectAuth = withBrowserAuthDefaults(auth);
89
+ const trellisUrl = resolveTrellisAppUrl(trellisApp.trellisUrl);
90
+ if (!trellisUrl) {
91
+ connectError = new TypeError(
92
+ "Expected trellisApp to resolve a Trellis URL",
93
+ );
94
+ return;
95
+ }
96
+
97
+ void (async () => {
98
+ try {
99
+ const connected = await TrellisClient.connect({
100
+ ...client,
101
+ trellisUrl,
102
+ contract: trellisApp.contract,
103
+ auth: connectAuth,
104
+ onAuthRequired: onAuthRequired
105
+ ? async (ctx) => {
106
+ await onAuthRequired(ctx.loginUrl, ctx);
107
+ return { status: "handled" };
108
+ }
109
+ : undefined,
110
+ }).orThrow();
111
+
112
+ if (active) {
113
+ trellis = connected;
114
+ } else {
115
+ await connected.connection.close();
116
+ }
117
+ } catch (error) {
118
+ if (!active) return;
119
+ if (error instanceof ClientAuthHandledError) return;
120
+ logConnectionError(error);
121
+ connectError = error;
122
+ }
123
+ })();
124
+
125
+ return () => {
126
+ active = false;
127
+ const connected = trellis;
128
+ trellis = null;
129
+ if (connected) {
130
+ void connected.connection.close();
131
+ }
132
+ };
133
+ });
134
+ </script>
135
+
136
+ {#if trellis}
137
+ <TrellisContextProvider {trellisApp} {trellis} {children} />
138
+ {:else if connectError}
139
+ {#if errorSnippet}
140
+ {@render errorSnippet(connectError)}
141
+ {/if}
142
+ {:else if loading}
143
+ {@render loading()}
144
+ {/if}
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,117 @@
1
+ /* context.js generated by Svelte v5.55.5 */
2
+ import * as $ from 'svelte/internal/client';
3
+ import { createContext } from "svelte";
4
+ import { createSubscriber } from "svelte/reactivity";
5
+
6
+ export function resolveTrellisAppUrl(trellisUrl) {
7
+ const resolved = typeof trellisUrl === "function" ? trellisUrl() : trellisUrl;
8
+
9
+ return resolved?.toString();
10
+ }
11
+
12
+ /** Svelte-reactive adapter around a framework-neutral Trellis connection. */
13
+ export class SvelteTrellisConnection {
14
+ #connection;
15
+ #subscribe;
16
+
17
+ /** Creates a reactive connection adapter for a connected Trellis runtime. */
18
+ constructor(connection) {
19
+ this.#connection = connection;
20
+
21
+ this.#subscribe = createSubscriber((update) => {
22
+ return this.#connection.subscribe(() => update());
23
+ });
24
+ }
25
+
26
+ /** Latest connection status, reactive when read by Svelte effects or markup. */
27
+ get status() {
28
+ this.#subscribe();
29
+
30
+ return this.#connection.status;
31
+ }
32
+
33
+ /** Closes the underlying Trellis runtime connection. */
34
+ close() {
35
+ return this.#connection.close();
36
+ }
37
+ }
38
+
39
+ const provideTrellisContext = Symbol("provideTrellisContext");
40
+ const trellisAppOwnerBrand = Symbol("trellisAppOwner");
41
+
42
+ /** Internal app-scoped typed Svelte context implementation. */
43
+ class TrellisAppImpl {
44
+ [trellisAppOwnerBrand] = true;
45
+ #contract;
46
+ #trellisUrl;
47
+ #getContext;
48
+ #setContext;
49
+
50
+ /** Creates an app-scoped context owner for a specific Trellis contract. */
51
+ constructor(options) {
52
+ const { contract, trellisUrl } = options;
53
+
54
+ this.#contract = contract;
55
+ this.#trellisUrl = trellisUrl;
56
+
57
+ const [getContext, setContext] = createContext();
58
+
59
+ this.#getContext = getContext;
60
+ this.#setContext = setContext;
61
+ }
62
+
63
+ /** Contract used by this app context and by `TrellisProvider` connections. */
64
+ get contract() {
65
+ return this.#contract;
66
+ }
67
+
68
+ /** Trellis URL configuration used by `TrellisProvider` connections. */
69
+ get trellisUrl() {
70
+ return this.#trellisUrl;
71
+ }
72
+
73
+ /** Returns the contract-typed connected Trellis client from Svelte context synchronously. */
74
+ getTrellis() {
75
+ return this.#getContext().trellis;
76
+ }
77
+
78
+ /** Returns a Svelte-reactive adapter for the real Trellis connection. */
79
+ getConnection() {
80
+ return this.#getContext().connection;
81
+ }
82
+
83
+ /** Installs the connected Trellis runtime into Svelte context synchronously. */
84
+ [provideTrellisContext](trellis) {
85
+ this.#setContext({
86
+ trellis,
87
+ connection: new SvelteTrellisConnection(trellis.connection)
88
+ });
89
+ }
90
+ }
91
+
92
+ /**
93
+ * Creates an app-scoped typed Svelte context owner for a Trellis contract and URL.
94
+ *
95
+ * The optional `TClient` type parameter is a type-only facade over the connected
96
+ * runtime client. It should be a generated client facade for `options.contract`.
97
+ */
98
+ export function createTrellisApp(options) {
99
+ return new TrellisAppImpl(options);
100
+ }
101
+
102
+ function isTrellisAppImpl(app) {
103
+ return app instanceof TrellisAppImpl;
104
+ }
105
+
106
+ /**
107
+ * Internal provider helper that synchronously installs connected Trellis context.
108
+ *
109
+ * This is intentionally not exported from `src/index.ts`.
110
+ */
111
+ export function provideConnectedTrellisContext(app, trellis) {
112
+ if (!isTrellisAppImpl(app)) {
113
+ throw new TypeError("Expected an app created by createTrellisApp");
114
+ }
115
+
116
+ app[provideTrellisContext](trellis);
117
+ }
@@ -0,0 +1,19 @@
1
+ /* device_activation.js generated by Svelte v5.55.5 */
2
+ import * as $ from 'svelte/internal/client';
3
+
4
+ import {
5
+ createInitialDeviceActivationState,
6
+ DeviceActivationControllerCore
7
+ } from "./device_activation_controller.js";
8
+
9
+ export class DeviceActivationController extends DeviceActivationControllerCore {
10
+ constructor(config) {
11
+ const state = $.proxy(createInitialDeviceActivationState());
12
+
13
+ super(config, state);
14
+ }
15
+ }
16
+
17
+ export function createDeviceActivationController(config) {
18
+ return new DeviceActivationController(config);
19
+ }
@@ -0,0 +1,245 @@
1
+ import { clearPreservedDeviceActivationCallbackState, getPreservedDeviceActivationCallbackState, preserveDeviceActivationCallbackState, } from "./internal/callback_state.js";
2
+ import { buildDeviceActivationCallbackPath, buildDeviceActivationConnectAuthUrlState, cleanupDeviceActivationCallbackUrl, resolveDeviceActivationUrlState, } from "./internal/portal_url.js";
3
+ import { createDeviceActivationReadyView, createDeviceActivationSignInRequiredView, createInvalidDeviceActivationView, mapDeviceActivationFailure, mapDeviceActivationProgress, mapDeviceActivationTerminal, } from "./internal/activation_view.js";
4
+ function errorMessage(error) {
5
+ return error instanceof Error ? error.message : String(error);
6
+ }
7
+ function redirectErrorMessage(error) {
8
+ const message = errorMessage(error);
9
+ return message.startsWith("Redirecting to auth for provider selection")
10
+ ? null
11
+ : message;
12
+ }
13
+ function bindErrorMessage(result) {
14
+ if (result.status === "bound")
15
+ return null;
16
+ if (result.status === "approval_denied")
17
+ return "Portal access was denied.";
18
+ if (result.status === "approval_required") {
19
+ return "Approval is still pending.";
20
+ }
21
+ if (result.status === "insufficient_capabilities") {
22
+ return `Missing capabilities: ${result.missingCapabilities.join(", ")}`;
23
+ }
24
+ return result.message;
25
+ }
26
+ function defaultGetUrl() {
27
+ return new URL(globalThis.location.href);
28
+ }
29
+ function defaultReplaceUrl(url) {
30
+ globalThis.history.replaceState(globalThis.history.state, "", url);
31
+ }
32
+ function defaultCreateCallbackToken() {
33
+ return crypto.randomUUID();
34
+ }
35
+ export function createInitialDeviceActivationState() {
36
+ return {
37
+ loading: true,
38
+ requestPending: false,
39
+ authError: null,
40
+ view: null,
41
+ flowId: null,
42
+ };
43
+ }
44
+ export class DeviceActivationControllerCore {
45
+ state;
46
+ #authState;
47
+ #createClient;
48
+ #getUrl;
49
+ #replaceUrl;
50
+ #sessionStorage;
51
+ #createCallbackToken;
52
+ #client = null;
53
+ #observationRunId = 0;
54
+ constructor(config, state = createInitialDeviceActivationState()) {
55
+ this.state = state;
56
+ this.#authState = config.authState;
57
+ this.#createClient = config.createClient;
58
+ this.#getUrl = config.getUrl ?? defaultGetUrl;
59
+ this.#replaceUrl = config.replaceUrl ?? defaultReplaceUrl;
60
+ this.#sessionStorage = config.sessionStorage ?? null;
61
+ this.#createCallbackToken = config.createCallbackToken ??
62
+ defaultCreateCallbackToken;
63
+ }
64
+ get loading() {
65
+ return this.state.loading;
66
+ }
67
+ get requestPending() {
68
+ return this.state.requestPending;
69
+ }
70
+ get authError() {
71
+ return this.state.authError;
72
+ }
73
+ get view() {
74
+ return this.state.view;
75
+ }
76
+ get flowId() {
77
+ return this.state.flowId;
78
+ }
79
+ async load() {
80
+ this.stop();
81
+ this.state.loading = true;
82
+ this.state.authError = null;
83
+ this.state.view = null;
84
+ this.#client = null;
85
+ const currentUrl = this.#getUrl();
86
+ let clientUrl = currentUrl;
87
+ const preservedState = this.#sessionStorage
88
+ ? getPreservedDeviceActivationCallbackState(this.#sessionStorage)
89
+ : null;
90
+ const { flowId, isAuthCallback } = resolveDeviceActivationUrlState(currentUrl, preservedState);
91
+ this.state.flowId = flowId;
92
+ if (!flowId) {
93
+ this.state.view = createInvalidDeviceActivationView("Missing flow id.");
94
+ this.state.loading = false;
95
+ return;
96
+ }
97
+ try {
98
+ await this.#authState.init();
99
+ if (isAuthCallback) {
100
+ const bindResult = await this.#authState.handleCallback(currentUrl.toString());
101
+ const cleanedUrl = cleanupDeviceActivationCallbackUrl(currentUrl, flowId);
102
+ if (cleanedUrl) {
103
+ this.#replaceUrl(cleanedUrl);
104
+ clientUrl = new URL(cleanedUrl, currentUrl.origin);
105
+ }
106
+ if (this.#sessionStorage) {
107
+ clearPreservedDeviceActivationCallbackState(this.#sessionStorage);
108
+ }
109
+ const callbackAuthError = currentUrl.searchParams.get("authError");
110
+ if (callbackAuthError && !bindResult) {
111
+ this.state.authError = callbackAuthError;
112
+ this.state.view = createDeviceActivationSignInRequiredView(flowId);
113
+ return;
114
+ }
115
+ if (bindResult) {
116
+ const bindError = bindErrorMessage(bindResult);
117
+ if (bindError) {
118
+ this.state.authError = bindError;
119
+ this.state.view = createDeviceActivationSignInRequiredView(flowId);
120
+ return;
121
+ }
122
+ }
123
+ }
124
+ try {
125
+ this.#client = await this.#createClient(buildDeviceActivationConnectAuthUrlState(clientUrl));
126
+ this.state.view = createDeviceActivationReadyView(flowId);
127
+ }
128
+ catch (error) {
129
+ this.#client = null;
130
+ if (isAuthCallback) {
131
+ this.state.authError = errorMessage(error);
132
+ }
133
+ this.state.view = createDeviceActivationSignInRequiredView(flowId);
134
+ }
135
+ }
136
+ catch (error) {
137
+ this.state.authError = errorMessage(error);
138
+ this.state.view = createDeviceActivationSignInRequiredView(flowId);
139
+ }
140
+ finally {
141
+ this.state.loading = false;
142
+ }
143
+ }
144
+ stop() {
145
+ this.#observationRunId += 1;
146
+ }
147
+ async signIn() {
148
+ this.state.authError = null;
149
+ if (!this.state.flowId || !this.#sessionStorage)
150
+ return;
151
+ const callbackToken = this.#createCallbackToken();
152
+ preserveDeviceActivationCallbackState(this.#sessionStorage, {
153
+ flowId: this.state.flowId,
154
+ callbackToken,
155
+ });
156
+ try {
157
+ await this.#authState.signIn({
158
+ redirectTo: buildDeviceActivationCallbackPath(this.#getUrl(), callbackToken),
159
+ });
160
+ }
161
+ catch (error) {
162
+ const message = redirectErrorMessage(error);
163
+ if (message) {
164
+ this.state.authError = message;
165
+ }
166
+ }
167
+ }
168
+ async requestActivation() {
169
+ const flowId = this.state.flowId;
170
+ if (!flowId || !this.#client)
171
+ return;
172
+ this.stop();
173
+ const runId = this.#observationRunId;
174
+ this.state.requestPending = true;
175
+ this.state.authError = null;
176
+ try {
177
+ const operation = await this.#client.activateDevice({ flowId });
178
+ const watch = await operation.watch().match({
179
+ ok: (value) => value,
180
+ err: () => null,
181
+ });
182
+ const watchPromise = watch
183
+ ? this.#observeWatch(flowId, watch, runId)
184
+ : Promise.resolve(false);
185
+ const terminal = await operation.wait().orThrow();
186
+ const handledByWatch = await watchPromise;
187
+ if (!handledByWatch && this.#isRunActive(runId)) {
188
+ this.#applyTerminal(flowId, terminal);
189
+ }
190
+ }
191
+ catch (error) {
192
+ if (!this.#isRunActive(runId))
193
+ return;
194
+ const nextView = mapDeviceActivationFailure(flowId, error);
195
+ if (nextView) {
196
+ this.state.view = nextView;
197
+ }
198
+ else {
199
+ this.state.view = createDeviceActivationReadyView(flowId);
200
+ this.state.authError = errorMessage(error);
201
+ }
202
+ }
203
+ finally {
204
+ if (this.#isRunActive(runId)) {
205
+ this.state.requestPending = false;
206
+ }
207
+ }
208
+ }
209
+ #isRunActive(runId) {
210
+ return this.#observationRunId === runId;
211
+ }
212
+ #applyTerminal(flowId, terminal) {
213
+ const view = mapDeviceActivationTerminal(flowId, terminal);
214
+ if (view) {
215
+ this.state.view = view;
216
+ return;
217
+ }
218
+ this.state.view = createDeviceActivationReadyView(flowId);
219
+ if (terminal.error?.message) {
220
+ this.state.authError = terminal.error.message;
221
+ }
222
+ }
223
+ async #observeWatch(flowId, watch, runId) {
224
+ const iterator = watch[Symbol.asyncIterator]();
225
+ while (true) {
226
+ const next = await iterator.next();
227
+ if (next.done) {
228
+ return false;
229
+ }
230
+ const event = next.value;
231
+ if (!this.#isRunActive(runId)) {
232
+ await iterator.return?.();
233
+ return false;
234
+ }
235
+ if (event.type === "completed" || event.type === "failed" ||
236
+ event.type === "cancelled") {
237
+ this.#applyTerminal(flowId, event.snapshot);
238
+ return true;
239
+ }
240
+ if (event.type === "progress") {
241
+ this.state.view = mapDeviceActivationProgress(flowId, event.progress);
242
+ }
243
+ }
244
+ }
245
+ }
package/dist/index.js ADDED
@@ -0,0 +1,4 @@
1
+ export { default as TrellisProvider } from "./components/TrellisProvider.svelte";
2
+ export { createTrellisApp, resolveTrellisAppUrl } from "./context.js";
3
+ export { createDeviceActivationController, DeviceActivationController, } from "./device_activation.js";
4
+ export { createPortalFlow, PortalFlowController, } from "./portal_flow.js";
@@ -0,0 +1,160 @@
1
+ function isDeviceActivationProgressInput(value) {
2
+ const record = value;
3
+ return record.status === "pending_review" &&
4
+ typeof record.instanceId === "string" &&
5
+ typeof record.deploymentId === "string" &&
6
+ typeof record.reviewId === "string" &&
7
+ (typeof record.requestedAt === "string" ||
8
+ record.requestedAt instanceof Date);
9
+ }
10
+ function isoString(value) {
11
+ return value instanceof Date ? value.toISOString() : value;
12
+ }
13
+ function errorMessage(error) {
14
+ if (error instanceof Error)
15
+ return error.message;
16
+ if (typeof error === "object" && error !== null && "message" in error) {
17
+ const message = Reflect.get(error, "message");
18
+ if (typeof message === "string")
19
+ return message;
20
+ }
21
+ return String(error);
22
+ }
23
+ function errorContext(error) {
24
+ if (typeof error !== "object" || error === null) {
25
+ return null;
26
+ }
27
+ if ("getContext" in error) {
28
+ const getContext = Reflect.get(error, "getContext");
29
+ if (typeof getContext === "function") {
30
+ const context = Reflect.apply(getContext, error, []);
31
+ return typeof context === "object" && context !== null
32
+ ? context
33
+ : null;
34
+ }
35
+ }
36
+ if (!("context" in error))
37
+ return null;
38
+ const context = Reflect.get(error, "context");
39
+ return typeof context === "object" && context !== null
40
+ ? context
41
+ : null;
42
+ }
43
+ function errorReason(error) {
44
+ if (typeof error !== "object" || error === null || !("reason" in error)) {
45
+ return undefined;
46
+ }
47
+ const reason = Reflect.get(error, "reason");
48
+ return typeof reason === "string" ? reason : undefined;
49
+ }
50
+ export function createDeviceActivationReadyView(flowId) {
51
+ return { mode: "ready", flowId };
52
+ }
53
+ export function createDeviceActivationSignInRequiredView(flowId) {
54
+ return { mode: "sign_in_required", flowId };
55
+ }
56
+ export function createInvalidDeviceActivationView(reason, flowId) {
57
+ return flowId
58
+ ? { mode: "invalid_flow", reason, flowId }
59
+ : { mode: "invalid_flow", reason };
60
+ }
61
+ export function mapDeviceActivationOutput(flowId, result) {
62
+ if (result.status === "activated") {
63
+ return {
64
+ mode: "activated",
65
+ flowId,
66
+ instanceId: result.instanceId,
67
+ deploymentId: result.deploymentId,
68
+ activatedAt: isoString(result.activatedAt),
69
+ ...(result.confirmationCode
70
+ ? { confirmationCode: result.confirmationCode }
71
+ : {}),
72
+ };
73
+ }
74
+ if (isDeviceActivationProgressInput(result)) {
75
+ return mapDeviceActivationProgress(flowId, result);
76
+ }
77
+ if (result.reason === "device_flow_expired") {
78
+ return {
79
+ mode: "expired",
80
+ flowId,
81
+ reason: "The activation request expired. Start again from the auth service.",
82
+ };
83
+ }
84
+ if (result.reason === "device_activation_revoked") {
85
+ return {
86
+ mode: "rejected",
87
+ flowId,
88
+ reason: "The activation request was revoked.",
89
+ };
90
+ }
91
+ if (result.reason === "activation_not_started") {
92
+ return createDeviceActivationReadyView(flowId);
93
+ }
94
+ return {
95
+ mode: "rejected",
96
+ flowId,
97
+ ...(result.reason ? { reason: result.reason } : {}),
98
+ };
99
+ }
100
+ export function mapDeviceActivationProgress(flowId, progress) {
101
+ return {
102
+ mode: "pending_review",
103
+ flowId,
104
+ instanceId: progress.instanceId,
105
+ deploymentId: progress.deploymentId,
106
+ reviewId: progress.reviewId,
107
+ requestedAt: isoString(progress.requestedAt),
108
+ };
109
+ }
110
+ export function mapDeviceActivationFailure(flowId, error) {
111
+ const message = errorMessage(error);
112
+ const context = errorContext(error);
113
+ const authReason = errorReason(error);
114
+ const reason = typeof context?.reason === "string"
115
+ ? context.reason
116
+ : authReason;
117
+ if (reason === "device_flow_not_found" ||
118
+ authReason === "device_activation_flow_not_found" ||
119
+ message.includes("device_flow_not_found") ||
120
+ message.includes("device_activation_flow_not_found")) {
121
+ return createInvalidDeviceActivationView("This activation link is no longer valid.", flowId);
122
+ }
123
+ if (reason === "device_flow_expired" ||
124
+ authReason === "device_activation_flow_expired" ||
125
+ message.includes("device_flow_expired") ||
126
+ message.includes("device_activation_flow_expired")) {
127
+ return {
128
+ mode: "expired",
129
+ flowId,
130
+ reason: "The activation request expired. Start again from the auth service.",
131
+ };
132
+ }
133
+ if (reason === "unknown_device" || authReason === "unknown_device" ||
134
+ message.includes("unknown_device")) {
135
+ return createInvalidDeviceActivationView("This activation link no longer matches a known device.", flowId);
136
+ }
137
+ if (reason === "device_deployment_not_found" ||
138
+ authReason === "device_deployment_not_found" ||
139
+ message.includes("device_deployment_not_found")) {
140
+ return createInvalidDeviceActivationView("This device deployment is no longer available.", flowId);
141
+ }
142
+ if (authReason === "invalid_request" || message.includes("invalid_request")) {
143
+ return createInvalidDeviceActivationView("Trellis rejected this activation request. Start again from the device.", flowId);
144
+ }
145
+ if (reason === "device_activation_revoked" ||
146
+ message.includes("device_activation_revoked")) {
147
+ return { mode: "rejected", flowId, reason };
148
+ }
149
+ return null;
150
+ }
151
+ export function mapDeviceActivationTerminal(flowId, terminal) {
152
+ if (terminal.state === "completed") {
153
+ return terminal.output
154
+ ? mapDeviceActivationOutput(flowId, terminal.output)
155
+ : null;
156
+ }
157
+ return terminal.error
158
+ ? mapDeviceActivationFailure(flowId, terminal.error)
159
+ : null;
160
+ }
@@ -0,0 +1,24 @@
1
+ const PRESERVED_ACTIVATION_FLOW_ID_STORAGE_KEY = "portal.activate.flowId";
2
+ const ACTIVATION_CALLBACK_TOKEN_STORAGE_KEY = "portal.activate.callbackToken";
3
+ const ACTIVATION_CALLBACK_QUERY_PARAM = "portalCallback";
4
+ export function getPreservedDeviceActivationCallbackState(storage) {
5
+ const flowId = storage.getItem(PRESERVED_ACTIVATION_FLOW_ID_STORAGE_KEY);
6
+ const callbackToken = storage.getItem(ACTIVATION_CALLBACK_TOKEN_STORAGE_KEY);
7
+ if (!flowId || !callbackToken)
8
+ return null;
9
+ return { flowId, callbackToken };
10
+ }
11
+ export function preserveDeviceActivationCallbackState(storage, nextState) {
12
+ storage.setItem(PRESERVED_ACTIVATION_FLOW_ID_STORAGE_KEY, nextState.flowId);
13
+ storage.setItem(ACTIVATION_CALLBACK_TOKEN_STORAGE_KEY, nextState.callbackToken);
14
+ }
15
+ export function clearPreservedDeviceActivationCallbackState(storage) {
16
+ storage.removeItem(PRESERVED_ACTIVATION_FLOW_ID_STORAGE_KEY);
17
+ storage.removeItem(ACTIVATION_CALLBACK_TOKEN_STORAGE_KEY);
18
+ }
19
+ export function isDeviceActivationAuthCallback(currentUrl, preservedState) {
20
+ if (!preservedState)
21
+ return false;
22
+ return currentUrl.searchParams.get(ACTIVATION_CALLBACK_QUERY_PARAM) ===
23
+ preservedState.callbackToken;
24
+ }
@@ -0,0 +1,57 @@
1
+ import { isDeviceActivationAuthCallback } from "./callback_state.js";
2
+ const ACTIVATION_CALLBACK_QUERY_PARAM = "portalCallback";
3
+ const AUTH_ERROR_QUERY_PARAM = "authError";
4
+ const DEVICE_FLOW_ID_QUERY_PARAM = "deviceFlowId";
5
+ const FLOW_ID_QUERY_PARAM = "flowId";
6
+ export function buildDeviceActivationCallbackPath(currentUrl, callbackToken) {
7
+ const callbackUrl = new URL(currentUrl.pathname, currentUrl.origin);
8
+ callbackUrl.searchParams.set(ACTIVATION_CALLBACK_QUERY_PARAM, callbackToken);
9
+ const flowId = currentUrl.searchParams.get(FLOW_ID_QUERY_PARAM);
10
+ if (flowId) {
11
+ callbackUrl.searchParams.set(DEVICE_FLOW_ID_QUERY_PARAM, flowId);
12
+ }
13
+ callbackUrl.hash = currentUrl.hash;
14
+ return `${callbackUrl.pathname}${callbackUrl.search}${callbackUrl.hash}`;
15
+ }
16
+ export function buildDeviceActivationConnectAuthUrlState(currentUrl) {
17
+ const redirectUrl = new URL(currentUrl);
18
+ redirectUrl.searchParams.delete(ACTIVATION_CALLBACK_QUERY_PARAM);
19
+ redirectUrl.searchParams.delete(AUTH_ERROR_QUERY_PARAM);
20
+ redirectUrl.searchParams.delete(DEVICE_FLOW_ID_QUERY_PARAM);
21
+ const authCurrentUrl = new URL(redirectUrl);
22
+ authCurrentUrl.searchParams.delete(DEVICE_FLOW_ID_QUERY_PARAM);
23
+ authCurrentUrl.searchParams.delete(FLOW_ID_QUERY_PARAM);
24
+ return {
25
+ currentUrl: authCurrentUrl,
26
+ redirectTo: redirectUrl.toString(),
27
+ };
28
+ }
29
+ export function cleanupDeviceActivationCallbackUrl(currentUrl, flowId) {
30
+ if (!currentUrl.searchParams.has(FLOW_ID_QUERY_PARAM) &&
31
+ !currentUrl.searchParams.has(AUTH_ERROR_QUERY_PARAM) &&
32
+ !currentUrl.searchParams.has(DEVICE_FLOW_ID_QUERY_PARAM) &&
33
+ !currentUrl.searchParams.has(ACTIVATION_CALLBACK_QUERY_PARAM)) {
34
+ return null;
35
+ }
36
+ const nextUrl = new URL(currentUrl);
37
+ nextUrl.searchParams.delete(FLOW_ID_QUERY_PARAM);
38
+ nextUrl.searchParams.delete(AUTH_ERROR_QUERY_PARAM);
39
+ nextUrl.searchParams.delete(DEVICE_FLOW_ID_QUERY_PARAM);
40
+ nextUrl.searchParams.delete(ACTIVATION_CALLBACK_QUERY_PARAM);
41
+ if (flowId) {
42
+ nextUrl.searchParams.set(FLOW_ID_QUERY_PARAM, flowId);
43
+ }
44
+ return `${nextUrl.pathname}${nextUrl.search}${nextUrl.hash}`;
45
+ }
46
+ export function resolveDeviceActivationUrlState(currentUrl, preservedState) {
47
+ const callbackFlowId = currentUrl.searchParams.get(DEVICE_FLOW_ID_QUERY_PARAM);
48
+ const isAuthCallback = isDeviceActivationAuthCallback(currentUrl, preservedState) ||
49
+ (currentUrl.searchParams.has(ACTIVATION_CALLBACK_QUERY_PARAM) &&
50
+ !!callbackFlowId);
51
+ return {
52
+ flowId: isAuthCallback
53
+ ? preservedState?.flowId ?? callbackFlowId
54
+ : currentUrl.searchParams.get(FLOW_ID_QUERY_PARAM),
55
+ isAuthCallback,
56
+ };
57
+ }
@@ -0,0 +1,143 @@
1
+ /* portal_flow.js generated by Svelte v5.55.5 */
2
+ import * as $ from 'svelte/internal/client';
3
+
4
+ import {
5
+ fetchPortalFlowState,
6
+ portalFlowIdFromUrl,
7
+ portalProviderLoginUrl,
8
+ submitPortalApproval
9
+ } from "@qlever-llc/trellis/auth/browser";
10
+
11
+ function errorMessage(error) {
12
+ return error instanceof Error ? error.message : String(error);
13
+ }
14
+
15
+ function defaultGetUrl() {
16
+ return new URL(globalThis.location.href);
17
+ }
18
+
19
+ export class PortalFlowController {
20
+ #flowId = $.state(null);
21
+
22
+ get flowId() {
23
+ return $.get(this.#flowId);
24
+ }
25
+
26
+ set flowId(value) {
27
+ $.set(this.#flowId, value, true);
28
+ }
29
+
30
+ #state = $.state(null);
31
+
32
+ get state() {
33
+ return $.get(this.#state);
34
+ }
35
+
36
+ set state(value) {
37
+ $.set(this.#state, value, true);
38
+ }
39
+
40
+ #loading = $.state(false);
41
+
42
+ get loading() {
43
+ return $.get(this.#loading);
44
+ }
45
+
46
+ set loading(value) {
47
+ $.set(this.#loading, value, true);
48
+ }
49
+
50
+ #error = $.state(null);
51
+
52
+ get error() {
53
+ return $.get(this.#error);
54
+ }
55
+
56
+ set error(value) {
57
+ $.set(this.#error, value, true);
58
+ }
59
+
60
+ #config;
61
+ #getUrl;
62
+
63
+ constructor(config) {
64
+ this.#config = { authUrl: config.authUrl };
65
+ this.#getUrl = config.getUrl ?? defaultGetUrl;
66
+ }
67
+
68
+ async load() {
69
+ this.loading = true;
70
+ this.error = null;
71
+ this.state = null;
72
+
73
+ try {
74
+ const flowId = portalFlowIdFromUrl(this.#getUrl());
75
+
76
+ this.flowId = flowId;
77
+
78
+ if (!flowId) {
79
+ this.error = "Missing flow id.";
80
+
81
+ return null;
82
+ }
83
+
84
+ const state = await fetchPortalFlowState(this.#config, flowId);
85
+
86
+ this.state = state;
87
+
88
+ return state;
89
+ } catch(error) {
90
+ this.error = errorMessage(error);
91
+ this.state = null;
92
+
93
+ return null;
94
+ } finally {
95
+ this.loading = false;
96
+ }
97
+ }
98
+
99
+ providerUrl(providerId) {
100
+ if (!this.flowId) {
101
+ throw new Error("Missing flow id.");
102
+ }
103
+
104
+ return portalProviderLoginUrl(this.#config, providerId, this.flowId);
105
+ }
106
+
107
+ async approve() {
108
+ return this.#submit("approved");
109
+ }
110
+
111
+ async deny() {
112
+ return this.#submit("denied");
113
+ }
114
+
115
+ async #submit(decision) {
116
+ if (!this.flowId) {
117
+ this.error = "Missing flow id.";
118
+
119
+ return null;
120
+ }
121
+
122
+ this.loading = true;
123
+ this.error = null;
124
+
125
+ try {
126
+ const state = await submitPortalApproval(this.#config, this.flowId, decision);
127
+
128
+ this.state = state;
129
+
130
+ return state;
131
+ } catch(error) {
132
+ this.error = errorMessage(error);
133
+
134
+ return null;
135
+ } finally {
136
+ this.loading = false;
137
+ }
138
+ }
139
+ }
140
+
141
+ export function createPortalFlow(config) {
142
+ return new PortalFlowController(config);
143
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@qlever-llc/trellis-svelte",
3
- "version": "0.8.0",
3
+ "version": "0.8.2",
4
4
  "type": "module",
5
5
  "description": "Svelte components and state helpers for Trellis browser applications.",
6
6
  "license": "Apache-2.0",
@@ -16,24 +16,25 @@
16
16
  "access": "public"
17
17
  },
18
18
  "files": [
19
+ "dist",
19
20
  "src",
20
21
  "README.md"
21
22
  ],
22
23
  "exports": {
23
24
  ".": {
24
25
  "types": "./src/index.ts",
25
- "svelte": "./src/index.ts",
26
- "default": "./src/index.ts"
26
+ "svelte": "./dist/index.js",
27
+ "default": "./dist/index.js"
27
28
  }
28
29
  },
29
30
  "dependencies": {
30
31
  "@nats-io/nats-core": "^3.3.1",
31
- "@qlever-llc/result": "^0.8.0",
32
- "@qlever-llc/trellis": "^0.8.0",
32
+ "@qlever-llc/result": "^0.8.2",
33
+ "@qlever-llc/trellis": "^0.8.2",
33
34
  "typebox": "^1.0.15"
34
35
  },
35
36
  "peerDependencies": {
36
37
  "svelte": "^5.0.0"
37
38
  },
38
- "svelte": "./src/index.ts"
39
+ "svelte": "./dist/index.js"
39
40
  }