@oatscenter/trellis 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 +59 -0
- package/auth/authorization/client_context.d.ts +61 -0
- package/auth/authorization/client_context.js +266 -0
- package/auth/authorization/nats_registry.d.ts +41 -0
- package/auth/authorization/nats_registry.js +283 -0
- package/auth/authorization/provider_cache.d.ts +90 -0
- package/auth/authorization/provider_cache.js +783 -0
- package/auth/authorization/refresh.d.ts +46 -0
- package/auth/authorization/refresh.js +246 -0
- package/auth/authorization/types.d.ts +207 -0
- package/auth/authorization/types.js +60 -0
- package/auth/authorization_context.d.ts +11 -0
- package/auth/authorization_context.js +10 -0
- package/auth/browser/flow_types.d.ts +84 -0
- package/auth/browser/flow_types.js +70 -0
- package/auth/browser/portal.d.ts +21 -0
- package/auth/browser/portal.js +307 -0
- package/auth/browser/storage.d.ts +26 -0
- package/auth/browser/storage.js +224 -0
- package/auth/browser.d.ts +11 -0
- package/auth/browser.js +10 -0
- package/auth/browser_recovery.d.ts +13 -0
- package/auth/browser_recovery.js +38 -0
- package/auth/device_activation.d.ts +134 -0
- package/auth/device_activation.js +376 -0
- package/auth/http_error.d.ts +12 -0
- package/auth/http_error.js +32 -0
- package/auth/keys.d.ts +5 -0
- package/auth/keys.js +63 -0
- package/auth/mod.d.ts +16 -0
- package/auth/mod.js +16 -0
- package/auth/proof.d.ts +31 -0
- package/auth/proof.js +95 -0
- package/auth/protocol_wasm/trellis_protocol_wasm.d.ts +138 -0
- package/auth/protocol_wasm/trellis_protocol_wasm.js +605 -0
- package/auth/protocol_wasm/trellis_protocol_wasm_bg.wasm +0 -0
- package/auth/protocol_wasm/trellis_protocol_wasm_bytes.d.ts +1 -0
- package/auth/protocol_wasm/trellis_protocol_wasm_bytes.js +2 -0
- package/auth/protocol_wasm.d.ts +179 -0
- package/auth/protocol_wasm.js +133 -0
- package/auth/session_auth.d.ts +30 -0
- package/auth/session_auth.js +77 -0
- package/auth/session_proof.d.ts +53 -0
- package/auth/session_proof.js +43 -0
- package/auth/time.d.ts +6 -0
- package/auth/time.js +7 -0
- package/auth/trellis_id.d.ts +1 -0
- package/auth/trellis_id.js +5 -0
- package/auth/utils.d.ts +10 -0
- package/auth/utils.js +69 -0
- package/auth.d.ts +1 -0
- package/auth.js +1 -0
- package/caller.d.ts +111 -0
- package/caller.js +85 -0
- package/client.d.ts +14 -0
- package/client.js +1 -0
- package/client_connect.d.ts +143 -0
- package/client_connect.js +1097 -0
- package/codec.d.ts +13 -0
- package/codec.js +192 -0
- package/connection.d.ts +107 -0
- package/connection.js +396 -0
- package/device/activation.d.ts +42 -0
- package/device/activation.js +319 -0
- package/device.d.ts +80 -0
- package/device.js +687 -0
- package/env.d.ts +2 -0
- package/env.js +10 -0
- package/errors/AuthError.d.ts +30 -0
- package/errors/AuthError.js +34 -0
- package/errors/KVError.d.ts +30 -0
- package/errors/KVError.js +36 -0
- package/errors/OperationLifecycleError.d.ts +110 -0
- package/errors/OperationLifecycleError.js +142 -0
- package/errors/RemoteError.d.ts +61 -0
- package/errors/RemoteError.js +72 -0
- package/errors/SchemaValidationError.d.ts +48 -0
- package/errors/SchemaValidationError.js +49 -0
- package/errors/StoreError.d.ts +21 -0
- package/errors/StoreError.js +31 -0
- package/errors/TransferError.d.ts +21 -0
- package/errors/TransferError.js +31 -0
- package/errors/TransportError.d.ts +29 -0
- package/errors/TransportError.js +33 -0
- package/errors/TrellisError.d.ts +15 -0
- package/errors/TrellisError.js +15 -0
- package/errors/ValidationError.d.ts +50 -0
- package/errors/ValidationError.js +59 -0
- package/errors/index.d.ts +254 -0
- package/errors/index.js +238 -0
- package/generated.d.ts +4 -0
- package/generated.js +3 -0
- package/generated_support.d.ts +120 -0
- package/generated_support.js +359 -0
- package/globals.d.ts +9 -0
- package/globals.js +16 -0
- package/health_transport.d.ts +18 -0
- package/health_transport.js +32 -0
- package/helpers.d.ts +11 -0
- package/helpers.js +53 -0
- package/index.d.ts +2 -0
- package/index.js +2 -0
- package/internal_sdk/generated/apis/auth/mod.d.ts +805 -0
- package/internal_sdk/generated/apis/auth/mod.js +958 -0
- package/internal_sdk/generated/apis/core/mod.d.ts +79 -0
- package/internal_sdk/generated/apis/core/mod.js +93 -0
- package/internal_sdk/generated/apis/events/mod.d.ts +224 -0
- package/internal_sdk/generated/apis/events/mod.js +313 -0
- package/internal_sdk/generated/apis/health/mod.d.ts +106 -0
- package/internal_sdk/generated/apis/health/mod.js +132 -0
- package/internal_sdk/generated/apis/index.d.ts +7 -0
- package/internal_sdk/generated/apis/index.js +8 -0
- package/internal_sdk/generated/apis/jobs/mod.d.ts +176 -0
- package/internal_sdk/generated/apis/jobs/mod.js +209 -0
- package/internal_sdk/generated/apis/state/mod.d.ts +104 -0
- package/internal_sdk/generated/apis/state/mod.js +139 -0
- package/internal_sdk/generated/index.d.ts +4 -0
- package/internal_sdk/generated/index.js +5 -0
- package/internal_sdk/generated/package.json +18 -0
- package/internal_sdk/generated/participants/cli/mod.d.ts +402 -0
- package/internal_sdk/generated/participants/cli/mod.js +460 -0
- package/internal_sdk/generated/participants/cli/types.d.ts +2 -0
- package/internal_sdk/generated/participants/cli/types.js +3 -0
- package/internal_sdk/generated/participants/console/mod.d.ts +489 -0
- package/internal_sdk/generated/participants/console/mod.js +579 -0
- package/internal_sdk/generated/participants/console/types.d.ts +2 -0
- package/internal_sdk/generated/participants/console/types.js +3 -0
- package/internal_sdk/generated/participants/eventsRuntime/mod.d.ts +64 -0
- package/internal_sdk/generated/participants/eventsRuntime/mod.js +42 -0
- package/internal_sdk/generated/participants/eventsRuntime/types.d.ts +2 -0
- package/internal_sdk/generated/participants/eventsRuntime/types.js +3 -0
- package/internal_sdk/generated/participants/healthRuntime/mod.d.ts +58 -0
- package/internal_sdk/generated/participants/healthRuntime/mod.js +36 -0
- package/internal_sdk/generated/participants/healthRuntime/types.d.ts +2 -0
- package/internal_sdk/generated/participants/healthRuntime/types.js +3 -0
- package/internal_sdk/generated/participants/index.d.ts +8 -0
- package/internal_sdk/generated/participants/index.js +9 -0
- package/internal_sdk/generated/participants/jobsRuntime/mod.d.ts +64 -0
- package/internal_sdk/generated/participants/jobsRuntime/mod.js +42 -0
- package/internal_sdk/generated/participants/jobsRuntime/types.d.ts +2 -0
- package/internal_sdk/generated/participants/jobsRuntime/types.js +3 -0
- package/internal_sdk/generated/participants/platform/mod.d.ts +164 -0
- package/internal_sdk/generated/participants/platform/mod.js +149 -0
- package/internal_sdk/generated/participants/platform/types.d.ts +7 -0
- package/internal_sdk/generated/participants/platform/types.js +5 -0
- package/internal_sdk/generated/participants/portal/mod.d.ts +131 -0
- package/internal_sdk/generated/participants/portal/mod.js +109 -0
- package/internal_sdk/generated/participants/portal/types.d.ts +2 -0
- package/internal_sdk/generated/participants/portal/types.js +3 -0
- package/internal_sdk/generated/types/_internal/p0.d.ts +13511 -0
- package/internal_sdk/generated/types/_internal/p0.js +6105 -0
- package/internal_sdk/generated/types/index.d.ts +2951 -0
- package/internal_sdk/generated/types/index.js +1477 -0
- package/jobs.d.ts +309 -0
- package/jobs.js +389 -0
- package/kv.d.ts +85 -0
- package/kv.js +330 -0
- package/operations.d.ts +189 -0
- package/operations.js +847 -0
- package/package.json +73 -0
- package/pagination.d.ts +25 -0
- package/pagination.js +117 -0
- package/participant.d.ts +6 -0
- package/participant.js +6 -0
- package/participant_runtime/api.d.ts +131 -0
- package/participant_runtime/api.js +42 -0
- package/participant_runtime/json.d.ts +16 -0
- package/participant_runtime/json.js +193 -0
- package/participant_runtime/metadata.d.ts +34 -0
- package/participant_runtime/metadata.js +4 -0
- package/participant_runtime/participant.d.ts +200 -0
- package/participant_runtime/participant.js +330 -0
- package/participant_runtime/schemas.d.ts +347 -0
- package/participant_runtime/schemas.js +211 -0
- package/participant_runtime/surface_names.d.ts +16 -0
- package/participant_runtime/surface_names.js +34 -0
- package/portable.d.ts +30 -0
- package/portable.js +14 -0
- package/provider.d.ts +69 -0
- package/provider.js +68 -0
- package/runtime_transport.d.ts +25 -0
- package/runtime_transport.js +48 -0
- package/service/mod.d.ts +14 -0
- package/service/mod.js +13 -0
- package/service/outbox_inbox.d.ts +259 -0
- package/service/outbox_inbox.js +908 -0
- package/service/runtime/bootstrap.d.ts +56 -0
- package/service/runtime/bootstrap.js +226 -0
- package/service/runtime/core.d.ts +46 -0
- package/service/runtime/core.js +1747 -0
- package/service/runtime/health.d.ts +97 -0
- package/service/runtime/health.js +153 -0
- package/service/runtime/health_schemas.d.ts +23 -0
- package/service/runtime/health_schemas.js +20 -0
- package/service/runtime/internal_jobs/active-job.d.ts +40 -0
- package/service/runtime/internal_jobs/active-job.js +91 -0
- package/service/runtime/internal_jobs/bindings.d.ts +40 -0
- package/service/runtime/internal_jobs/bindings.js +1 -0
- package/service/runtime/internal_jobs/cancellation-registry.d.ts +13 -0
- package/service/runtime/internal_jobs/cancellation-registry.js +55 -0
- package/service/runtime/internal_jobs/heartbeat.d.ts +33 -0
- package/service/runtime/internal_jobs/heartbeat.js +65 -0
- package/service/runtime/internal_jobs/job-manager.d.ts +126 -0
- package/service/runtime/internal_jobs/job-manager.js +821 -0
- package/service/runtime/internal_jobs/key-coordinator.d.ts +280 -0
- package/service/runtime/internal_jobs/key-coordinator.js +747 -0
- package/service/runtime/internal_jobs/projection.d.ts +3 -0
- package/service/runtime/internal_jobs/projection.js +42 -0
- package/service/runtime/internal_jobs/runtime-worker.d.ts +126 -0
- package/service/runtime/internal_jobs/runtime-worker.js +454 -0
- package/service/runtime/internal_jobs/types.d.ts +246 -0
- package/service/runtime/internal_jobs/types.js +163 -0
- package/service/runtime/logger.d.ts +2 -0
- package/service/runtime/logger.js +7 -0
- package/service/runtime/runtime.d.ts +16 -0
- package/service/runtime/runtime.js +1 -0
- package/service/runtime/service.d.ts +372 -0
- package/service/runtime/service.js +1896 -0
- package/service/runtime/transfer/download.d.ts +27 -0
- package/service/runtime/transfer/download.js +1 -0
- package/service/runtime/transfer/protocol.d.ts +15 -0
- package/service/runtime/transfer/protocol.js +46 -0
- package/service/runtime/transfer/queue.d.ts +18 -0
- package/service/runtime/transfer/queue.js +136 -0
- package/service/runtime/transfer/upload.d.ts +40 -0
- package/service/runtime/transfer/upload.js +27 -0
- package/service/runtime/transfer.d.ts +62 -0
- package/service/runtime/transfer.js +763 -0
- package/session.d.ts +862 -0
- package/session.js +3407 -0
- package/store.d.ts +71 -0
- package/store.js +412 -0
- package/store_entry.d.ts +20 -0
- package/store_entry.js +75 -0
- package/tasks.d.ts +10 -0
- package/tasks.js +28 -0
- package/telemetry/carrier.d.ts +8 -0
- package/telemetry/carrier.js +20 -0
- package/telemetry/core.d.ts +7 -0
- package/telemetry/core.js +14 -0
- package/telemetry/env.d.ts +2 -0
- package/telemetry/env.js +10 -0
- package/telemetry/init.d.ts +2 -0
- package/telemetry/init.js +7 -0
- package/telemetry/metrics.d.ts +81 -0
- package/telemetry/metrics.js +267 -0
- package/telemetry/mod.d.ts +13 -0
- package/telemetry/mod.js +8 -0
- package/telemetry/nats.d.ts +6 -0
- package/telemetry/nats.js +6 -0
- package/telemetry/result.d.ts +1 -0
- package/telemetry/result.js +18 -0
- package/telemetry/runtime.d.ts +1 -0
- package/telemetry/runtime.js +141 -0
- package/telemetry/trace.d.ts +4 -0
- package/telemetry/trace.js +26 -0
- package/telemetry/trellis.d.ts +1 -0
- package/telemetry/trellis.js +1 -0
- package/telemetry.d.ts +1 -0
- package/telemetry.js +1 -0
- package/transfer.d.ts +112 -0
- package/transfer.js +525 -0
- package/transfer_protocol.d.ts +1 -0
- package/transfer_protocol.js +13 -0
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { Type } from "typebox";
|
|
2
|
+
const OpenObjectSchema = Type.Object({}, { additionalProperties: true });
|
|
3
|
+
const AppSchema = Type.Object({
|
|
4
|
+
displayName: Type.String({ minLength: 1 }),
|
|
5
|
+
}, { additionalProperties: true });
|
|
6
|
+
const ApprovalSchema = Type.Object({
|
|
7
|
+
contractId: Type.String({ minLength: 1 }),
|
|
8
|
+
contractDigest: Type.String({ minLength: 1 }),
|
|
9
|
+
displayName: Type.String({ minLength: 1 }),
|
|
10
|
+
description: Type.String(),
|
|
11
|
+
capabilities: Type.Record(Type.String(), Type.Object({
|
|
12
|
+
displayName: Type.String({ minLength: 1 }),
|
|
13
|
+
description: Type.String(),
|
|
14
|
+
})),
|
|
15
|
+
});
|
|
16
|
+
const UserSchema = Type.Object({
|
|
17
|
+
origin: Type.String({ minLength: 1 }),
|
|
18
|
+
id: Type.String({ minLength: 1 }),
|
|
19
|
+
name: Type.Optional(Type.String({ minLength: 1 })),
|
|
20
|
+
email: Type.Optional(Type.String({ minLength: 1 })),
|
|
21
|
+
image: Type.Optional(Type.String({ minLength: 1 })),
|
|
22
|
+
});
|
|
23
|
+
export const PortalFlowStateSchema = Type.Union([
|
|
24
|
+
Type.Object({
|
|
25
|
+
status: Type.Literal("choose_provider"),
|
|
26
|
+
flowId: Type.String({ minLength: 1 }),
|
|
27
|
+
providers: Type.Array(Type.Object({
|
|
28
|
+
id: Type.String({ minLength: 1 }),
|
|
29
|
+
displayName: Type.String({ minLength: 1 }),
|
|
30
|
+
})),
|
|
31
|
+
app: AppSchema,
|
|
32
|
+
portal: Type.Optional(OpenObjectSchema),
|
|
33
|
+
registration: Type.Optional(OpenObjectSchema),
|
|
34
|
+
}),
|
|
35
|
+
Type.Object({
|
|
36
|
+
status: Type.Literal("processing"),
|
|
37
|
+
flowId: Type.String({ minLength: 1 }),
|
|
38
|
+
}),
|
|
39
|
+
Type.Object({
|
|
40
|
+
status: Type.Literal("approval_required"),
|
|
41
|
+
flowId: Type.String({ minLength: 1 }),
|
|
42
|
+
consentViewDigest: Type.String({ minLength: 1 }),
|
|
43
|
+
optionalBundles: Type.Array(OpenObjectSchema),
|
|
44
|
+
user: UserSchema,
|
|
45
|
+
approval: ApprovalSchema,
|
|
46
|
+
}),
|
|
47
|
+
Type.Object({
|
|
48
|
+
status: Type.Literal("approval_denied"),
|
|
49
|
+
flowId: Type.String({ minLength: 1 }),
|
|
50
|
+
approval: ApprovalSchema,
|
|
51
|
+
returnLocation: Type.Optional(Type.String({ minLength: 1 })),
|
|
52
|
+
}),
|
|
53
|
+
Type.Object({
|
|
54
|
+
status: Type.Literal("insufficient_capabilities"),
|
|
55
|
+
flowId: Type.String({ minLength: 1 }),
|
|
56
|
+
user: Type.Optional(UserSchema),
|
|
57
|
+
approval: ApprovalSchema,
|
|
58
|
+
missingCapabilities: Type.Array(Type.String()),
|
|
59
|
+
userCapabilities: Type.Array(Type.String()),
|
|
60
|
+
returnLocation: Type.Optional(Type.String({ minLength: 1 })),
|
|
61
|
+
}),
|
|
62
|
+
Type.Object({
|
|
63
|
+
status: Type.Literal("redirect"),
|
|
64
|
+
location: Type.String({ minLength: 1 }),
|
|
65
|
+
}),
|
|
66
|
+
Type.Object({
|
|
67
|
+
status: Type.Literal("expired"),
|
|
68
|
+
returnLocation: Type.Optional(Type.String({ minLength: 1 })),
|
|
69
|
+
}),
|
|
70
|
+
]);
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { type PortalFlowState } from "./flow_types.ts";
|
|
2
|
+
export type { PortalFlowState } from "./flow_types.ts";
|
|
3
|
+
export type ApprovalDecision = "approved" | "denied";
|
|
4
|
+
export type AuthConfig = {
|
|
5
|
+
authUrl: string;
|
|
6
|
+
/** Explicit portal origin for non-browser automation; browsers supply Origin. */
|
|
7
|
+
portalOrigin?: string;
|
|
8
|
+
};
|
|
9
|
+
export type PortalBinding = {
|
|
10
|
+
secret: string;
|
|
11
|
+
digest: string;
|
|
12
|
+
};
|
|
13
|
+
/** Returns the portal browser's per-flow verifier, creating it when absent. */
|
|
14
|
+
export declare function createPortalBinding(): Promise<PortalBinding>;
|
|
15
|
+
/** Returns the portal browser's per-flow verifier, creating it when absent. */
|
|
16
|
+
export declare function getOrCreatePortalBinding(flowId: string, storage: Storage): Promise<PortalBinding>;
|
|
17
|
+
export declare function portalFlowIdFromUrl(url: URL): string | null;
|
|
18
|
+
export declare function fetchPortalFlowState(config: AuthConfig, flowId: string, binding: PortalBinding): Promise<PortalFlowState>;
|
|
19
|
+
export declare function portalProviderLoginUrl(config: AuthConfig, providerId: string, flowId: string, binding: PortalBinding): string;
|
|
20
|
+
export declare function submitPortalApproval(config: AuthConfig, flowId: string, binding: PortalBinding, decision: ApprovalDecision): Promise<PortalFlowState>;
|
|
21
|
+
export declare function portalRedirectLocation(state: PortalFlowState | null): string | null;
|
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
import { decodeTrellisHttpError } from "../http_error.js";
|
|
2
|
+
import { PortalFlowStateSchema } from "./flow_types.js";
|
|
3
|
+
import { Type } from "typebox";
|
|
4
|
+
import { Value } from "typebox/value";
|
|
5
|
+
const PORTAL_BINDING_HEADER = "trellis-portal-binding";
|
|
6
|
+
const PORTAL_BINDING_KEY_PREFIX = "trellis.portal-binding.v1:";
|
|
7
|
+
function base64Url(bytes) {
|
|
8
|
+
let binary = "";
|
|
9
|
+
for (const byte of bytes)
|
|
10
|
+
binary += String.fromCharCode(byte);
|
|
11
|
+
return btoa(binary).replaceAll("+", "-").replaceAll("/", "_").replace(/=+$/, "");
|
|
12
|
+
}
|
|
13
|
+
function decodeBase64Url(value) {
|
|
14
|
+
try {
|
|
15
|
+
const base64 = value.replaceAll("-", "+").replaceAll("_", "/");
|
|
16
|
+
const binary = atob(base64.padEnd(Math.ceil(base64.length / 4) * 4, "="));
|
|
17
|
+
return Uint8Array.from(binary, (character) => character.charCodeAt(0));
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
/** Returns the portal browser's per-flow verifier, creating it when absent. */
|
|
24
|
+
export async function createPortalBinding() {
|
|
25
|
+
const bytes = crypto.getRandomValues(new Uint8Array(32));
|
|
26
|
+
return {
|
|
27
|
+
secret: base64Url(bytes),
|
|
28
|
+
digest: base64Url(new Uint8Array(await crypto.subtle.digest("SHA-256", Uint8Array.from(bytes)))),
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
/** Returns the portal browser's per-flow verifier, creating it when absent. */
|
|
32
|
+
export async function getOrCreatePortalBinding(flowId, storage) {
|
|
33
|
+
const key = `${PORTAL_BINDING_KEY_PREFIX}${flowId}`;
|
|
34
|
+
const stored = storage.getItem(key);
|
|
35
|
+
const storedBytes = stored ? decodeBase64Url(stored) : null;
|
|
36
|
+
let bytes;
|
|
37
|
+
let secret;
|
|
38
|
+
if (stored && storedBytes?.length === 32) {
|
|
39
|
+
bytes = storedBytes;
|
|
40
|
+
secret = stored;
|
|
41
|
+
}
|
|
42
|
+
else {
|
|
43
|
+
const created = await createPortalBinding();
|
|
44
|
+
const decoded = decodeBase64Url(created.secret);
|
|
45
|
+
if (!decoded)
|
|
46
|
+
throw new Error("generated portal binding is invalid");
|
|
47
|
+
bytes = decoded;
|
|
48
|
+
secret = created.secret;
|
|
49
|
+
}
|
|
50
|
+
if (secret !== stored)
|
|
51
|
+
storage.setItem(key, secret);
|
|
52
|
+
return {
|
|
53
|
+
secret,
|
|
54
|
+
digest: base64Url(new Uint8Array(await crypto.subtle.digest("SHA-256", Uint8Array.from(bytes)))),
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
function authBaseUrl(config) {
|
|
58
|
+
return config.authUrl.replace(/\/$/, "");
|
|
59
|
+
}
|
|
60
|
+
const BrowserFlowWireProperties = {
|
|
61
|
+
flowId: Type.String({ minLength: 1 }),
|
|
62
|
+
expiresAt: Type.Integer(),
|
|
63
|
+
state: Type.Union([
|
|
64
|
+
Type.Literal("choose_provider"),
|
|
65
|
+
Type.Literal("authenticated"),
|
|
66
|
+
Type.Literal("approval_required"),
|
|
67
|
+
Type.Literal("approval_denied"),
|
|
68
|
+
Type.Literal("approved"),
|
|
69
|
+
Type.Literal("consumed"),
|
|
70
|
+
Type.Literal("expired"),
|
|
71
|
+
]),
|
|
72
|
+
providers: Type.Array(Type.String({ minLength: 1 })),
|
|
73
|
+
registrationEnabled: Type.Boolean(),
|
|
74
|
+
federatedRegistrationEnabled: Type.Boolean(),
|
|
75
|
+
consentView: Type.Object({
|
|
76
|
+
participantId: Type.String({ minLength: 1 }),
|
|
77
|
+
packageDigest: Type.String({ minLength: 1 }),
|
|
78
|
+
installedRevision: Type.Integer({ minimum: 1 }),
|
|
79
|
+
expectedGrantRevision: Type.Integer({ minimum: 0 }),
|
|
80
|
+
capabilities: Type.Array(Type.Object({
|
|
81
|
+
id: Type.String({ minLength: 1 }),
|
|
82
|
+
title: Type.String(),
|
|
83
|
+
description: Type.String(),
|
|
84
|
+
consequence: Type.String(),
|
|
85
|
+
consentDigest: Type.String({ minLength: 1 }),
|
|
86
|
+
required: Type.Boolean(),
|
|
87
|
+
eligible: Type.Boolean(),
|
|
88
|
+
alreadyApproved: Type.Boolean(),
|
|
89
|
+
})),
|
|
90
|
+
resources: Type.Array(Type.Object({
|
|
91
|
+
kind: Type.Union([
|
|
92
|
+
Type.Literal("kv"),
|
|
93
|
+
Type.Literal("state"),
|
|
94
|
+
Type.Literal("store"),
|
|
95
|
+
]),
|
|
96
|
+
name: Type.String({ minLength: 1 }),
|
|
97
|
+
title: Type.String(),
|
|
98
|
+
description: Type.String(),
|
|
99
|
+
required: Type.Boolean(),
|
|
100
|
+
requestedCommitment: Type.Record(Type.String(), Type.Unknown()),
|
|
101
|
+
actual: Type.Optional(Type.Record(Type.String(), Type.Unknown())),
|
|
102
|
+
change: Type.Union([
|
|
103
|
+
Type.Literal("new"),
|
|
104
|
+
Type.Literal("unchanged"),
|
|
105
|
+
Type.Literal("reduced"),
|
|
106
|
+
Type.Literal("expanded"),
|
|
107
|
+
Type.Literal("incompatible"),
|
|
108
|
+
Type.Literal("detached"),
|
|
109
|
+
]),
|
|
110
|
+
eligible: Type.Boolean(),
|
|
111
|
+
alreadyApproved: Type.Boolean(),
|
|
112
|
+
})),
|
|
113
|
+
companion: Type.Optional(Type.Unknown()),
|
|
114
|
+
decisionDigest: Type.String({ minLength: 1 }),
|
|
115
|
+
}),
|
|
116
|
+
redirectTarget: Type.Optional(Type.Union([
|
|
117
|
+
Type.Null(),
|
|
118
|
+
Type.String({ minLength: 1 }),
|
|
119
|
+
])),
|
|
120
|
+
};
|
|
121
|
+
const BrowserFlowWireSchema = Type.Object(BrowserFlowWireProperties);
|
|
122
|
+
const PortalFlowWireSchema = Type.Object({
|
|
123
|
+
...BrowserFlowWireProperties,
|
|
124
|
+
decisionDigest: Type.String({ minLength: 1 }),
|
|
125
|
+
user: Type.Object({
|
|
126
|
+
origin: Type.String({ minLength: 1 }),
|
|
127
|
+
id: Type.String({ minLength: 1 }),
|
|
128
|
+
name: Type.Optional(Type.String({ minLength: 1 })),
|
|
129
|
+
email: Type.Optional(Type.String({ minLength: 1 })),
|
|
130
|
+
image: Type.Optional(Type.String({ minLength: 1 })),
|
|
131
|
+
}),
|
|
132
|
+
});
|
|
133
|
+
function approval(wire) {
|
|
134
|
+
const capabilities = {};
|
|
135
|
+
for (const capability of wire.consentView.capabilities) {
|
|
136
|
+
capabilities[`capability:${capability.id}`] = {
|
|
137
|
+
displayName: capability.title || capability.id,
|
|
138
|
+
description: capability.description,
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
for (const resource of wire.consentView.resources) {
|
|
142
|
+
capabilities[`resource:${resource.kind}:${resource.name}`] = {
|
|
143
|
+
displayName: resource.title || resource.name,
|
|
144
|
+
description: resource.description,
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
return {
|
|
148
|
+
contractId: wire.consentView.participantId,
|
|
149
|
+
contractDigest: wire.consentView.packageDigest,
|
|
150
|
+
displayName: wire.consentView.participantId,
|
|
151
|
+
description: "",
|
|
152
|
+
capabilities,
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
function portalState(wire) {
|
|
156
|
+
const evidence = approval(wire);
|
|
157
|
+
let state;
|
|
158
|
+
if (wire.state === "choose_provider") {
|
|
159
|
+
const federatedProviders = wire.providers.filter((id) => id !== "local")
|
|
160
|
+
.map((id) => ({ id, displayName: id }));
|
|
161
|
+
state = {
|
|
162
|
+
status: "choose_provider",
|
|
163
|
+
flowId: wire.flowId,
|
|
164
|
+
providers: wire.providers.map((id) => ({ id, displayName: id })),
|
|
165
|
+
app: {
|
|
166
|
+
contractId: evidence.contractId,
|
|
167
|
+
contractDigest: evidence.contractDigest,
|
|
168
|
+
displayName: evidence.displayName,
|
|
169
|
+
description: evidence.description,
|
|
170
|
+
},
|
|
171
|
+
registration: {
|
|
172
|
+
localIdentity: { available: wire.registrationEnabled },
|
|
173
|
+
federatedIdentity: {
|
|
174
|
+
available: wire.federatedRegistrationEnabled,
|
|
175
|
+
providers: federatedProviders,
|
|
176
|
+
},
|
|
177
|
+
},
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
else if (wire.state === "authenticated") {
|
|
181
|
+
state = { status: "processing", flowId: wire.flowId };
|
|
182
|
+
}
|
|
183
|
+
else if (wire.state === "approval_required") {
|
|
184
|
+
if (!("user" in wire) || !("decisionDigest" in wire)) {
|
|
185
|
+
throw new Error("Authenticated portal flow requires portal binding");
|
|
186
|
+
}
|
|
187
|
+
state = {
|
|
188
|
+
status: "approval_required",
|
|
189
|
+
flowId: wire.flowId,
|
|
190
|
+
consentViewDigest: wire.decisionDigest,
|
|
191
|
+
optionalBundles: [],
|
|
192
|
+
user: wire.user,
|
|
193
|
+
approval: evidence,
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
else if (wire.state === "approval_denied") {
|
|
197
|
+
state = {
|
|
198
|
+
status: "approval_denied",
|
|
199
|
+
flowId: wire.flowId,
|
|
200
|
+
approval: evidence,
|
|
201
|
+
...(wire.redirectTarget ? { returnLocation: wire.redirectTarget } : {}),
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
else if (wire.state === "approved" || wire.state === "consumed") {
|
|
205
|
+
if (!wire.redirectTarget) {
|
|
206
|
+
throw new Error("Completed portal flow has no redirect target");
|
|
207
|
+
}
|
|
208
|
+
const location = new URL(wire.redirectTarget);
|
|
209
|
+
location.searchParams.set("flowId", wire.flowId);
|
|
210
|
+
state = { status: "redirect", location: location.toString() };
|
|
211
|
+
}
|
|
212
|
+
else if (wire.state === "expired") {
|
|
213
|
+
state = {
|
|
214
|
+
status: "expired",
|
|
215
|
+
...(wire.redirectTarget ? { returnLocation: wire.redirectTarget } : {}),
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
return Value.Parse(PortalFlowStateSchema, state);
|
|
219
|
+
}
|
|
220
|
+
async function fetchBrowserFlowWire(config, flowId) {
|
|
221
|
+
const response = await fetch(`${authBaseUrl(config)}/auth/flow/${encodeURIComponent(flowId)}`);
|
|
222
|
+
if (!response.ok) {
|
|
223
|
+
throw await decodeTrellisHttpError(response);
|
|
224
|
+
}
|
|
225
|
+
return Value.Parse(BrowserFlowWireSchema, await response.json());
|
|
226
|
+
}
|
|
227
|
+
async function fetchBoundPortalFlowWire(config, flowId, binding) {
|
|
228
|
+
const response = await fetch(`${authBaseUrl(config)}/auth/flow/${encodeURIComponent(flowId)}/portal`, {
|
|
229
|
+
method: "POST",
|
|
230
|
+
headers: {
|
|
231
|
+
...(config.portalOrigin ? { origin: config.portalOrigin } : {}),
|
|
232
|
+
[PORTAL_BINDING_HEADER]: binding.secret,
|
|
233
|
+
},
|
|
234
|
+
});
|
|
235
|
+
if (!response.ok)
|
|
236
|
+
throw await decodeTrellisHttpError(response);
|
|
237
|
+
return Value.Parse(PortalFlowWireSchema, await response.json());
|
|
238
|
+
}
|
|
239
|
+
export function portalFlowIdFromUrl(url) {
|
|
240
|
+
return url.searchParams.get("flowId");
|
|
241
|
+
}
|
|
242
|
+
export async function fetchPortalFlowState(config, flowId, binding) {
|
|
243
|
+
const flow = await fetchBrowserFlowWire(config, flowId);
|
|
244
|
+
if (flow.state !== "authenticated" && flow.state !== "approval_required") {
|
|
245
|
+
return portalState(flow);
|
|
246
|
+
}
|
|
247
|
+
return portalState(await fetchBoundPortalFlowWire(config, flowId, binding));
|
|
248
|
+
}
|
|
249
|
+
export function portalProviderLoginUrl(config, providerId, flowId, binding) {
|
|
250
|
+
const base = `${authBaseUrl(config)}/auth/login/${encodeURIComponent(providerId)}`;
|
|
251
|
+
const query = new URLSearchParams({
|
|
252
|
+
flowId,
|
|
253
|
+
portalBindingDigest: binding.digest,
|
|
254
|
+
});
|
|
255
|
+
return `${base}?${query}`;
|
|
256
|
+
}
|
|
257
|
+
export async function submitPortalApproval(config, flowId, binding, decision) {
|
|
258
|
+
const flow = await fetchBrowserFlowWire(config, flowId);
|
|
259
|
+
const wire = flow.state === "authenticated" || flow.state === "approval_required"
|
|
260
|
+
? await fetchBoundPortalFlowWire(config, flowId, binding)
|
|
261
|
+
: null;
|
|
262
|
+
if (wire?.state !== "approval_required") {
|
|
263
|
+
throw new Error("Portal flow is not awaiting approval");
|
|
264
|
+
}
|
|
265
|
+
const response = await fetch(`${authBaseUrl(config)}/auth/flow/${encodeURIComponent(flowId)}/approval`, {
|
|
266
|
+
method: "POST",
|
|
267
|
+
headers: {
|
|
268
|
+
"content-type": "application/json",
|
|
269
|
+
...(config.portalOrigin ? { origin: config.portalOrigin } : {}),
|
|
270
|
+
[PORTAL_BINDING_HEADER]: binding.secret,
|
|
271
|
+
},
|
|
272
|
+
body: JSON.stringify({
|
|
273
|
+
decision: decision === "approved" ? "approve" : "reject",
|
|
274
|
+
approval: decision === "approved"
|
|
275
|
+
? {
|
|
276
|
+
mode: "capabilities",
|
|
277
|
+
installedRevision: wire.consentView.installedRevision,
|
|
278
|
+
expectedGrantRevision: wire.consentView.expectedGrantRevision,
|
|
279
|
+
decisionDigest: wire.consentView.decisionDigest,
|
|
280
|
+
approvedCapabilities: wire.consentView.capabilities.filter((item) => item.required && item.eligible).map((item) => ({
|
|
281
|
+
id: item.id,
|
|
282
|
+
consentDigest: item.consentDigest,
|
|
283
|
+
})),
|
|
284
|
+
approvedResources: wire.consentView.resources.filter((item) => item.required && item.eligible).map((item) => ({
|
|
285
|
+
kind: item.kind,
|
|
286
|
+
name: item.name,
|
|
287
|
+
commitment: item.requestedCommitment,
|
|
288
|
+
})),
|
|
289
|
+
companionApproved: wire.consentView.companion != null,
|
|
290
|
+
}
|
|
291
|
+
: undefined,
|
|
292
|
+
}),
|
|
293
|
+
});
|
|
294
|
+
if (!response.ok) {
|
|
295
|
+
throw await decodeTrellisHttpError(response);
|
|
296
|
+
}
|
|
297
|
+
return portalState(Value.Parse(PortalFlowWireSchema, await response.json()));
|
|
298
|
+
}
|
|
299
|
+
export function portalRedirectLocation(state) {
|
|
300
|
+
if (state?.status === "redirect")
|
|
301
|
+
return state.location;
|
|
302
|
+
if (state?.status === "approval_denied")
|
|
303
|
+
return state.returnLocation ?? null;
|
|
304
|
+
if (state?.status === "expired")
|
|
305
|
+
return state.returnLocation ?? null;
|
|
306
|
+
return null;
|
|
307
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/** Browser-owned installation credential and optional remembered login. */
|
|
2
|
+
export type BrowserSessionCredential = Readonly<{
|
|
3
|
+
generation: number;
|
|
4
|
+
seed: Uint8Array;
|
|
5
|
+
sessionKey: string;
|
|
6
|
+
loginSessionId?: string;
|
|
7
|
+
expiresAt?: number | null;
|
|
8
|
+
pendingFlowId?: string;
|
|
9
|
+
}>;
|
|
10
|
+
/** Canonical identity for one participant installation at one Trellis origin. */
|
|
11
|
+
export declare function browserInstallationScope(trellisUrl: string, participantId: string): string;
|
|
12
|
+
/** Persists only a browser installation key and nullable login metadata. */
|
|
13
|
+
export declare class BrowserSessionStore {
|
|
14
|
+
#private;
|
|
15
|
+
constructor(scope: string, persistence?: "remembered" | "temporary");
|
|
16
|
+
getOrCreateCredential(now?: number): Promise<BrowserSessionCredential>;
|
|
17
|
+
readLogin(now?: number): Promise<BrowserSessionCredential | undefined>;
|
|
18
|
+
rememberFlow(expected: Pick<BrowserSessionCredential, "generation" | "sessionKey">, flowId: string): Promise<boolean>;
|
|
19
|
+
completeBind(expected: Pick<BrowserSessionCredential, "generation" | "sessionKey" | "pendingFlowId">, login: {
|
|
20
|
+
loginSessionId: string;
|
|
21
|
+
expiresAt: number | null;
|
|
22
|
+
}): Promise<boolean>;
|
|
23
|
+
clearLogin(expected: Pick<BrowserSessionCredential, "generation" | "sessionKey"> & {
|
|
24
|
+
loginSessionId?: string;
|
|
25
|
+
}): Promise<boolean>;
|
|
26
|
+
}
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
import { publicKeyBase64urlFromSeed } from "../keys.js";
|
|
2
|
+
const DB_NAME = "trellis-auth";
|
|
3
|
+
const DB_VERSION = 3;
|
|
4
|
+
const STORE_NAME = "installations";
|
|
5
|
+
const temporaryInstallations = new Map();
|
|
6
|
+
function openDB() {
|
|
7
|
+
return new Promise((resolve, reject) => {
|
|
8
|
+
const request = indexedDB.open(DB_NAME, DB_VERSION);
|
|
9
|
+
request.onerror = () => reject(request.error);
|
|
10
|
+
request.onsuccess = () => resolve(request.result);
|
|
11
|
+
request.onupgradeneeded = () => {
|
|
12
|
+
const db = request.result;
|
|
13
|
+
if (!db.objectStoreNames.contains(STORE_NAME)) {
|
|
14
|
+
db.createObjectStore(STORE_NAME, { keyPath: "id" });
|
|
15
|
+
}
|
|
16
|
+
};
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
function validGeneration(value) {
|
|
20
|
+
return Number.isSafeInteger(value) && Number(value) >= 0;
|
|
21
|
+
}
|
|
22
|
+
function credential(record) {
|
|
23
|
+
if (!record || !validGeneration(record.generation) ||
|
|
24
|
+
!(record.seed instanceof Uint8Array) || !record.sessionKey)
|
|
25
|
+
return undefined;
|
|
26
|
+
return {
|
|
27
|
+
generation: record.generation,
|
|
28
|
+
seed: record.seed.slice(),
|
|
29
|
+
sessionKey: record.sessionKey,
|
|
30
|
+
...(record.loginSessionId === undefined
|
|
31
|
+
? {}
|
|
32
|
+
: { loginSessionId: record.loginSessionId }),
|
|
33
|
+
...(record.expiresAt === undefined ? {} : { expiresAt: record.expiresAt }),
|
|
34
|
+
...(record.pendingFlowId === undefined
|
|
35
|
+
? {}
|
|
36
|
+
: { pendingFlowId: record.pendingFlowId }),
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
function expired(record, now) {
|
|
40
|
+
return record.loginSessionId !== undefined && record.expiresAt !== null &&
|
|
41
|
+
record.expiresAt !== undefined && record.expiresAt <= now;
|
|
42
|
+
}
|
|
43
|
+
function tombstone(record) {
|
|
44
|
+
return {
|
|
45
|
+
id: record.id,
|
|
46
|
+
generation: validGeneration(record.generation) ? record.generation + 1 : 0,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
/** Canonical identity for one participant installation at one Trellis origin. */
|
|
50
|
+
export function browserInstallationScope(trellisUrl, participantId) {
|
|
51
|
+
return JSON.stringify([
|
|
52
|
+
"trellis.browser-installation.v2",
|
|
53
|
+
new URL(trellisUrl).origin,
|
|
54
|
+
participantId,
|
|
55
|
+
]);
|
|
56
|
+
}
|
|
57
|
+
/** Persists only a browser installation key and nullable login metadata. */
|
|
58
|
+
export class BrowserSessionStore {
|
|
59
|
+
#id;
|
|
60
|
+
#temporary;
|
|
61
|
+
constructor(scope, persistence = "remembered") {
|
|
62
|
+
if (!scope.trim() || scope.length > 4_096) {
|
|
63
|
+
throw new Error("browser installation scope is invalid");
|
|
64
|
+
}
|
|
65
|
+
this.#id = scope;
|
|
66
|
+
this.#temporary = persistence === "temporary";
|
|
67
|
+
}
|
|
68
|
+
async getOrCreateCredential(now = Date.now()) {
|
|
69
|
+
const candidateSeed = crypto.getRandomValues(new Uint8Array(32));
|
|
70
|
+
const candidateSessionKey = publicKeyBase64urlFromSeed(candidateSeed);
|
|
71
|
+
if (this.#temporary) {
|
|
72
|
+
let current = temporaryInstallations.get(this.#id);
|
|
73
|
+
if (current && expired(current, now)) {
|
|
74
|
+
current = tombstone(current);
|
|
75
|
+
temporaryInstallations.set(this.#id, current);
|
|
76
|
+
}
|
|
77
|
+
const existing = credential(current);
|
|
78
|
+
if (existing)
|
|
79
|
+
return existing;
|
|
80
|
+
const created = {
|
|
81
|
+
id: this.#id,
|
|
82
|
+
generation: validGeneration(current?.generation)
|
|
83
|
+
? current.generation
|
|
84
|
+
: 0,
|
|
85
|
+
seed: candidateSeed,
|
|
86
|
+
sessionKey: candidateSessionKey,
|
|
87
|
+
};
|
|
88
|
+
temporaryInstallations.set(this.#id, created);
|
|
89
|
+
return credential(created);
|
|
90
|
+
}
|
|
91
|
+
const db = await openDB();
|
|
92
|
+
return await new Promise((resolve, reject) => {
|
|
93
|
+
let result;
|
|
94
|
+
const tx = db.transaction(STORE_NAME, "readwrite");
|
|
95
|
+
const store = tx.objectStore(STORE_NAME);
|
|
96
|
+
const request = store.get(this.#id);
|
|
97
|
+
request.onerror = () => reject(request.error);
|
|
98
|
+
request.onsuccess = () => {
|
|
99
|
+
let current = request.result;
|
|
100
|
+
if (current && expired(current, now))
|
|
101
|
+
current = tombstone(current);
|
|
102
|
+
const existing = credential(current);
|
|
103
|
+
if (existing) {
|
|
104
|
+
result = existing;
|
|
105
|
+
if (current !== request.result)
|
|
106
|
+
store.put(current);
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
const created = {
|
|
110
|
+
id: this.#id,
|
|
111
|
+
generation: validGeneration(current?.generation)
|
|
112
|
+
? current.generation
|
|
113
|
+
: 0,
|
|
114
|
+
seed: candidateSeed,
|
|
115
|
+
sessionKey: candidateSessionKey,
|
|
116
|
+
};
|
|
117
|
+
store.put(created);
|
|
118
|
+
result = credential(created);
|
|
119
|
+
};
|
|
120
|
+
tx.oncomplete = () => {
|
|
121
|
+
db.close();
|
|
122
|
+
resolve(result);
|
|
123
|
+
};
|
|
124
|
+
tx.onerror = () => reject(tx.error);
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
async readLogin(now = Date.now()) {
|
|
128
|
+
if (this.#temporary) {
|
|
129
|
+
const current = temporaryInstallations.get(this.#id);
|
|
130
|
+
if (!current)
|
|
131
|
+
return undefined;
|
|
132
|
+
if (expired(current, now)) {
|
|
133
|
+
temporaryInstallations.set(this.#id, tombstone(current));
|
|
134
|
+
return undefined;
|
|
135
|
+
}
|
|
136
|
+
return credential(current);
|
|
137
|
+
}
|
|
138
|
+
const db = await openDB();
|
|
139
|
+
return await new Promise((resolve, reject) => {
|
|
140
|
+
let result;
|
|
141
|
+
const tx = db.transaction(STORE_NAME, "readwrite");
|
|
142
|
+
const store = tx.objectStore(STORE_NAME);
|
|
143
|
+
const request = store.get(this.#id);
|
|
144
|
+
request.onerror = () => reject(request.error);
|
|
145
|
+
request.onsuccess = () => {
|
|
146
|
+
const current = request.result;
|
|
147
|
+
if (current && expired(current, now)) {
|
|
148
|
+
store.put(tombstone(current));
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
result = credential(current);
|
|
152
|
+
};
|
|
153
|
+
tx.oncomplete = () => {
|
|
154
|
+
db.close();
|
|
155
|
+
resolve(result);
|
|
156
|
+
};
|
|
157
|
+
tx.onerror = () => reject(tx.error);
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
rememberFlow(expected, flowId) {
|
|
161
|
+
return this.#update((current) => {
|
|
162
|
+
if (current.generation !== expected.generation ||
|
|
163
|
+
current.sessionKey !== expected.sessionKey)
|
|
164
|
+
return undefined;
|
|
165
|
+
return { ...current, pendingFlowId: flowId };
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
completeBind(expected, login) {
|
|
169
|
+
return this.#update((current) => {
|
|
170
|
+
if (current.generation !== expected.generation ||
|
|
171
|
+
current.sessionKey !== expected.sessionKey ||
|
|
172
|
+
!expected.pendingFlowId ||
|
|
173
|
+
current.pendingFlowId !== expected.pendingFlowId)
|
|
174
|
+
return undefined;
|
|
175
|
+
const { pendingFlowId: _, ...retained } = current;
|
|
176
|
+
return { ...retained, ...login };
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
clearLogin(expected) {
|
|
180
|
+
return this.#update((current) => {
|
|
181
|
+
if (current.generation !== expected.generation ||
|
|
182
|
+
current.sessionKey !== expected.sessionKey ||
|
|
183
|
+
(expected.loginSessionId !== undefined &&
|
|
184
|
+
current.loginSessionId !== expected.loginSessionId))
|
|
185
|
+
return undefined;
|
|
186
|
+
return tombstone(current);
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
async #update(update) {
|
|
190
|
+
if (this.#temporary) {
|
|
191
|
+
const current = temporaryInstallations.get(this.#id);
|
|
192
|
+
if (!current)
|
|
193
|
+
return false;
|
|
194
|
+
const next = update(current);
|
|
195
|
+
if (!next)
|
|
196
|
+
return false;
|
|
197
|
+
temporaryInstallations.set(this.#id, next);
|
|
198
|
+
return true;
|
|
199
|
+
}
|
|
200
|
+
const db = await openDB();
|
|
201
|
+
return await new Promise((resolve, reject) => {
|
|
202
|
+
let changed = false;
|
|
203
|
+
const tx = db.transaction(STORE_NAME, "readwrite");
|
|
204
|
+
const store = tx.objectStore(STORE_NAME);
|
|
205
|
+
const request = store.get(this.#id);
|
|
206
|
+
request.onerror = () => reject(request.error);
|
|
207
|
+
request.onsuccess = () => {
|
|
208
|
+
const current = request.result;
|
|
209
|
+
if (!current)
|
|
210
|
+
return;
|
|
211
|
+
const next = update(current);
|
|
212
|
+
if (!next)
|
|
213
|
+
return;
|
|
214
|
+
store.put(next);
|
|
215
|
+
changed = true;
|
|
216
|
+
};
|
|
217
|
+
tx.oncomplete = () => {
|
|
218
|
+
db.close();
|
|
219
|
+
resolve(changed);
|
|
220
|
+
};
|
|
221
|
+
tx.onerror = () => reject(tx.error);
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module
|
|
3
|
+
* Browser-based authentication utilities for session-key based authentication.
|
|
4
|
+
* Uses WebCrypto API and IndexedDB for secure key storage.
|
|
5
|
+
*/
|
|
6
|
+
export { type ApprovalDecision, createPortalBinding, fetchPortalFlowState, getOrCreatePortalBinding, type PortalBinding, portalFlowIdFromUrl, type PortalFlowState, type PortalFlowState as BrowserPortalFlowState, portalProviderLoginUrl, portalRedirectLocation, submitPortalApproval, } from "./browser/portal.ts";
|
|
7
|
+
export { BrowserSessionStore } from "./browser/storage.ts";
|
|
8
|
+
export { classifyBrowserAuthError, isRecoverableBrowserAuthError, } from "./browser_recovery.ts";
|
|
9
|
+
export type { BrowserAuthRecoveryClassification, BrowserAuthRecoveryKind, } from "./browser_recovery.ts";
|
|
10
|
+
export { decodeTrellisHttpError, TrellisHttpError } from "./http_error.ts";
|
|
11
|
+
export { base64urlDecode, base64urlEncode, sha256, toArrayBuffer, utf8, } from "./utils.ts";
|
package/auth/browser.js
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module
|
|
3
|
+
* Browser-based authentication utilities for session-key based authentication.
|
|
4
|
+
* Uses WebCrypto API and IndexedDB for secure key storage.
|
|
5
|
+
*/
|
|
6
|
+
export { createPortalBinding, fetchPortalFlowState, getOrCreatePortalBinding, portalFlowIdFromUrl, portalProviderLoginUrl, portalRedirectLocation, submitPortalApproval, } from "./browser/portal.js";
|
|
7
|
+
export { BrowserSessionStore } from "./browser/storage.js";
|
|
8
|
+
export { classifyBrowserAuthError, isRecoverableBrowserAuthError, } from "./browser_recovery.js";
|
|
9
|
+
export { decodeTrellisHttpError, TrellisHttpError } from "./http_error.js";
|
|
10
|
+
export { base64urlDecode, base64urlEncode, sha256, toArrayBuffer, utf8, } from "./utils.js";
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/** Stable browser-auth recovery categories for app-owned recovery flows. */
|
|
2
|
+
export type BrowserAuthRecoveryKind = "recoverable_stale_session" | "recoverable_expired_flow" | "recoverable_auth_required" | "policy_denied" | "insufficient_capabilities" | "runtime_unavailable" | "unknown";
|
|
3
|
+
/** Classification result for a browser-auth related failure. */
|
|
4
|
+
export type BrowserAuthRecoveryClassification = {
|
|
5
|
+
kind: BrowserAuthRecoveryKind;
|
|
6
|
+
recoverable: boolean;
|
|
7
|
+
reason?: string;
|
|
8
|
+
code?: string;
|
|
9
|
+
};
|
|
10
|
+
/** Classifies an exact auth machine error code. */
|
|
11
|
+
export declare function classifyBrowserAuthError(error: unknown): BrowserAuthRecoveryClassification;
|
|
12
|
+
/** Returns whether a browser auth error can silently restart sign-in. */
|
|
13
|
+
export declare function isRecoverableBrowserAuthError(error: unknown): boolean;
|