@oatscenter/trellis-svelte 0.100.0-rc.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 +16 -0
- package/dist/authorization_context.d.ts +18 -0
- package/dist/authorization_context.js +150 -0
- package/dist/components/TrellisContextProvider.svelte +24 -0
- package/dist/components/TrellisContextProvider.svelte.d.ts +11 -0
- package/dist/components/TrellisProvider.svelte +192 -0
- package/dist/components/TrellisProvider.svelte.d.ts +6 -0
- package/dist/components/TrellisProvider.types.d.ts +15 -0
- package/dist/components/TrellisProvider.types.js +1 -0
- package/dist/context.d.ts +72 -0
- package/dist/context.js +116 -0
- package/dist/device_activation.d.ts +6 -0
- package/dist/device_activation.js +19 -0
- package/dist/device_activation_controller.d.ts +91 -0
- package/dist/device_activation_controller.js +318 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +5 -0
- package/dist/internal/activation_view.d.ts +44 -0
- package/dist/internal/activation_view.js +95 -0
- package/dist/internal/callback_state.d.ts +9 -0
- package/dist/internal/callback_state.js +24 -0
- package/dist/internal/portal_url.d.ts +13 -0
- package/dist/internal/portal_url.js +57 -0
- package/dist/portal_flow.d.ts +24 -0
- package/dist/portal_flow.js +173 -0
- package/package.json +42 -0
- package/src/authorization_context.svelte.ts +153 -0
- package/src/components/TrellisContextProvider.svelte +24 -0
- package/src/components/TrellisProvider.svelte +192 -0
- package/src/components/TrellisProvider.types.ts +28 -0
- package/src/context.svelte.ts +205 -0
- package/src/device_activation.svelte.ts +28 -0
- package/src/device_activation_controller.ts +528 -0
- package/src/index.ts +31 -0
- package/src/internal/activation_view.ts +179 -0
- package/src/internal/callback_state.ts +48 -0
- package/src/internal/portal_url.ts +94 -0
- package/src/portal_flow.svelte.ts +143 -0
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import type { AsyncResult, BaseError } from "@oatscenter/result";
|
|
2
|
+
import type { OperationEvent, TerminalOperation } from "@oatscenter/trellis";
|
|
3
|
+
import type { DeviceUserAuthoritiesResolveOutput, DeviceUserAuthoritiesResolveProgress } from "@oatscenter/trellis/auth";
|
|
4
|
+
import { type DeviceActivationView } from "./internal/activation_view.js";
|
|
5
|
+
import { type StorageLike } from "./internal/callback_state.js";
|
|
6
|
+
import { type DeviceActivationConnectAuthUrlState } from "./internal/portal_url.js";
|
|
7
|
+
export type DeviceActivationSignInOptions = {
|
|
8
|
+
redirectTo?: string;
|
|
9
|
+
landingPath?: string;
|
|
10
|
+
context?: unknown;
|
|
11
|
+
};
|
|
12
|
+
export type DeviceActivationBindResult = {
|
|
13
|
+
status: "bound";
|
|
14
|
+
} | {
|
|
15
|
+
status: "approval_denied";
|
|
16
|
+
} | {
|
|
17
|
+
status: "approval_required";
|
|
18
|
+
} | {
|
|
19
|
+
status: "insufficient_capabilities";
|
|
20
|
+
missingCapabilities: string[];
|
|
21
|
+
} | {
|
|
22
|
+
status: "error";
|
|
23
|
+
message: string;
|
|
24
|
+
};
|
|
25
|
+
export type DeviceActivationAuth = {
|
|
26
|
+
init(): Promise<unknown>;
|
|
27
|
+
handleCallback(callbackUrl: string): Promise<DeviceActivationBindResult | null>;
|
|
28
|
+
signIn(options?: DeviceActivationSignInOptions): Promise<void>;
|
|
29
|
+
};
|
|
30
|
+
export type DeviceActivationControllerConfig = {
|
|
31
|
+
authState: DeviceActivationAuth;
|
|
32
|
+
createClient(authUrlState: DeviceActivationConnectAuthUrlState): Promise<DeviceActivationClient>;
|
|
33
|
+
getUrl?: () => URL;
|
|
34
|
+
replaceUrl?: (url: string) => void;
|
|
35
|
+
sessionStorage?: StorageLike;
|
|
36
|
+
createCallbackToken?: () => string;
|
|
37
|
+
};
|
|
38
|
+
export type DeviceActivationOperationRef = {
|
|
39
|
+
watch(): AsyncResult<AsyncIterable<OperationEvent<DeviceUserAuthoritiesResolveProgress, DeviceUserAuthoritiesResolveOutput>>, BaseError>;
|
|
40
|
+
wait(): AsyncResult<TerminalOperation<DeviceUserAuthoritiesResolveProgress, DeviceUserAuthoritiesResolveOutput>, BaseError>;
|
|
41
|
+
};
|
|
42
|
+
type CompanionConsent = NonNullable<DeviceUserAuthoritiesResolveProgress["companionConsent"]>;
|
|
43
|
+
export type DeviceCompanionApproval = {
|
|
44
|
+
mode: "capabilities";
|
|
45
|
+
installedRevision: CompanionConsent["installedRevision"];
|
|
46
|
+
expectedGrantRevision: CompanionConsent["expectedGrantRevision"];
|
|
47
|
+
decisionDigest: CompanionConsent["decisionDigest"];
|
|
48
|
+
approvedCapabilities: Array<{
|
|
49
|
+
id: CompanionConsent["capabilities"][number]["id"];
|
|
50
|
+
consentDigest: CompanionConsent["capabilities"][number]["consentDigest"];
|
|
51
|
+
}>;
|
|
52
|
+
approvedResources: Array<{
|
|
53
|
+
kind: CompanionConsent["resources"][number]["kind"];
|
|
54
|
+
name: CompanionConsent["resources"][number]["name"];
|
|
55
|
+
commitment: CompanionConsent["resources"][number]["requestedCommitment"];
|
|
56
|
+
}>;
|
|
57
|
+
companionApproved: boolean;
|
|
58
|
+
};
|
|
59
|
+
export type DeviceActivationClient = {
|
|
60
|
+
activateDevice(input: {
|
|
61
|
+
flowId: string;
|
|
62
|
+
confirmationCode: string;
|
|
63
|
+
companionApproval?: DeviceCompanionApproval;
|
|
64
|
+
}): Promise<DeviceActivationOperationRef>;
|
|
65
|
+
};
|
|
66
|
+
export type DeviceActivationState = {
|
|
67
|
+
loading: boolean;
|
|
68
|
+
requestPending: boolean;
|
|
69
|
+
authError: string | null;
|
|
70
|
+
view: DeviceActivationView | null;
|
|
71
|
+
flowId: string | null;
|
|
72
|
+
confirmationCode: string;
|
|
73
|
+
};
|
|
74
|
+
export declare function createInitialDeviceActivationState(): DeviceActivationState;
|
|
75
|
+
export declare class DeviceActivationControllerCore {
|
|
76
|
+
#private;
|
|
77
|
+
protected readonly state: DeviceActivationState;
|
|
78
|
+
constructor(config: DeviceActivationControllerConfig, state?: DeviceActivationState);
|
|
79
|
+
get loading(): boolean;
|
|
80
|
+
get requestPending(): boolean;
|
|
81
|
+
get authError(): string | null;
|
|
82
|
+
get view(): DeviceActivationView | null;
|
|
83
|
+
get flowId(): string | null;
|
|
84
|
+
get confirmationCode(): string;
|
|
85
|
+
set confirmationCode(value: string);
|
|
86
|
+
load(): Promise<void>;
|
|
87
|
+
stop(): void;
|
|
88
|
+
signIn(): Promise<void>;
|
|
89
|
+
requestActivation(): Promise<void>;
|
|
90
|
+
}
|
|
91
|
+
export {};
|
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
import { ulid } from "ulid";
|
|
2
|
+
import { createDeviceActivationReadyView, createDeviceActivationSignInRequiredView, createInvalidDeviceActivationView, isDeviceActivationReviewerRequiredFailure, mapDeviceActivationFailure, mapDeviceActivationProgress, mapDeviceActivationTerminal, } from "./internal/activation_view.js";
|
|
3
|
+
import { clearPreservedDeviceActivationCallbackState, getPreservedDeviceActivationCallbackState, preserveDeviceActivationCallbackState, } from "./internal/callback_state.js";
|
|
4
|
+
import { buildDeviceActivationCallbackPath, buildDeviceActivationConnectAuthUrlState, cleanupDeviceActivationCallbackUrl, resolveDeviceActivationUrlState, } from "./internal/portal_url.js";
|
|
5
|
+
function errorMessage(error) {
|
|
6
|
+
return error instanceof Error ? error.message : String(error);
|
|
7
|
+
}
|
|
8
|
+
function buildCompanionApproval(consent) {
|
|
9
|
+
return {
|
|
10
|
+
mode: "capabilities",
|
|
11
|
+
installedRevision: consent.installedRevision,
|
|
12
|
+
expectedGrantRevision: consent.expectedGrantRevision,
|
|
13
|
+
decisionDigest: consent.decisionDigest,
|
|
14
|
+
approvedCapabilities: consent.capabilities
|
|
15
|
+
.filter((item) => item.required && item.eligible)
|
|
16
|
+
.map((item) => ({ id: item.id, consentDigest: item.consentDigest })),
|
|
17
|
+
approvedResources: consent.resources
|
|
18
|
+
.filter((item) => item.required && item.eligible)
|
|
19
|
+
.map((item) => ({
|
|
20
|
+
kind: item.kind,
|
|
21
|
+
name: item.name,
|
|
22
|
+
commitment: item.requestedCommitment,
|
|
23
|
+
})),
|
|
24
|
+
companionApproved: false,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
function redirectErrorMessage(error) {
|
|
28
|
+
const message = errorMessage(error);
|
|
29
|
+
return message.startsWith("Redirecting to auth for provider selection")
|
|
30
|
+
? null
|
|
31
|
+
: message;
|
|
32
|
+
}
|
|
33
|
+
function bindErrorMessage(result) {
|
|
34
|
+
if (result.status === "bound")
|
|
35
|
+
return null;
|
|
36
|
+
if (result.status === "approval_denied")
|
|
37
|
+
return "Portal access was denied.";
|
|
38
|
+
if (result.status === "approval_required") {
|
|
39
|
+
return "Approval is still pending.";
|
|
40
|
+
}
|
|
41
|
+
if (result.status === "insufficient_capabilities") {
|
|
42
|
+
return `Missing capabilities: ${result.missingCapabilities.join(", ")}`;
|
|
43
|
+
}
|
|
44
|
+
return result.message;
|
|
45
|
+
}
|
|
46
|
+
function defaultGetUrl() {
|
|
47
|
+
return new URL(globalThis.location.href);
|
|
48
|
+
}
|
|
49
|
+
function defaultReplaceUrl(url) {
|
|
50
|
+
globalThis.history.replaceState(globalThis.history.state, "", url);
|
|
51
|
+
}
|
|
52
|
+
function defaultCreateCallbackToken() {
|
|
53
|
+
return ulid();
|
|
54
|
+
}
|
|
55
|
+
export function createInitialDeviceActivationState() {
|
|
56
|
+
return {
|
|
57
|
+
loading: true,
|
|
58
|
+
requestPending: false,
|
|
59
|
+
authError: null,
|
|
60
|
+
view: null,
|
|
61
|
+
flowId: null,
|
|
62
|
+
confirmationCode: "",
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
export class DeviceActivationControllerCore {
|
|
66
|
+
state;
|
|
67
|
+
#authState;
|
|
68
|
+
#createClient;
|
|
69
|
+
#getUrl;
|
|
70
|
+
#replaceUrl;
|
|
71
|
+
#sessionStorage;
|
|
72
|
+
#createCallbackToken;
|
|
73
|
+
#client = null;
|
|
74
|
+
#observationRunId = 0;
|
|
75
|
+
constructor(config, state = createInitialDeviceActivationState()) {
|
|
76
|
+
this.state = state;
|
|
77
|
+
this.#authState = config.authState;
|
|
78
|
+
this.#createClient = config.createClient;
|
|
79
|
+
this.#getUrl = config.getUrl ?? defaultGetUrl;
|
|
80
|
+
this.#replaceUrl = config.replaceUrl ?? defaultReplaceUrl;
|
|
81
|
+
this.#sessionStorage = config.sessionStorage ?? null;
|
|
82
|
+
this.#createCallbackToken = config.createCallbackToken ??
|
|
83
|
+
defaultCreateCallbackToken;
|
|
84
|
+
}
|
|
85
|
+
get loading() {
|
|
86
|
+
return this.state.loading;
|
|
87
|
+
}
|
|
88
|
+
get requestPending() {
|
|
89
|
+
return this.state.requestPending;
|
|
90
|
+
}
|
|
91
|
+
get authError() {
|
|
92
|
+
return this.state.authError;
|
|
93
|
+
}
|
|
94
|
+
get view() {
|
|
95
|
+
return this.state.view;
|
|
96
|
+
}
|
|
97
|
+
get flowId() {
|
|
98
|
+
return this.state.flowId;
|
|
99
|
+
}
|
|
100
|
+
get confirmationCode() {
|
|
101
|
+
return this.state.confirmationCode;
|
|
102
|
+
}
|
|
103
|
+
set confirmationCode(value) {
|
|
104
|
+
this.state.confirmationCode = value;
|
|
105
|
+
}
|
|
106
|
+
async load() {
|
|
107
|
+
this.stop();
|
|
108
|
+
this.state.loading = true;
|
|
109
|
+
this.state.authError = null;
|
|
110
|
+
this.state.view = null;
|
|
111
|
+
this.#client = null;
|
|
112
|
+
const currentUrl = this.#getUrl();
|
|
113
|
+
let clientUrl = currentUrl;
|
|
114
|
+
const preservedState = this.#sessionStorage
|
|
115
|
+
? getPreservedDeviceActivationCallbackState(this.#sessionStorage)
|
|
116
|
+
: null;
|
|
117
|
+
const { flowId, isAuthCallback } = resolveDeviceActivationUrlState(currentUrl, preservedState);
|
|
118
|
+
this.state.flowId = flowId;
|
|
119
|
+
if (!flowId) {
|
|
120
|
+
this.state.view = createInvalidDeviceActivationView("Missing flow id.");
|
|
121
|
+
this.state.loading = false;
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
try {
|
|
125
|
+
await this.#authState.init();
|
|
126
|
+
if (isAuthCallback) {
|
|
127
|
+
const bindResult = await this.#authState.handleCallback(currentUrl.toString());
|
|
128
|
+
const cleanedUrl = cleanupDeviceActivationCallbackUrl(currentUrl, flowId);
|
|
129
|
+
if (cleanedUrl) {
|
|
130
|
+
this.#replaceUrl(cleanedUrl);
|
|
131
|
+
clientUrl = new URL(cleanedUrl, currentUrl.origin);
|
|
132
|
+
}
|
|
133
|
+
if (this.#sessionStorage) {
|
|
134
|
+
clearPreservedDeviceActivationCallbackState(this.#sessionStorage);
|
|
135
|
+
}
|
|
136
|
+
const callbackAuthError = currentUrl.searchParams.get("authError");
|
|
137
|
+
if (callbackAuthError && !bindResult) {
|
|
138
|
+
this.state.authError = callbackAuthError;
|
|
139
|
+
this.state.view = createDeviceActivationSignInRequiredView(flowId);
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
if (bindResult) {
|
|
143
|
+
const bindError = bindErrorMessage(bindResult);
|
|
144
|
+
if (bindError) {
|
|
145
|
+
this.state.authError = bindError;
|
|
146
|
+
this.state.view = createDeviceActivationSignInRequiredView(flowId);
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
try {
|
|
152
|
+
this.#client = await this.#createClient(buildDeviceActivationConnectAuthUrlState(clientUrl));
|
|
153
|
+
this.state.view = createDeviceActivationReadyView(flowId);
|
|
154
|
+
}
|
|
155
|
+
catch (error) {
|
|
156
|
+
this.#client = null;
|
|
157
|
+
if (isAuthCallback) {
|
|
158
|
+
this.state.authError = errorMessage(error);
|
|
159
|
+
}
|
|
160
|
+
this.state.view = createDeviceActivationSignInRequiredView(flowId);
|
|
161
|
+
if (!isAuthCallback) {
|
|
162
|
+
await this.signIn();
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
catch (error) {
|
|
167
|
+
this.state.authError = errorMessage(error);
|
|
168
|
+
this.state.view = createDeviceActivationSignInRequiredView(flowId);
|
|
169
|
+
}
|
|
170
|
+
finally {
|
|
171
|
+
this.state.loading = false;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
stop() {
|
|
175
|
+
this.#observationRunId += 1;
|
|
176
|
+
}
|
|
177
|
+
async signIn() {
|
|
178
|
+
this.state.authError = null;
|
|
179
|
+
if (!this.state.flowId || !this.#sessionStorage)
|
|
180
|
+
return;
|
|
181
|
+
const callbackToken = this.#createCallbackToken();
|
|
182
|
+
preserveDeviceActivationCallbackState(this.#sessionStorage, {
|
|
183
|
+
flowId: this.state.flowId,
|
|
184
|
+
callbackToken,
|
|
185
|
+
});
|
|
186
|
+
try {
|
|
187
|
+
await this.#authState.signIn({
|
|
188
|
+
redirectTo: buildDeviceActivationCallbackPath(this.#getUrl(), callbackToken),
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
catch (error) {
|
|
192
|
+
const message = redirectErrorMessage(error);
|
|
193
|
+
if (message) {
|
|
194
|
+
this.state.authError = message;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
async requestActivation() {
|
|
199
|
+
const flowId = this.state.flowId;
|
|
200
|
+
const confirmationCode = this.state.confirmationCode.trim();
|
|
201
|
+
if (!flowId || !confirmationCode || !this.#client)
|
|
202
|
+
return;
|
|
203
|
+
this.stop();
|
|
204
|
+
const runId = this.#observationRunId;
|
|
205
|
+
this.state.requestPending = true;
|
|
206
|
+
this.state.authError = null;
|
|
207
|
+
try {
|
|
208
|
+
let operation = await this.#client.activateDevice({
|
|
209
|
+
flowId,
|
|
210
|
+
confirmationCode,
|
|
211
|
+
});
|
|
212
|
+
if (hasOperationSnapshot(operation)) {
|
|
213
|
+
let snapshot = await operation.get().match({
|
|
214
|
+
ok: (value) => value,
|
|
215
|
+
err: () => null,
|
|
216
|
+
});
|
|
217
|
+
for (let attempt = 0; attempt < 40 &&
|
|
218
|
+
!snapshot?.progress?.companionConsent &&
|
|
219
|
+
snapshot?.state === "running" &&
|
|
220
|
+
this.#isRunActive(runId); attempt++) {
|
|
221
|
+
await new Promise((resolve) => setTimeout(resolve, 250));
|
|
222
|
+
snapshot = await operation.get().match({
|
|
223
|
+
ok: (value) => value,
|
|
224
|
+
err: () => null,
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
if (snapshot?.progress && this.#isRunActive(runId)) {
|
|
228
|
+
this.state.view = mapDeviceActivationProgress(flowId, snapshot.progress);
|
|
229
|
+
}
|
|
230
|
+
const companionConsent = snapshot?.progress?.companionConsent;
|
|
231
|
+
if (companionConsent && this.#isRunActive(runId)) {
|
|
232
|
+
try {
|
|
233
|
+
operation = await this.#client.activateDevice({
|
|
234
|
+
flowId,
|
|
235
|
+
confirmationCode,
|
|
236
|
+
companionApproval: buildCompanionApproval(companionConsent),
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
catch (error) {
|
|
240
|
+
// Only a permission outcome means a reviewer must decide later;
|
|
241
|
+
// malformed, stale, or transport failures stay visible/retryable.
|
|
242
|
+
if (!isDeviceActivationReviewerRequiredFailure(error)) {
|
|
243
|
+
throw error;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
const watch = await operation.watch().match({
|
|
249
|
+
ok: (value) => value,
|
|
250
|
+
err: () => null,
|
|
251
|
+
});
|
|
252
|
+
const watchPromise = watch
|
|
253
|
+
? this.#observeWatch(flowId, watch, runId)
|
|
254
|
+
: Promise.resolve(false);
|
|
255
|
+
const terminal = await operation.wait().orThrow();
|
|
256
|
+
const handledByWatch = await watchPromise;
|
|
257
|
+
if (!handledByWatch && this.#isRunActive(runId)) {
|
|
258
|
+
this.#applyTerminal(flowId, terminal);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
catch (error) {
|
|
262
|
+
if (!this.#isRunActive(runId))
|
|
263
|
+
return;
|
|
264
|
+
const nextView = mapDeviceActivationFailure(flowId, error);
|
|
265
|
+
if (nextView) {
|
|
266
|
+
this.state.view = nextView;
|
|
267
|
+
}
|
|
268
|
+
else {
|
|
269
|
+
this.state.view = createDeviceActivationReadyView(flowId);
|
|
270
|
+
this.state.authError = errorMessage(error);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
finally {
|
|
274
|
+
if (this.#isRunActive(runId)) {
|
|
275
|
+
this.state.requestPending = false;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
#isRunActive(runId) {
|
|
280
|
+
return this.#observationRunId === runId;
|
|
281
|
+
}
|
|
282
|
+
#applyTerminal(flowId, terminal) {
|
|
283
|
+
const view = mapDeviceActivationTerminal(flowId, terminal);
|
|
284
|
+
if (view) {
|
|
285
|
+
this.state.view = view;
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
this.state.view = createDeviceActivationReadyView(flowId);
|
|
289
|
+
if (terminal.error?.message) {
|
|
290
|
+
this.state.authError = terminal.error.message;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
async #observeWatch(flowId, watch, runId) {
|
|
294
|
+
const iterator = watch[Symbol.asyncIterator]();
|
|
295
|
+
while (true) {
|
|
296
|
+
const next = await iterator.next();
|
|
297
|
+
if (next.done) {
|
|
298
|
+
return false;
|
|
299
|
+
}
|
|
300
|
+
const event = next.value;
|
|
301
|
+
if (!this.#isRunActive(runId)) {
|
|
302
|
+
await iterator.return?.();
|
|
303
|
+
return false;
|
|
304
|
+
}
|
|
305
|
+
if (event.type === "completed" || event.type === "failed" ||
|
|
306
|
+
event.type === "cancelled") {
|
|
307
|
+
this.#applyTerminal(flowId, event.snapshot);
|
|
308
|
+
return true;
|
|
309
|
+
}
|
|
310
|
+
if (event.type === "progress") {
|
|
311
|
+
this.state.view = mapDeviceActivationProgress(flowId, event.progress);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
function hasOperationSnapshot(operation) {
|
|
317
|
+
return "get" in operation && typeof operation.get === "function";
|
|
318
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export { default as TrellisProvider } from "./components/TrellisProvider.svelte";
|
|
2
|
+
export type { TrellisProviderProps } from "./components/TrellisProvider.types.js";
|
|
3
|
+
export type { CreateTrellisAppOptions, SvelteTrellisConnection, TrellisApp, TrellisAppOwner, TrellisAppUrl, TrellisAppUrlResolver, TrellisClientFor, TrellisContextClient, TrellisParticipantLike, } from "./context.js";
|
|
4
|
+
export { createTrellisApp, resolveTrellisAppUrl } from "./context.js";
|
|
5
|
+
export { AuthorizationContextController, type AuthorizationContextStatus, } from "./authorization_context.js";
|
|
6
|
+
export { createDeviceActivationController, type DeviceActivationAuth, type DeviceActivationClient, DeviceActivationController, type DeviceActivationControllerConfig, type DeviceActivationOperationRef, } from "./device_activation.js";
|
|
7
|
+
export { createPortalFlow, type CreatePortalFlowConfig, PortalFlowController, } from "./portal_flow.js";
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { default as TrellisProvider } from "./components/TrellisProvider.svelte";
|
|
2
|
+
export { createTrellisApp, resolveTrellisAppUrl } from "./context.js";
|
|
3
|
+
export { AuthorizationContextController, } from "./authorization_context.js";
|
|
4
|
+
export { createDeviceActivationController, DeviceActivationController, } from "./device_activation.js";
|
|
5
|
+
export { createPortalFlow, PortalFlowController, } from "./portal_flow.js";
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { DeviceUserAuthoritiesResolveOutput, DeviceUserAuthoritiesResolveProgress } from "@oatscenter/trellis/auth";
|
|
2
|
+
import type { TerminalOperation } from "@oatscenter/trellis";
|
|
3
|
+
export type DeviceActivationView = {
|
|
4
|
+
mode: "sign_in_required";
|
|
5
|
+
flowId: string;
|
|
6
|
+
} | {
|
|
7
|
+
mode: "ready";
|
|
8
|
+
flowId: string;
|
|
9
|
+
} | {
|
|
10
|
+
mode: "pending_review";
|
|
11
|
+
flowId: string;
|
|
12
|
+
state: DeviceUserAuthoritiesResolveProgress["state"];
|
|
13
|
+
} | {
|
|
14
|
+
mode: "activated";
|
|
15
|
+
flowId: string;
|
|
16
|
+
instanceId: string;
|
|
17
|
+
deploymentId: string;
|
|
18
|
+
activatedAt: string;
|
|
19
|
+
} | {
|
|
20
|
+
mode: "rejected";
|
|
21
|
+
flowId: string;
|
|
22
|
+
reason?: string;
|
|
23
|
+
} | {
|
|
24
|
+
mode: "expired";
|
|
25
|
+
flowId: string;
|
|
26
|
+
reason: string;
|
|
27
|
+
} | {
|
|
28
|
+
mode: "invalid_flow";
|
|
29
|
+
reason: string;
|
|
30
|
+
flowId?: string;
|
|
31
|
+
};
|
|
32
|
+
/**
|
|
33
|
+
* Whether a companion resubmission failed because the signed-in user is not
|
|
34
|
+
* allowed to decide it, which leaves the activation to a reviewer. Malformed,
|
|
35
|
+
* stale, and transport failures are not reviewer outcomes.
|
|
36
|
+
*/
|
|
37
|
+
export declare function isDeviceActivationReviewerRequiredFailure(error: unknown): boolean;
|
|
38
|
+
export declare function createDeviceActivationReadyView(flowId: string): DeviceActivationView;
|
|
39
|
+
export declare function createDeviceActivationSignInRequiredView(flowId: string): DeviceActivationView;
|
|
40
|
+
export declare function createInvalidDeviceActivationView(reason: string, flowId?: string): DeviceActivationView;
|
|
41
|
+
export declare function mapDeviceActivationOutput(flowId: string, result: DeviceUserAuthoritiesResolveOutput): DeviceActivationView;
|
|
42
|
+
export declare function mapDeviceActivationProgress(flowId: string, progress: DeviceUserAuthoritiesResolveProgress): DeviceActivationView;
|
|
43
|
+
export declare function mapDeviceActivationFailure(flowId: string, error: unknown): DeviceActivationView | null;
|
|
44
|
+
export declare function mapDeviceActivationTerminal(flowId: string, terminal: TerminalOperation<unknown, DeviceUserAuthoritiesResolveOutput>): DeviceActivationView | null;
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
function isoString(value) {
|
|
2
|
+
return value instanceof Date
|
|
3
|
+
? value.toISOString()
|
|
4
|
+
: typeof value === "number" || typeof value === "bigint"
|
|
5
|
+
? new Date(Number(value)).toISOString()
|
|
6
|
+
: value;
|
|
7
|
+
}
|
|
8
|
+
function errorReason(error) {
|
|
9
|
+
if (typeof error !== "object" || error === null || !("reason" in error)) {
|
|
10
|
+
return undefined;
|
|
11
|
+
}
|
|
12
|
+
const reason = Reflect.get(error, "reason");
|
|
13
|
+
return typeof reason === "string" ? reason : undefined;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Whether a companion resubmission failed because the signed-in user is not
|
|
17
|
+
* allowed to decide it, which leaves the activation to a reviewer. Malformed,
|
|
18
|
+
* stale, and transport failures are not reviewer outcomes.
|
|
19
|
+
*/
|
|
20
|
+
export function isDeviceActivationReviewerRequiredFailure(error) {
|
|
21
|
+
const reason = errorReason(error);
|
|
22
|
+
return (reason === "not_authorized" ||
|
|
23
|
+
reason === "insufficient_permissions" ||
|
|
24
|
+
reason === "forbidden");
|
|
25
|
+
}
|
|
26
|
+
export function createDeviceActivationReadyView(flowId) {
|
|
27
|
+
return { mode: "ready", flowId };
|
|
28
|
+
}
|
|
29
|
+
export function createDeviceActivationSignInRequiredView(flowId) {
|
|
30
|
+
return { mode: "sign_in_required", flowId };
|
|
31
|
+
}
|
|
32
|
+
export function createInvalidDeviceActivationView(reason, flowId) {
|
|
33
|
+
return flowId
|
|
34
|
+
? { mode: "invalid_flow", reason, flowId }
|
|
35
|
+
: { mode: "invalid_flow", reason };
|
|
36
|
+
}
|
|
37
|
+
export function mapDeviceActivationOutput(flowId, result) {
|
|
38
|
+
if (result.review.state === "approved") {
|
|
39
|
+
return {
|
|
40
|
+
mode: "activated",
|
|
41
|
+
flowId,
|
|
42
|
+
instanceId: result.device.instanceId,
|
|
43
|
+
deploymentId: result.device.deploymentId,
|
|
44
|
+
activatedAt: isoString(result.review.decidedAt ?? result.device.updatedAt),
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
return {
|
|
48
|
+
mode: "rejected",
|
|
49
|
+
flowId,
|
|
50
|
+
...(result.review.reason ? { reason: result.review.reason } : {}),
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
export function mapDeviceActivationProgress(flowId, progress) {
|
|
54
|
+
return {
|
|
55
|
+
mode: "pending_review",
|
|
56
|
+
flowId,
|
|
57
|
+
state: progress.state,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
export function mapDeviceActivationFailure(flowId, error) {
|
|
61
|
+
const reason = errorReason(error);
|
|
62
|
+
if (reason === "device_activation_flow_not_found") {
|
|
63
|
+
return createInvalidDeviceActivationView("This activation link is no longer valid.", flowId);
|
|
64
|
+
}
|
|
65
|
+
if (reason === "device_activation_flow_expired") {
|
|
66
|
+
return {
|
|
67
|
+
mode: "expired",
|
|
68
|
+
flowId,
|
|
69
|
+
reason: "The activation request expired. Start again from the auth service.",
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
if (reason === "unknown_device") {
|
|
73
|
+
return createInvalidDeviceActivationView("This activation link no longer matches a known device.", flowId);
|
|
74
|
+
}
|
|
75
|
+
if (reason === "device_deployment_not_found") {
|
|
76
|
+
return createInvalidDeviceActivationView("This device deployment is no longer available.", flowId);
|
|
77
|
+
}
|
|
78
|
+
if (reason === "invalid_request") {
|
|
79
|
+
return createInvalidDeviceActivationView("Trellis rejected this activation request. Start again from the device.", flowId);
|
|
80
|
+
}
|
|
81
|
+
if (reason === "device_activation_revoked") {
|
|
82
|
+
return { mode: "rejected", flowId, reason };
|
|
83
|
+
}
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
export function mapDeviceActivationTerminal(flowId, terminal) {
|
|
87
|
+
if (terminal.state === "completed") {
|
|
88
|
+
return terminal.output
|
|
89
|
+
? mapDeviceActivationOutput(flowId, terminal.output)
|
|
90
|
+
: null;
|
|
91
|
+
}
|
|
92
|
+
return terminal.error
|
|
93
|
+
? mapDeviceActivationFailure(flowId, terminal.error)
|
|
94
|
+
: null;
|
|
95
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export type DeviceActivationCallbackState = {
|
|
2
|
+
flowId: string;
|
|
3
|
+
callbackToken: string;
|
|
4
|
+
};
|
|
5
|
+
export type StorageLike = Pick<Storage, "getItem" | "setItem" | "removeItem">;
|
|
6
|
+
export declare function getPreservedDeviceActivationCallbackState(storage: StorageLike): DeviceActivationCallbackState | null;
|
|
7
|
+
export declare function preserveDeviceActivationCallbackState(storage: StorageLike, nextState: DeviceActivationCallbackState): void;
|
|
8
|
+
export declare function clearPreservedDeviceActivationCallbackState(storage: StorageLike): void;
|
|
9
|
+
export declare function isDeviceActivationAuthCallback(currentUrl: URL, preservedState: DeviceActivationCallbackState | null): boolean;
|
|
@@ -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,13 @@
|
|
|
1
|
+
import type { DeviceActivationCallbackState } from "./callback_state.js";
|
|
2
|
+
export type DeviceActivationConnectAuthUrlState = {
|
|
3
|
+
currentUrl: URL;
|
|
4
|
+
redirectTo: string;
|
|
5
|
+
};
|
|
6
|
+
export type DeviceActivationUrlState = {
|
|
7
|
+
flowId: string | null;
|
|
8
|
+
isAuthCallback: boolean;
|
|
9
|
+
};
|
|
10
|
+
export declare function buildDeviceActivationCallbackPath(currentUrl: URL, callbackToken: string): string;
|
|
11
|
+
export declare function buildDeviceActivationConnectAuthUrlState(currentUrl: URL): DeviceActivationConnectAuthUrlState;
|
|
12
|
+
export declare function cleanupDeviceActivationCallbackUrl(currentUrl: URL, flowId: string | null): string | null;
|
|
13
|
+
export declare function resolveDeviceActivationUrlState(currentUrl: URL, preservedState: DeviceActivationCallbackState | null): DeviceActivationUrlState;
|
|
@@ -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
|
+
}
|