@qlever-llc/trellis 0.10.18-rc.1 → 0.10.18
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/esm/auth/browser/portal.d.ts.map +1 -1
- package/esm/auth/browser/portal.js +2 -0
- package/esm/auth/browser.d.ts +2 -0
- package/esm/auth/browser.d.ts.map +1 -1
- package/esm/auth/browser.js +1 -0
- package/esm/auth/browser_recovery.d.ts +22 -0
- package/esm/auth/browser_recovery.d.ts.map +1 -0
- package/esm/auth/browser_recovery.js +238 -0
- package/esm/auth/mod.d.ts +1 -1
- package/esm/auth/mod.d.ts.map +1 -1
- package/esm/auth/mod.js +1 -1
- package/esm/auth/protocol.d.ts +1 -0
- package/esm/auth/protocol.d.ts.map +1 -1
- package/esm/auth/protocol.js +1 -0
- package/esm/browser.d.ts +2 -2
- package/esm/browser.d.ts.map +1 -1
- package/esm/browser.js +1 -1
- package/esm/client_connect.js +1 -1
- package/esm/generated-sdk/auth/api.d.ts +1 -4
- package/esm/generated-sdk/auth/api.d.ts.map +1 -1
- package/esm/generated-sdk/auth/api.js +1 -6
- package/esm/generated-sdk/auth/client.d.ts +0 -15
- package/esm/generated-sdk/auth/client.d.ts.map +1 -1
- package/esm/generated-sdk/auth/contract.d.ts.map +1 -1
- package/esm/generated-sdk/auth/contract.js +4 -1
- package/esm/generated-sdk/trellis-core/api.d.ts +1 -4
- package/esm/generated-sdk/trellis-core/api.d.ts.map +1 -1
- package/esm/generated-sdk/trellis-core/api.js +1 -6
- package/esm/generated-sdk/trellis-core/client.d.ts +3 -28
- package/esm/generated-sdk/trellis-core/client.d.ts.map +1 -1
- package/package.json +2 -2
- package/script/auth/browser/portal.d.ts.map +1 -1
- package/script/auth/browser/portal.js +2 -0
- package/script/auth/browser.d.ts +2 -0
- package/script/auth/browser.d.ts.map +1 -1
- package/script/auth/browser.js +4 -1
- package/script/auth/browser_recovery.d.ts +22 -0
- package/script/auth/browser_recovery.d.ts.map +1 -0
- package/script/auth/browser_recovery.js +242 -0
- package/script/auth/mod.d.ts +1 -1
- package/script/auth/mod.d.ts.map +1 -1
- package/script/auth/mod.js +7 -5
- package/script/auth/protocol.d.ts +1 -0
- package/script/auth/protocol.d.ts.map +1 -1
- package/script/auth/protocol.js +1 -0
- package/script/browser.d.ts +2 -2
- package/script/browser.d.ts.map +1 -1
- package/script/browser.js +4 -2
- package/script/client_connect.js +1 -1
- package/script/generated-sdk/auth/api.d.ts +1 -4
- package/script/generated-sdk/auth/api.d.ts.map +1 -1
- package/script/generated-sdk/auth/api.js +1 -6
- package/script/generated-sdk/auth/client.d.ts +0 -15
- package/script/generated-sdk/auth/client.d.ts.map +1 -1
- package/script/generated-sdk/auth/contract.d.ts.map +1 -1
- package/script/generated-sdk/auth/contract.js +4 -1
- package/script/generated-sdk/trellis-core/api.d.ts +1 -4
- package/script/generated-sdk/trellis-core/api.d.ts.map +1 -1
- package/script/generated-sdk/trellis-core/api.js +1 -6
- package/script/generated-sdk/trellis-core/client.d.ts +3 -28
- package/script/generated-sdk/trellis-core/client.d.ts.map +1 -1
- package/src/auth/browser/portal.ts +1 -0
- package/src/auth/browser.ts +8 -0
- package/src/auth/browser_recovery.ts +319 -0
- package/src/auth/mod.ts +4 -0
- package/src/auth/protocol.ts +1 -0
- package/src/browser.ts +4 -0
- package/src/client_connect.ts +1 -1
- package/src/sdk/_generated/auth/api.ts +2 -9
- package/src/sdk/_generated/auth/client.ts +0 -37
- package/src/sdk/_generated/auth/contract.ts +4 -1
- package/src/sdk/_generated/core/api.ts +2 -9
- package/src/sdk/_generated/core/client.ts +2 -41
|
@@ -1,18 +1,9 @@
|
|
|
1
|
-
import type { AsyncResult, BaseError,
|
|
1
|
+
import type { AsyncResult, BaseError, HandlerTrellis, MaybeAsync, ReceiveTransferGrant, ReceiveTransferHandle, RequestOpts, RpcHandlerContext, SendTransferGrant, SendTransferHandle, TrellisConnection } from "@qlever-llc/trellis";
|
|
2
2
|
import type { Api } from "./api.js";
|
|
3
3
|
import type * as Types from "./types.js";
|
|
4
|
-
import type * as HealthSdk from "@qlever-llc/trellis/sdk/health";
|
|
5
4
|
type WithDeps<TDeps> = [TDeps] extends [undefined] ? {} : {
|
|
6
5
|
deps: TDeps;
|
|
7
6
|
};
|
|
8
|
-
type EventCallback<TMessage> = {
|
|
9
|
-
bivarianceHack(message: TMessage, context: EventListenerContext): MaybeAsync<void, BaseError>;
|
|
10
|
-
}["bivarianceHack"];
|
|
11
|
-
type ServiceEventHandler<TEvent, TDeps = undefined> = (args: {
|
|
12
|
-
event: TEvent;
|
|
13
|
-
context: EventListenerContext;
|
|
14
|
-
client: HandlerClient;
|
|
15
|
-
} & WithDeps<TDeps>) => MaybeAsync<void, BaseError>;
|
|
16
7
|
type RpcHandler<TInput, TOutput, TDeps = undefined> = (args: {
|
|
17
8
|
input: TInput;
|
|
18
9
|
context: RpcHandlerContext;
|
|
@@ -35,15 +26,7 @@ export interface TrellisCoreClient {
|
|
|
35
26
|
surfaceStatus(input: Types.TrellisSurfaceStatusInput, opts?: RequestOpts): AsyncResult<Types.TrellisSurfaceStatusOutput, BaseError>;
|
|
36
27
|
};
|
|
37
28
|
};
|
|
38
|
-
readonly event: {
|
|
39
|
-
readonly health: {
|
|
40
|
-
heartbeat: {
|
|
41
|
-
publish(event: Omit<HealthSdk.HealthHeartbeatEvent, "header">): AsyncResult<void, ValidationError | UnexpectedError>;
|
|
42
|
-
prepare(event: Omit<HealthSdk.HealthHeartbeatEvent, "header">): Result<PreparedTrellisEvent<Omit<HealthSdk.HealthHeartbeatEvent, "header">>, ValidationError | UnexpectedError>;
|
|
43
|
-
listen(handler: EventCallback<HealthSdk.HealthHeartbeatEvent>, subjectData?: Record<string, unknown>, opts?: EventOpts): AsyncResult<void, ValidationError | UnexpectedError>;
|
|
44
|
-
};
|
|
45
|
-
};
|
|
46
|
-
};
|
|
29
|
+
readonly event: {};
|
|
47
30
|
readonly feed: {};
|
|
48
31
|
readonly operation: {};
|
|
49
32
|
wait(): AsyncResult<void, BaseError>;
|
|
@@ -57,15 +40,7 @@ export type ServiceWithDeps<TDeps> = Omit<TrellisCoreClient, "event"> & {
|
|
|
57
40
|
readonly handle: ServiceHandle<TDeps>;
|
|
58
41
|
with<TNextDeps>(deps: TNextDeps): ServiceWithDeps<TNextDeps>;
|
|
59
42
|
};
|
|
60
|
-
export
|
|
61
|
-
readonly health: {
|
|
62
|
-
heartbeat: {
|
|
63
|
-
publish(event: Omit<HealthSdk.HealthHeartbeatEvent, "header">): AsyncResult<void, ValidationError | UnexpectedError>;
|
|
64
|
-
prepare(event: Omit<HealthSdk.HealthHeartbeatEvent, "header">): Result<PreparedTrellisEvent<Omit<HealthSdk.HealthHeartbeatEvent, "header">>, ValidationError | UnexpectedError>;
|
|
65
|
-
listen(handler: ServiceEventHandler<HealthSdk.HealthHeartbeatEvent, TDeps>, subjectData?: Record<string, unknown>, opts?: EventOpts): AsyncResult<void, ValidationError | UnexpectedError>;
|
|
66
|
-
};
|
|
67
|
-
};
|
|
68
|
-
}
|
|
43
|
+
export type ServiceEventSurface<TDeps> = {};
|
|
69
44
|
export interface ServiceHandle<TDeps = undefined> {
|
|
70
45
|
readonly rpc: {
|
|
71
46
|
readonly trellis: {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../../../src/sdk/_generated/core/client.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAEV,WAAW,EACX,SAAS,
|
|
1
|
+
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../../../src/sdk/_generated/core/client.ts"],"names":[],"mappings":"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"}
|
|
@@ -71,5 +71,6 @@ export function portalRedirectLocation(
|
|
|
71
71
|
): string | null {
|
|
72
72
|
if (state?.status === "redirect") return state.location;
|
|
73
73
|
if (state?.status === "approval_denied") return state.returnLocation ?? null;
|
|
74
|
+
if (state?.status === "expired") return state.returnLocation ?? null;
|
|
74
75
|
return null;
|
|
75
76
|
}
|
package/src/auth/browser.ts
CHANGED
|
@@ -45,6 +45,14 @@ export {
|
|
|
45
45
|
signBytes,
|
|
46
46
|
} from "./browser/session.js";
|
|
47
47
|
export { deleteKeyPair, hasKeyPair } from "./browser/storage.js";
|
|
48
|
+
export {
|
|
49
|
+
classifyBrowserAuthError,
|
|
50
|
+
isRecoverableBrowserAuthError,
|
|
51
|
+
} from "./browser_recovery.js";
|
|
52
|
+
export type {
|
|
53
|
+
BrowserAuthRecoveryClassification,
|
|
54
|
+
BrowserAuthRecoveryKind,
|
|
55
|
+
} from "./browser_recovery.js";
|
|
48
56
|
export {
|
|
49
57
|
approvalCapabilityKeys,
|
|
50
58
|
type ApprovalDecision as ApprovalDecisionData,
|
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
/** Browser auth recovery classification helpers. */
|
|
2
|
+
|
|
3
|
+
/** Stable browser-auth recovery categories for app-owned recovery flows. */
|
|
4
|
+
export type BrowserAuthRecoveryKind =
|
|
5
|
+
| "recoverable_stale_session"
|
|
6
|
+
| "recoverable_expired_flow"
|
|
7
|
+
| "recoverable_auth_required"
|
|
8
|
+
| "policy_denied"
|
|
9
|
+
| "insufficient_capabilities"
|
|
10
|
+
| "runtime_unavailable"
|
|
11
|
+
| "unknown";
|
|
12
|
+
|
|
13
|
+
/** Classification result for a browser-auth related failure. */
|
|
14
|
+
export type BrowserAuthRecoveryClassification = {
|
|
15
|
+
kind: BrowserAuthRecoveryKind;
|
|
16
|
+
recoverable: boolean;
|
|
17
|
+
reason?: string;
|
|
18
|
+
code?: string;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
type ErrorSignal = {
|
|
22
|
+
code?: string;
|
|
23
|
+
reason?: string;
|
|
24
|
+
message?: string;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const MAX_ERROR_DEPTH = 8;
|
|
28
|
+
|
|
29
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
30
|
+
return value !== null && typeof value === "object";
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function stringProperty(
|
|
34
|
+
record: Record<string, unknown>,
|
|
35
|
+
property: string,
|
|
36
|
+
): string | undefined {
|
|
37
|
+
const value = record[property];
|
|
38
|
+
return typeof value === "string" ? value : undefined;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function normalize(value: string): string {
|
|
42
|
+
return value.trim().toLowerCase().replaceAll("-", "_").replaceAll(" ", "_");
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function signalValue(signal: ErrorSignal): string | undefined {
|
|
46
|
+
return signal.code ?? signal.reason ?? signal.message;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function classification(
|
|
50
|
+
kind: BrowserAuthRecoveryKind,
|
|
51
|
+
recoverable: boolean,
|
|
52
|
+
signal?: ErrorSignal,
|
|
53
|
+
): BrowserAuthRecoveryClassification {
|
|
54
|
+
const reason = signal?.reason ?? signalValue(signal ?? {});
|
|
55
|
+
return {
|
|
56
|
+
kind,
|
|
57
|
+
recoverable,
|
|
58
|
+
...(reason ? { reason } : {}),
|
|
59
|
+
...(signal?.code ? { code: signal.code } : {}),
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function maybeSerializable(value: unknown): unknown {
|
|
64
|
+
if (!isRecord(value)) return undefined;
|
|
65
|
+
const toSerializable = value.toSerializable;
|
|
66
|
+
if (typeof toSerializable !== "function") return undefined;
|
|
67
|
+
try {
|
|
68
|
+
return toSerializable.call(value);
|
|
69
|
+
} catch {
|
|
70
|
+
return undefined;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function pushRecordSignals(
|
|
75
|
+
record: Record<string, unknown>,
|
|
76
|
+
signals: ErrorSignal[],
|
|
77
|
+
queue: unknown[],
|
|
78
|
+
): void {
|
|
79
|
+
const message = stringProperty(record, "message");
|
|
80
|
+
const code = stringProperty(record, "code") ??
|
|
81
|
+
stringProperty(record, "error");
|
|
82
|
+
const reason = stringProperty(record, "reason") ??
|
|
83
|
+
stringProperty(record, "status");
|
|
84
|
+
if (message || code || reason) {
|
|
85
|
+
signals.push({
|
|
86
|
+
...(code ? { code } : {}),
|
|
87
|
+
...(reason ? { reason } : {}),
|
|
88
|
+
...(message ? { message } : {}),
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const context = record.context;
|
|
93
|
+
if (isRecord(context)) {
|
|
94
|
+
const contextReason = stringProperty(context, "reason");
|
|
95
|
+
const causeMessage = stringProperty(context, "causeMessage");
|
|
96
|
+
const contextCode = stringProperty(context, "code");
|
|
97
|
+
const contextMessage = stringProperty(context, "message");
|
|
98
|
+
if (contextReason || causeMessage || contextCode || contextMessage) {
|
|
99
|
+
signals.push({
|
|
100
|
+
...(contextCode ? { code: contextCode } : {}),
|
|
101
|
+
...(contextReason ? { reason: contextReason } : {}),
|
|
102
|
+
...(causeMessage ?? contextMessage
|
|
103
|
+
? { message: causeMessage ?? contextMessage }
|
|
104
|
+
: {}),
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
for (const nested of [context.cause, context.error, context.remoteError]) {
|
|
108
|
+
if (nested !== undefined) queue.push(nested);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
for (const nested of [record.cause, record.error, record.remoteError]) {
|
|
113
|
+
if (nested !== undefined) queue.push(nested);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function collectErrorSignals(error: unknown): ErrorSignal[] {
|
|
118
|
+
const signals: ErrorSignal[] = [];
|
|
119
|
+
const queue: unknown[] = [error];
|
|
120
|
+
const seen = new WeakSet<object>();
|
|
121
|
+
let depth = 0;
|
|
122
|
+
|
|
123
|
+
while (queue.length > 0 && depth < MAX_ERROR_DEPTH) {
|
|
124
|
+
depth += 1;
|
|
125
|
+
const value = queue.shift();
|
|
126
|
+
if (typeof value === "string") {
|
|
127
|
+
signals.push({ message: value });
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
if (!isRecord(value)) continue;
|
|
131
|
+
if (seen.has(value)) continue;
|
|
132
|
+
seen.add(value);
|
|
133
|
+
|
|
134
|
+
if (value instanceof Error) {
|
|
135
|
+
signals.push({ message: value.message });
|
|
136
|
+
}
|
|
137
|
+
pushRecordSignals(value, signals, queue);
|
|
138
|
+
|
|
139
|
+
const serialized = maybeSerializable(value);
|
|
140
|
+
if (serialized && serialized !== value) queue.push(serialized);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return signals;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function hasNormalizedValue(
|
|
147
|
+
signals: ErrorSignal[],
|
|
148
|
+
values: ReadonlySet<string>,
|
|
149
|
+
): ErrorSignal | undefined {
|
|
150
|
+
for (const signal of signals) {
|
|
151
|
+
for (const value of [signal.code, signal.reason]) {
|
|
152
|
+
if (value && values.has(normalize(value))) return signal;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
return undefined;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function hasMessagePattern(
|
|
159
|
+
signals: ErrorSignal[],
|
|
160
|
+
patterns: readonly RegExp[],
|
|
161
|
+
): ErrorSignal | undefined {
|
|
162
|
+
for (const signal of signals) {
|
|
163
|
+
const message = signal.message;
|
|
164
|
+
if (!message) continue;
|
|
165
|
+
const normalized = message.toLowerCase();
|
|
166
|
+
if (patterns.some((pattern) => pattern.test(normalized))) return signal;
|
|
167
|
+
}
|
|
168
|
+
return undefined;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function insufficientPermissionsAuthSignal(
|
|
172
|
+
signals: ErrorSignal[],
|
|
173
|
+
): ErrorSignal | undefined {
|
|
174
|
+
for (const signal of signals) {
|
|
175
|
+
const reason = signal.reason ? normalize(signal.reason) : undefined;
|
|
176
|
+
const code = signal.code ? normalize(signal.code) : undefined;
|
|
177
|
+
if (reason !== "insufficient_permissions") continue;
|
|
178
|
+
if (code === "trellis.bootstrap.auth_required") return signal;
|
|
179
|
+
const message = signal.message?.toLowerCase() ?? "";
|
|
180
|
+
if (
|
|
181
|
+
message.includes("auth required") ||
|
|
182
|
+
message.includes("requires sign-in") ||
|
|
183
|
+
message.includes("requires signin") ||
|
|
184
|
+
message.includes("stale") ||
|
|
185
|
+
message.includes("session")
|
|
186
|
+
) {
|
|
187
|
+
return signal;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
return undefined;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const APPROVAL_DENIED_VALUES = new Set([
|
|
194
|
+
"approval_denied",
|
|
195
|
+
"trellis.auth.approval_denied",
|
|
196
|
+
]);
|
|
197
|
+
|
|
198
|
+
const INSUFFICIENT_CAPABILITIES_VALUES = new Set([
|
|
199
|
+
"insufficient_capabilities",
|
|
200
|
+
"trellis.auth.insufficient_capabilities",
|
|
201
|
+
]);
|
|
202
|
+
|
|
203
|
+
const RUNTIME_UNAVAILABLE_VALUES = new Set([
|
|
204
|
+
"trellis.bootstrap.not_ready",
|
|
205
|
+
"trellis.runtime.unavailable",
|
|
206
|
+
"runtime_unavailable",
|
|
207
|
+
]);
|
|
208
|
+
|
|
209
|
+
const EXPIRED_FLOW_VALUES = new Set([
|
|
210
|
+
"flow_expired",
|
|
211
|
+
"expired",
|
|
212
|
+
"trellis.auth.bind_expired",
|
|
213
|
+
"trellis.auth.flow_expired",
|
|
214
|
+
]);
|
|
215
|
+
|
|
216
|
+
const STALE_SESSION_VALUES = new Set([
|
|
217
|
+
"session_not_found",
|
|
218
|
+
"session_expired",
|
|
219
|
+
"user_not_found",
|
|
220
|
+
"contract_not_active",
|
|
221
|
+
"trellis.auth.session_not_found",
|
|
222
|
+
"trellis.auth.session_expired",
|
|
223
|
+
"trellis.auth.user_not_found",
|
|
224
|
+
"trellis.auth.contract_not_active",
|
|
225
|
+
]);
|
|
226
|
+
|
|
227
|
+
const AUTH_REQUIRED_VALUES = new Set([
|
|
228
|
+
"auth_required",
|
|
229
|
+
"trellis.bootstrap.auth_required",
|
|
230
|
+
"trellis.auth.login_failed",
|
|
231
|
+
]);
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Classifies browser auth, bootstrap, callback, and transport-like failures.
|
|
235
|
+
*
|
|
236
|
+
* The classifier accepts raw errors, serialized Trellis errors, nested causes,
|
|
237
|
+
* and nested remote errors. Recoverable classifications are intended for
|
|
238
|
+
* app-owned flows that can clear stale auth and restart sign-in without showing a
|
|
239
|
+
* user-facing terminal error.
|
|
240
|
+
*/
|
|
241
|
+
export function classifyBrowserAuthError(
|
|
242
|
+
error: unknown,
|
|
243
|
+
): BrowserAuthRecoveryClassification {
|
|
244
|
+
const signals = collectErrorSignals(error);
|
|
245
|
+
|
|
246
|
+
const approvalDenied = hasNormalizedValue(signals, APPROVAL_DENIED_VALUES) ??
|
|
247
|
+
hasMessagePattern(signals, [/approval .*denied/, /access .*denied/]);
|
|
248
|
+
if (approvalDenied) {
|
|
249
|
+
return classification("policy_denied", false, approvalDenied);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
const inactiveOrInvalid = hasNormalizedValue(
|
|
253
|
+
signals,
|
|
254
|
+
new Set(["invalid_credentials", "user_inactive", "inactive_account"]),
|
|
255
|
+
) ?? hasMessagePattern(signals, [
|
|
256
|
+
/invalid credential/,
|
|
257
|
+
/inactive account/,
|
|
258
|
+
/user inactive/,
|
|
259
|
+
]);
|
|
260
|
+
if (inactiveOrInvalid) {
|
|
261
|
+
return classification("policy_denied", false, inactiveOrInvalid);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
const insufficientCapabilities = hasNormalizedValue(
|
|
265
|
+
signals,
|
|
266
|
+
INSUFFICIENT_CAPABILITIES_VALUES,
|
|
267
|
+
) ?? hasMessagePattern(signals, [/insufficient capabilit/]);
|
|
268
|
+
if (insufficientCapabilities) {
|
|
269
|
+
return classification(
|
|
270
|
+
"insufficient_capabilities",
|
|
271
|
+
false,
|
|
272
|
+
insufficientCapabilities,
|
|
273
|
+
);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
const runtimeUnavailable = hasNormalizedValue(
|
|
277
|
+
signals,
|
|
278
|
+
RUNTIME_UNAVAILABLE_VALUES,
|
|
279
|
+
) ?? hasMessagePattern(signals, [/runtime unavailable/]);
|
|
280
|
+
if (runtimeUnavailable) {
|
|
281
|
+
return classification("runtime_unavailable", false, runtimeUnavailable);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
const expiredFlow = hasNormalizedValue(signals, EXPIRED_FLOW_VALUES) ??
|
|
285
|
+
hasMessagePattern(signals, [/flow .*expired/, /sign\-in .*expired/]);
|
|
286
|
+
if (expiredFlow) {
|
|
287
|
+
return classification("recoverable_expired_flow", true, expiredFlow);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
const staleSession = hasNormalizedValue(signals, STALE_SESSION_VALUES) ??
|
|
291
|
+
hasMessagePattern(signals, [
|
|
292
|
+
/session .*expired/,
|
|
293
|
+
/session .*not found/,
|
|
294
|
+
/user .*not found/,
|
|
295
|
+
/contract .*not active/,
|
|
296
|
+
]);
|
|
297
|
+
if (staleSession) {
|
|
298
|
+
return classification("recoverable_stale_session", true, staleSession);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
const authRequired = hasNormalizedValue(signals, AUTH_REQUIRED_VALUES) ??
|
|
302
|
+
insufficientPermissionsAuthSignal(signals) ??
|
|
303
|
+
hasMessagePattern(signals, [
|
|
304
|
+
/auth required/,
|
|
305
|
+
/requires sign\-in/,
|
|
306
|
+
/requires signin/,
|
|
307
|
+
/requires authentication/,
|
|
308
|
+
]);
|
|
309
|
+
if (authRequired) {
|
|
310
|
+
return classification("recoverable_auth_required", true, authRequired);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
return classification("unknown", false);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/** Returns whether a browser auth error can silently restart sign-in. */
|
|
317
|
+
export function isRecoverableBrowserAuthError(error: unknown): boolean {
|
|
318
|
+
return classifyBrowserAuthError(error).recoverable;
|
|
319
|
+
}
|
package/src/auth/mod.ts
CHANGED
|
@@ -43,7 +43,10 @@ export {
|
|
|
43
43
|
export {
|
|
44
44
|
type AuthConfig,
|
|
45
45
|
bindFlow,
|
|
46
|
+
type BrowserAuthRecoveryClassification,
|
|
47
|
+
type BrowserAuthRecoveryKind,
|
|
46
48
|
buildLoginUrl,
|
|
49
|
+
classifyBrowserAuthError,
|
|
47
50
|
clearSessionKey,
|
|
48
51
|
createRpcProof,
|
|
49
52
|
fetchPortalFlowState,
|
|
@@ -52,6 +55,7 @@ export {
|
|
|
52
55
|
getPublicSessionKey,
|
|
53
56
|
hasSessionKey,
|
|
54
57
|
isBindSuccessResponse,
|
|
58
|
+
isRecoverableBrowserAuthError,
|
|
55
59
|
loadSessionKey,
|
|
56
60
|
natsConnectSigForIat,
|
|
57
61
|
portalFlowIdFromUrl,
|
package/src/auth/protocol.ts
CHANGED
|
@@ -1215,6 +1215,7 @@ export const PortalFlowStateSchema = Type.Union([
|
|
|
1215
1215
|
}),
|
|
1216
1216
|
Type.Object({
|
|
1217
1217
|
status: Type.Literal("expired"),
|
|
1218
|
+
returnLocation: Type.Optional(Type.String({ minLength: 1 })),
|
|
1218
1219
|
}),
|
|
1219
1220
|
]);
|
|
1220
1221
|
export type PortalFlowState = StaticDecode<typeof PortalFlowStateSchema>;
|
package/src/browser.ts
CHANGED
|
@@ -6,6 +6,7 @@ import "./_dnt.polyfills.js";
|
|
|
6
6
|
|
|
7
7
|
export {
|
|
8
8
|
bindFlow,
|
|
9
|
+
classifyBrowserAuthError,
|
|
9
10
|
clearSessionKey,
|
|
10
11
|
createAuth,
|
|
11
12
|
createRpcProof,
|
|
@@ -14,12 +15,15 @@ export {
|
|
|
14
15
|
getPublicSessionKey,
|
|
15
16
|
hasSessionKey,
|
|
16
17
|
isBindSuccessResponse,
|
|
18
|
+
isRecoverableBrowserAuthError,
|
|
17
19
|
loadSessionKey,
|
|
18
20
|
signBytes,
|
|
19
21
|
} from "./auth.js";
|
|
20
22
|
export type {
|
|
21
23
|
BindResponse,
|
|
22
24
|
BindSuccessResponse,
|
|
25
|
+
BrowserAuthRecoveryClassification,
|
|
26
|
+
BrowserAuthRecoveryKind,
|
|
23
27
|
NatsConnectOptions,
|
|
24
28
|
SessionKeyHandle,
|
|
25
29
|
} from "./auth.js";
|
package/src/client_connect.ts
CHANGED
|
@@ -1,15 +1,12 @@
|
|
|
1
1
|
// Generated from ./generated/contracts/manifests/trellis.auth@v1.json
|
|
2
2
|
import { OWNED_API } from "./owned_api.js";
|
|
3
|
-
import { OWNED_API as HealthApi } from "../health/mod.js";
|
|
4
3
|
|
|
5
4
|
export { OWNED_API };
|
|
6
5
|
|
|
7
6
|
export type UsedApi = {
|
|
8
7
|
rpc: {};
|
|
9
8
|
operations: {};
|
|
10
|
-
events: {
|
|
11
|
-
readonly "Health.Heartbeat": typeof HealthApi.events["Health.Heartbeat"];
|
|
12
|
-
};
|
|
9
|
+
events: {};
|
|
13
10
|
feeds: {};
|
|
14
11
|
subjects: {};
|
|
15
12
|
};
|
|
@@ -17,11 +14,7 @@ export type UsedApi = {
|
|
|
17
14
|
export const USED_API: UsedApi = {
|
|
18
15
|
rpc: {},
|
|
19
16
|
operations: {},
|
|
20
|
-
events: {
|
|
21
|
-
get "Health.Heartbeat"() {
|
|
22
|
-
return HealthApi.events["Health.Heartbeat"];
|
|
23
|
-
},
|
|
24
|
-
},
|
|
17
|
+
events: {},
|
|
25
18
|
feeds: {},
|
|
26
19
|
subjects: {},
|
|
27
20
|
};
|
|
@@ -33,7 +33,6 @@ import type {
|
|
|
33
33
|
} from "../../../index.js";
|
|
34
34
|
import type { API, Api } from "./api.js";
|
|
35
35
|
import type * as Types from "./types.js";
|
|
36
|
-
import type * as HealthSdk from "../health/mod.js";
|
|
37
36
|
|
|
38
37
|
type WithDeps<TDeps> = [TDeps] extends [undefined] ? {} : { deps: TDeps };
|
|
39
38
|
|
|
@@ -566,24 +565,6 @@ export interface TrellisAuthClient {
|
|
|
566
565
|
): AsyncResult<void, ValidationError | UnexpectedError>;
|
|
567
566
|
};
|
|
568
567
|
};
|
|
569
|
-
readonly health: {
|
|
570
|
-
heartbeat: {
|
|
571
|
-
publish(
|
|
572
|
-
event: Omit<HealthSdk.HealthHeartbeatEvent, "header">,
|
|
573
|
-
): AsyncResult<void, ValidationError | UnexpectedError>;
|
|
574
|
-
prepare(
|
|
575
|
-
event: Omit<HealthSdk.HealthHeartbeatEvent, "header">,
|
|
576
|
-
): Result<
|
|
577
|
-
PreparedTrellisEvent<Omit<HealthSdk.HealthHeartbeatEvent, "header">>,
|
|
578
|
-
ValidationError | UnexpectedError
|
|
579
|
-
>;
|
|
580
|
-
listen(
|
|
581
|
-
handler: EventCallback<HealthSdk.HealthHeartbeatEvent>,
|
|
582
|
-
subjectData?: Record<string, unknown>,
|
|
583
|
-
opts?: EventOpts,
|
|
584
|
-
): AsyncResult<void, ValidationError | UnexpectedError>;
|
|
585
|
-
};
|
|
586
|
-
};
|
|
587
568
|
};
|
|
588
569
|
readonly feed: {};
|
|
589
570
|
readonly operation: {
|
|
@@ -762,24 +743,6 @@ export interface ServiceEventSurface<TDeps> {
|
|
|
762
743
|
): AsyncResult<void, ValidationError | UnexpectedError>;
|
|
763
744
|
};
|
|
764
745
|
};
|
|
765
|
-
readonly health: {
|
|
766
|
-
heartbeat: {
|
|
767
|
-
publish(
|
|
768
|
-
event: Omit<HealthSdk.HealthHeartbeatEvent, "header">,
|
|
769
|
-
): AsyncResult<void, ValidationError | UnexpectedError>;
|
|
770
|
-
prepare(
|
|
771
|
-
event: Omit<HealthSdk.HealthHeartbeatEvent, "header">,
|
|
772
|
-
): Result<
|
|
773
|
-
PreparedTrellisEvent<Omit<HealthSdk.HealthHeartbeatEvent, "header">>,
|
|
774
|
-
ValidationError | UnexpectedError
|
|
775
|
-
>;
|
|
776
|
-
listen(
|
|
777
|
-
handler: ServiceEventHandler<HealthSdk.HealthHeartbeatEvent, TDeps>,
|
|
778
|
-
subjectData?: Record<string, unknown>,
|
|
779
|
-
opts?: EventOpts,
|
|
780
|
-
): AsyncResult<void, ValidationError | UnexpectedError>;
|
|
781
|
-
};
|
|
782
|
-
};
|
|
783
746
|
}
|
|
784
747
|
|
|
785
748
|
export interface ServiceHandle<TDeps = undefined> {
|
|
@@ -9575,7 +9575,10 @@ export const CONTRACT = {
|
|
|
9575
9575
|
"required": ["status", "location"],
|
|
9576
9576
|
"type": "object",
|
|
9577
9577
|
}, {
|
|
9578
|
-
"properties": {
|
|
9578
|
+
"properties": {
|
|
9579
|
+
"returnLocation": { "minLength": 1, "type": "string" },
|
|
9580
|
+
"status": { "const": "expired", "type": "string" },
|
|
9581
|
+
},
|
|
9579
9582
|
"required": ["status"],
|
|
9580
9583
|
"type": "object",
|
|
9581
9584
|
}],
|
|
@@ -1,15 +1,12 @@
|
|
|
1
1
|
// Generated from ./generated/contracts/manifests/trellis.core@v1.json
|
|
2
2
|
import { OWNED_API } from "./owned_api.js";
|
|
3
|
-
import { OWNED_API as HealthApi } from "../health/mod.js";
|
|
4
3
|
|
|
5
4
|
export { OWNED_API };
|
|
6
5
|
|
|
7
6
|
export type UsedApi = {
|
|
8
7
|
rpc: {};
|
|
9
8
|
operations: {};
|
|
10
|
-
events: {
|
|
11
|
-
readonly "Health.Heartbeat": typeof HealthApi.events["Health.Heartbeat"];
|
|
12
|
-
};
|
|
9
|
+
events: {};
|
|
13
10
|
feeds: {};
|
|
14
11
|
subjects: {};
|
|
15
12
|
};
|
|
@@ -17,11 +14,7 @@ export type UsedApi = {
|
|
|
17
14
|
export const USED_API: UsedApi = {
|
|
18
15
|
rpc: {},
|
|
19
16
|
operations: {},
|
|
20
|
-
events: {
|
|
21
|
-
get "Health.Heartbeat"() {
|
|
22
|
-
return HealthApi.events["Health.Heartbeat"];
|
|
23
|
-
},
|
|
24
|
-
},
|
|
17
|
+
events: {},
|
|
25
18
|
feeds: {},
|
|
26
19
|
subjects: {},
|
|
27
20
|
};
|
|
@@ -33,7 +33,6 @@ import type {
|
|
|
33
33
|
} from "../../../index.js";
|
|
34
34
|
import type { API, Api } from "./api.js";
|
|
35
35
|
import type * as Types from "./types.js";
|
|
36
|
-
import type * as HealthSdk from "../health/mod.js";
|
|
37
36
|
|
|
38
37
|
type WithDeps<TDeps> = [TDeps] extends [undefined] ? {} : { deps: TDeps };
|
|
39
38
|
|
|
@@ -111,26 +110,7 @@ export interface TrellisCoreClient {
|
|
|
111
110
|
): AsyncResult<Types.TrellisSurfaceStatusOutput, BaseError>;
|
|
112
111
|
};
|
|
113
112
|
};
|
|
114
|
-
readonly event: {
|
|
115
|
-
readonly health: {
|
|
116
|
-
heartbeat: {
|
|
117
|
-
publish(
|
|
118
|
-
event: Omit<HealthSdk.HealthHeartbeatEvent, "header">,
|
|
119
|
-
): AsyncResult<void, ValidationError | UnexpectedError>;
|
|
120
|
-
prepare(
|
|
121
|
-
event: Omit<HealthSdk.HealthHeartbeatEvent, "header">,
|
|
122
|
-
): Result<
|
|
123
|
-
PreparedTrellisEvent<Omit<HealthSdk.HealthHeartbeatEvent, "header">>,
|
|
124
|
-
ValidationError | UnexpectedError
|
|
125
|
-
>;
|
|
126
|
-
listen(
|
|
127
|
-
handler: EventCallback<HealthSdk.HealthHeartbeatEvent>,
|
|
128
|
-
subjectData?: Record<string, unknown>,
|
|
129
|
-
opts?: EventOpts,
|
|
130
|
-
): AsyncResult<void, ValidationError | UnexpectedError>;
|
|
131
|
-
};
|
|
132
|
-
};
|
|
133
|
-
};
|
|
113
|
+
readonly event: {};
|
|
134
114
|
readonly feed: {};
|
|
135
115
|
readonly operation: {};
|
|
136
116
|
wait(): AsyncResult<void, BaseError>;
|
|
@@ -147,26 +127,7 @@ export type ServiceWithDeps<TDeps> = Omit<TrellisCoreClient, "event"> & {
|
|
|
147
127
|
with<TNextDeps>(deps: TNextDeps): ServiceWithDeps<TNextDeps>;
|
|
148
128
|
};
|
|
149
129
|
|
|
150
|
-
export
|
|
151
|
-
readonly health: {
|
|
152
|
-
heartbeat: {
|
|
153
|
-
publish(
|
|
154
|
-
event: Omit<HealthSdk.HealthHeartbeatEvent, "header">,
|
|
155
|
-
): AsyncResult<void, ValidationError | UnexpectedError>;
|
|
156
|
-
prepare(
|
|
157
|
-
event: Omit<HealthSdk.HealthHeartbeatEvent, "header">,
|
|
158
|
-
): Result<
|
|
159
|
-
PreparedTrellisEvent<Omit<HealthSdk.HealthHeartbeatEvent, "header">>,
|
|
160
|
-
ValidationError | UnexpectedError
|
|
161
|
-
>;
|
|
162
|
-
listen(
|
|
163
|
-
handler: ServiceEventHandler<HealthSdk.HealthHeartbeatEvent, TDeps>,
|
|
164
|
-
subjectData?: Record<string, unknown>,
|
|
165
|
-
opts?: EventOpts,
|
|
166
|
-
): AsyncResult<void, ValidationError | UnexpectedError>;
|
|
167
|
-
};
|
|
168
|
-
};
|
|
169
|
-
}
|
|
130
|
+
export type ServiceEventSurface<TDeps> = {};
|
|
170
131
|
|
|
171
132
|
export interface ServiceHandle<TDeps = undefined> {
|
|
172
133
|
readonly rpc: {
|