@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
package/device.js
ADDED
|
@@ -0,0 +1,687 @@
|
|
|
1
|
+
export { checkDeviceActivation } from "./device/activation.js";
|
|
2
|
+
import { AsyncResult, Result, UnexpectedError, } from "@oatscenter/result";
|
|
3
|
+
import { ulid } from "ulid";
|
|
4
|
+
import { decodeTrellisHttpError, TrellisHttpError } from "./auth/http_error.js";
|
|
5
|
+
import { connectClientWithDeps, } from "./client_connect.js";
|
|
6
|
+
import { ContractResourceBindingsSchema, } from "./participant.js";
|
|
7
|
+
import { deriveDeviceConfirmationCode, deriveDeviceIdentity, deriveDeviceUserCompanion, requestDeviceEnrollment, verifyDeviceConfirmationCode, waitForDeviceActivation, } from "./auth/device_activation.js";
|
|
8
|
+
import { sessionProofRequestDigest } from "./auth/session_proof.js";
|
|
9
|
+
import { base64urlDecode, base64urlEncode, canonicalizeJsonValue, sha256, utf8, } from "./auth/utils.js";
|
|
10
|
+
import { estimateMidpointClockOffsetMs } from "./auth/time.js";
|
|
11
|
+
import { createAuth } from "./auth/session_auth.js";
|
|
12
|
+
import { AuthorizationContextRefreshResponseSchema } from "./auth/authorization/types.js";
|
|
13
|
+
import { DEFAULT_RUNTIME_MAX_RECONNECT_ATTEMPTS, loadDefaultRuntimeTransport, selectRuntimeTransportServers, } from "./runtime_transport.js";
|
|
14
|
+
import { ServiceHealthRuntime, } from "./service/runtime/health.js";
|
|
15
|
+
import { publishHealthHeartbeatSample } from "./health_transport.js";
|
|
16
|
+
import { Trellis } from "./session.js";
|
|
17
|
+
import { logger as noopLogger } from "./globals.js";
|
|
18
|
+
import { TransportError } from "./errors/index.js";
|
|
19
|
+
import { Type } from "typebox";
|
|
20
|
+
import { Value } from "typebox/value";
|
|
21
|
+
import { installConnectionAvailability, observeNatsTrellisConnection, } from "./connection.js";
|
|
22
|
+
import { AuthorizationContextCache, AuthorizationContextRefreshError, AuthorizationProviderCache, startAuthorizationContextRefresh, } from "./auth/authorization_context.js";
|
|
23
|
+
import { createCallerRuntime } from "./caller.js";
|
|
24
|
+
import { bindApiRoutes, getParticipantRuntime, participantAvailability, participantEvidence, refreshApiRoutes, } from "./participant_runtime/participant.js";
|
|
25
|
+
function deviceConnectResult(promise) {
|
|
26
|
+
return AsyncResult.from(promise.then((value) => Result.ok(value), (cause) => Result.err(cause instanceof TransportError
|
|
27
|
+
? cause
|
|
28
|
+
: new UnexpectedError({ cause }))));
|
|
29
|
+
}
|
|
30
|
+
const DeviceBootstrapInstallationSchema = Type.Object({
|
|
31
|
+
...AuthorizationContextRefreshResponseSchema.properties,
|
|
32
|
+
authorization: Type.Object({
|
|
33
|
+
participantId: Type.String({ minLength: 1 }),
|
|
34
|
+
participantDigest: Type.String({ minLength: 1 }),
|
|
35
|
+
resourceRuntime: ContractResourceBindingsSchema,
|
|
36
|
+
}),
|
|
37
|
+
});
|
|
38
|
+
const DeviceBootstrapReadySchema = Type.Object({
|
|
39
|
+
...DeviceBootstrapInstallationSchema.properties,
|
|
40
|
+
companion: Type.Optional(Type.Object({
|
|
41
|
+
participantId: Type.String({ minLength: 1 }),
|
|
42
|
+
loginSessionId: Type.String({ minLength: 1 }),
|
|
43
|
+
required: Type.Boolean(),
|
|
44
|
+
installation: DeviceBootstrapInstallationSchema,
|
|
45
|
+
})),
|
|
46
|
+
});
|
|
47
|
+
function normalizeRootSecret(rootSecret) {
|
|
48
|
+
if (typeof rootSecret === "string") {
|
|
49
|
+
const decoded = base64urlDecode(rootSecret.trim());
|
|
50
|
+
if (decoded.length === 0)
|
|
51
|
+
throw new Error("rootSecret must not be empty");
|
|
52
|
+
return decoded;
|
|
53
|
+
}
|
|
54
|
+
if (rootSecret.length === 0)
|
|
55
|
+
throw new Error("rootSecret must not be empty");
|
|
56
|
+
return rootSecret;
|
|
57
|
+
}
|
|
58
|
+
const defaultDeps = {
|
|
59
|
+
loadTransport: loadDefaultRuntimeTransport,
|
|
60
|
+
now: () => Date.now(),
|
|
61
|
+
};
|
|
62
|
+
function transportCauseContext(cause) {
|
|
63
|
+
if (cause instanceof Error) {
|
|
64
|
+
return { causeName: cause.name, causeMessage: cause.message };
|
|
65
|
+
}
|
|
66
|
+
return { cause: String(cause) };
|
|
67
|
+
}
|
|
68
|
+
function createTransportError(args) {
|
|
69
|
+
return new TransportError({
|
|
70
|
+
code: args.code,
|
|
71
|
+
message: args.message,
|
|
72
|
+
hint: args.hint,
|
|
73
|
+
cause: args.cause,
|
|
74
|
+
context: {
|
|
75
|
+
...(args.context ?? {}),
|
|
76
|
+
...(args.cause === undefined ? {} : transportCauseContext(args.cause)),
|
|
77
|
+
},
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
async function readJsonResponse(response, args) {
|
|
81
|
+
try {
|
|
82
|
+
return await response.json();
|
|
83
|
+
}
|
|
84
|
+
catch (cause) {
|
|
85
|
+
throw createTransportError({
|
|
86
|
+
...args,
|
|
87
|
+
cause,
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
function resolveDeviceLogger(log) {
|
|
92
|
+
if (log === false) {
|
|
93
|
+
return noopLogger;
|
|
94
|
+
}
|
|
95
|
+
return log ?? noopLogger;
|
|
96
|
+
}
|
|
97
|
+
function createInvalidConfirmationCodeTransportError(context) {
|
|
98
|
+
return createTransportError({
|
|
99
|
+
code: "trellis.device.invalid_confirmation_code",
|
|
100
|
+
message: "The device confirmation code is invalid.",
|
|
101
|
+
hint: "Retry with the current confirmation code for this activation, or restart activation if the code is no longer valid.",
|
|
102
|
+
context,
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
function createActivatedLocalState(localState) {
|
|
106
|
+
return {
|
|
107
|
+
...localState,
|
|
108
|
+
status: "activated",
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
function assertActivationStateMatchesIdentity(args) {
|
|
112
|
+
if (args.localState.publicIdentityKey !== args.publicIdentityKey) {
|
|
113
|
+
throw createTransportError({
|
|
114
|
+
code: "trellis.device.activation_state_mismatch",
|
|
115
|
+
message: "Local device activation state does not match the provided root secret.",
|
|
116
|
+
hint: "Use the activation state for the same device identity, or start a new activation for this root secret.",
|
|
117
|
+
context: {
|
|
118
|
+
statePublicIdentityKey: args.localState.publicIdentityKey,
|
|
119
|
+
publicIdentityKey: args.publicIdentityKey,
|
|
120
|
+
},
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
function assertActivationStateMatchesContract(args) {
|
|
125
|
+
if (args.localState.participantId !== args.participantId) {
|
|
126
|
+
throw createTransportError({
|
|
127
|
+
code: "trellis.device.activation_state_contract_mismatch",
|
|
128
|
+
message: "Local device activation state does not match the requested device participant.",
|
|
129
|
+
hint: "Use activation state for the same device contract, or start activation again for this contract digest.",
|
|
130
|
+
context: {
|
|
131
|
+
stateParticipantId: args.localState.participantId,
|
|
132
|
+
participantId: args.participantId,
|
|
133
|
+
},
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
function createActivationSession(args) {
|
|
138
|
+
assertActivationStateMatchesIdentity({
|
|
139
|
+
localState: args.localState,
|
|
140
|
+
publicIdentityKey: args.identity.publicIdentityKey,
|
|
141
|
+
});
|
|
142
|
+
assertActivationStateMatchesContract({
|
|
143
|
+
localState: args.localState,
|
|
144
|
+
participantId: args.participantId,
|
|
145
|
+
});
|
|
146
|
+
const activatedState = createActivatedLocalState(args.localState);
|
|
147
|
+
return {
|
|
148
|
+
activationUrl: args.localState.activationUrl,
|
|
149
|
+
localState: args.localState,
|
|
150
|
+
waitForOnlineApproval: async (opts) => {
|
|
151
|
+
if (args.localState.status === "activated") {
|
|
152
|
+
return activatedState;
|
|
153
|
+
}
|
|
154
|
+
await waitForDeviceActivation({
|
|
155
|
+
trellisUrl: args.trellisUrl,
|
|
156
|
+
publicIdentityKey: args.identity.publicIdentityKey,
|
|
157
|
+
identitySeed: args.identity.identitySeed,
|
|
158
|
+
activationKey: args.identity.activationKey,
|
|
159
|
+
participantId: args.provisioned.participantId,
|
|
160
|
+
...participantEvidence(args.participant),
|
|
161
|
+
provisioningSecret: args.provisioned.provisioningSecret ?? undefined,
|
|
162
|
+
nonce: args.localState.nonce,
|
|
163
|
+
signal: opts?.signal,
|
|
164
|
+
sessionIdentity: args.sessionIdentity,
|
|
165
|
+
connectionId: args.connectionId,
|
|
166
|
+
companion: args.companion,
|
|
167
|
+
});
|
|
168
|
+
return activatedState;
|
|
169
|
+
},
|
|
170
|
+
acceptConfirmationCode: async (code) => {
|
|
171
|
+
if (args.localState.status === "activated") {
|
|
172
|
+
return activatedState;
|
|
173
|
+
}
|
|
174
|
+
const ok = await verifyDeviceConfirmationCode({
|
|
175
|
+
activationKey: args.identity.activationKey,
|
|
176
|
+
publicIdentityKey: args.identity.publicIdentityKey,
|
|
177
|
+
nonce: args.localState.nonce,
|
|
178
|
+
confirmationCode: code,
|
|
179
|
+
});
|
|
180
|
+
if (!ok) {
|
|
181
|
+
throw createInvalidConfirmationCodeTransportError({
|
|
182
|
+
publicIdentityKey: args.identity.publicIdentityKey,
|
|
183
|
+
instanceId: args.localState.instanceId,
|
|
184
|
+
deploymentId: args.localState.deploymentId,
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
return activatedState;
|
|
188
|
+
},
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
async function fetchDeviceBootstrap(args) {
|
|
192
|
+
const sessionAuth = args.sessionAuth ?? await createAuth({
|
|
193
|
+
sessionKeySeed: base64urlEncode(crypto.getRandomValues(new Uint8Array(32))),
|
|
194
|
+
});
|
|
195
|
+
const requestStartedAtMs = args.now();
|
|
196
|
+
const issuedAt = Math.trunc(requestStartedAtMs + args.offsetState.serverClockOffsetMs);
|
|
197
|
+
const requestId = ulid();
|
|
198
|
+
const identityAuth = await createAuth({
|
|
199
|
+
sessionKeySeed: base64urlEncode(args.deviceIdentity.identitySeed),
|
|
200
|
+
});
|
|
201
|
+
const deviceIdentityKeyId = base64urlEncode(await sha256(base64urlDecode(identityAuth.sessionKey)));
|
|
202
|
+
const unsigned = {
|
|
203
|
+
identityKeyId: deviceIdentityKeyId,
|
|
204
|
+
sessionKey: sessionAuth.sessionKey,
|
|
205
|
+
connectionId: args.connectionId ?? ulid(),
|
|
206
|
+
requestId,
|
|
207
|
+
iat: issuedAt,
|
|
208
|
+
name: args.participant.identity,
|
|
209
|
+
...participantEvidence(args.participant),
|
|
210
|
+
};
|
|
211
|
+
const descriptor = args.participant.companion;
|
|
212
|
+
let companionInstallationSeedBase64url;
|
|
213
|
+
let companionSessionSeedBase64url;
|
|
214
|
+
if (descriptor) {
|
|
215
|
+
const installation = await deriveDeviceUserCompanion(args.rootSecret, args.trellisUrl, descriptor.participant.identity);
|
|
216
|
+
companionInstallationSeedBase64url = installation.installationSeedBase64url;
|
|
217
|
+
const companionAuth = await createAuth({
|
|
218
|
+
sessionKeySeed: installation.installationSeedBase64url,
|
|
219
|
+
});
|
|
220
|
+
companionSessionSeedBase64url = base64urlEncode(crypto.getRandomValues(new Uint8Array(32)));
|
|
221
|
+
const companionSessionAuth = await createAuth({
|
|
222
|
+
sessionKeySeed: companionSessionSeedBase64url,
|
|
223
|
+
});
|
|
224
|
+
const companionRequest = {
|
|
225
|
+
connectionId: ulid(),
|
|
226
|
+
requestId: ulid(),
|
|
227
|
+
issuedAt,
|
|
228
|
+
sessionKey: companionSessionAuth.sessionKey,
|
|
229
|
+
};
|
|
230
|
+
const digest = base64urlEncode(await sha256(utf8(canonicalizeJsonValue({
|
|
231
|
+
format: "trellis.device.user-companion.v1",
|
|
232
|
+
origin: new URL(args.trellisUrl).origin,
|
|
233
|
+
identityKeyId: deviceIdentityKeyId,
|
|
234
|
+
participantId: descriptor.participant.identity,
|
|
235
|
+
...companionRequest,
|
|
236
|
+
}))));
|
|
237
|
+
unsigned.companion = {
|
|
238
|
+
...companionRequest,
|
|
239
|
+
proof: base64urlEncode(await companionAuth.sign(base64urlDecode(digest))),
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
const response = await fetch(new URL("/bootstrap/device", args.trellisUrl), {
|
|
243
|
+
method: "POST",
|
|
244
|
+
signal: args.signal,
|
|
245
|
+
headers: { "Content-Type": "application/json" },
|
|
246
|
+
body: JSON.stringify({
|
|
247
|
+
...unsigned,
|
|
248
|
+
proof: await identityAuth.signSessionProof({
|
|
249
|
+
purpose: "deviceBootstrap",
|
|
250
|
+
origin: new URL(args.trellisUrl).origin,
|
|
251
|
+
unsignedRequest: unsigned,
|
|
252
|
+
}),
|
|
253
|
+
}),
|
|
254
|
+
});
|
|
255
|
+
const responseReceivedAtMs = args.now();
|
|
256
|
+
if (!response.ok)
|
|
257
|
+
throw await decodeTrellisHttpError(response);
|
|
258
|
+
const payload = await readJsonResponse(response, {
|
|
259
|
+
code: "trellis.bootstrap.invalid_response",
|
|
260
|
+
message: "Trellis returned an invalid device bootstrap response.",
|
|
261
|
+
hint: "Retry the connection or complete device activation.",
|
|
262
|
+
context: { trellisUrl: args.trellisUrl },
|
|
263
|
+
});
|
|
264
|
+
const ready = Value.Parse(DeviceBootstrapReadySchema, payload);
|
|
265
|
+
args.offsetState.serverClockOffsetMs = estimateMidpointClockOffsetMs({
|
|
266
|
+
requestStartedAtMs,
|
|
267
|
+
responseReceivedAtMs,
|
|
268
|
+
serverNowSeconds: ready.serverNow / 1_000,
|
|
269
|
+
});
|
|
270
|
+
sessionAuth.setServerClockOffsetMs(args.offsetState.serverClockOffsetMs + args.now() - Date.now());
|
|
271
|
+
return {
|
|
272
|
+
status: "ready",
|
|
273
|
+
sessionAuth,
|
|
274
|
+
connectInfo: {
|
|
275
|
+
connectionId: ready.runtime.connectionId,
|
|
276
|
+
participantId: ready.runtime.participantId,
|
|
277
|
+
participantDigest: ready.authorization.participantDigest,
|
|
278
|
+
transports: ready.transports,
|
|
279
|
+
transport: {
|
|
280
|
+
jwt: ready.routing.bootstrapJwt,
|
|
281
|
+
jwtExpiresAt: ready.routing.bootstrapJwtExpiresAt,
|
|
282
|
+
inboxPrefix: ready.runtime.inboxPrefix,
|
|
283
|
+
},
|
|
284
|
+
authorizationContext: ready.authorizationContext,
|
|
285
|
+
apiBindings: ready.apiBindings,
|
|
286
|
+
resourceBindings: ready.authorization.resourceRuntime,
|
|
287
|
+
},
|
|
288
|
+
...(ready.companion && companionInstallationSeedBase64url &&
|
|
289
|
+
companionSessionSeedBase64url
|
|
290
|
+
? {
|
|
291
|
+
companion: {
|
|
292
|
+
...ready.companion,
|
|
293
|
+
installationSeedBase64url: companionInstallationSeedBase64url,
|
|
294
|
+
sessionSeedBase64url: companionSessionSeedBase64url,
|
|
295
|
+
bootstrap: ready.companion.installation,
|
|
296
|
+
},
|
|
297
|
+
}
|
|
298
|
+
: {}),
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
/**
|
|
302
|
+
* @internal Exported for focused tests and platform-specific wrappers.
|
|
303
|
+
*/
|
|
304
|
+
export async function startDeviceActivationWithDeps(args, deps) {
|
|
305
|
+
const rootSecret = normalizeRootSecret(args.rootSecret);
|
|
306
|
+
const identity = await deriveDeviceIdentity(rootSecret);
|
|
307
|
+
const nonce = base64urlEncode(crypto.getRandomValues(new Uint8Array(32)));
|
|
308
|
+
const sessionIdentity = await createAuth({
|
|
309
|
+
sessionKeySeed: base64urlEncode(crypto.getRandomValues(new Uint8Array(32))),
|
|
310
|
+
});
|
|
311
|
+
const connectionId = ulid();
|
|
312
|
+
const companion = args.participant.companion;
|
|
313
|
+
if (companion && companion.participant.kind !== "app" &&
|
|
314
|
+
companion.participant.kind !== "agent") {
|
|
315
|
+
throw new Error("device companion must be an app or agent");
|
|
316
|
+
}
|
|
317
|
+
const companionIdentity = companion
|
|
318
|
+
? {
|
|
319
|
+
participantId: companion.participant.identity,
|
|
320
|
+
kind: companion.participant.kind,
|
|
321
|
+
installation: await deriveDeviceUserCompanion(rootSecret, args.trellisUrl, companion.participant.identity),
|
|
322
|
+
}
|
|
323
|
+
: undefined;
|
|
324
|
+
const activation = await requestDeviceEnrollment({
|
|
325
|
+
trellisUrl: args.trellisUrl,
|
|
326
|
+
publicIdentityKey: identity.publicIdentityKey,
|
|
327
|
+
identitySeed: identity.identitySeed,
|
|
328
|
+
sessionIdentity,
|
|
329
|
+
connectionId,
|
|
330
|
+
participantId: args.participant.identity,
|
|
331
|
+
...participantEvidence(args.participant),
|
|
332
|
+
provisioningSecret: args.provisioningSecret,
|
|
333
|
+
challengeDigest: base64urlEncode(await sha256(utf8(nonce))),
|
|
334
|
+
confirmationCode: await deriveDeviceConfirmationCode({
|
|
335
|
+
activationKey: identity.activationKey,
|
|
336
|
+
publicIdentityKey: identity.publicIdentityKey,
|
|
337
|
+
nonce,
|
|
338
|
+
}),
|
|
339
|
+
companion: companionIdentity,
|
|
340
|
+
});
|
|
341
|
+
const activationState = activation.activation;
|
|
342
|
+
if (activation.state !== "pending" ||
|
|
343
|
+
typeof activationState?.reviewId !== "string" ||
|
|
344
|
+
typeof activationState.activationUrl !== "string" ||
|
|
345
|
+
typeof activationState.principalId !== "string" ||
|
|
346
|
+
typeof activationState.deploymentId !== "string" ||
|
|
347
|
+
typeof activationState.instanceId !== "string") {
|
|
348
|
+
throw createTransportError({
|
|
349
|
+
code: "device_activation_unavailable",
|
|
350
|
+
message: "The device does not require activation.",
|
|
351
|
+
hint: "Connect the device directly.",
|
|
352
|
+
context: { status: activation.state },
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
return await createActivationSession({
|
|
356
|
+
trellisUrl: args.trellisUrl,
|
|
357
|
+
participantId: args.participant.identity,
|
|
358
|
+
identity,
|
|
359
|
+
provisioned: {
|
|
360
|
+
participantId: args.participant.identity,
|
|
361
|
+
provisioningSecret: args.provisioningSecret,
|
|
362
|
+
},
|
|
363
|
+
participant: args.participant,
|
|
364
|
+
now: deps.now,
|
|
365
|
+
localState: {
|
|
366
|
+
status: "pending",
|
|
367
|
+
participantId: args.participant.identity,
|
|
368
|
+
publicIdentityKey: identity.publicIdentityKey,
|
|
369
|
+
instanceId: activationState.instanceId,
|
|
370
|
+
deploymentId: activationState.deploymentId,
|
|
371
|
+
flowId: activationState.reviewId,
|
|
372
|
+
nonce,
|
|
373
|
+
activationUrl: activationState.activationUrl,
|
|
374
|
+
},
|
|
375
|
+
sessionIdentity,
|
|
376
|
+
connectionId,
|
|
377
|
+
companion: companionIdentity,
|
|
378
|
+
});
|
|
379
|
+
}
|
|
380
|
+
/**
|
|
381
|
+
* @internal Exported for focused tests and platform-specific wrappers.
|
|
382
|
+
*/
|
|
383
|
+
export async function resumeDeviceActivationWithDeps(args, deps) {
|
|
384
|
+
const rootSecret = normalizeRootSecret(args.rootSecret);
|
|
385
|
+
const identity = await deriveDeviceIdentity(rootSecret);
|
|
386
|
+
const companion = args.participant.companion;
|
|
387
|
+
return await createActivationSession({
|
|
388
|
+
trellisUrl: args.trellisUrl,
|
|
389
|
+
participantId: args.participant.identity,
|
|
390
|
+
identity,
|
|
391
|
+
provisioned: { participantId: args.participant.identity },
|
|
392
|
+
participant: args.participant,
|
|
393
|
+
now: deps.now,
|
|
394
|
+
localState: args.localState,
|
|
395
|
+
companion: companion
|
|
396
|
+
? {
|
|
397
|
+
participantId: companion.participant.identity,
|
|
398
|
+
kind: companion.participant.kind,
|
|
399
|
+
installation: await deriveDeviceUserCompanion(rootSecret, args.trellisUrl, companion.participant.identity),
|
|
400
|
+
}
|
|
401
|
+
: undefined,
|
|
402
|
+
});
|
|
403
|
+
}
|
|
404
|
+
/**
|
|
405
|
+
* @internal Exported for focused tests; applications should use
|
|
406
|
+
* `TrellisDevice.connect`.
|
|
407
|
+
*/
|
|
408
|
+
export async function connectDeviceWithDeps(args, deps) {
|
|
409
|
+
const log = resolveDeviceLogger(args.log);
|
|
410
|
+
const rootSecret = normalizeRootSecret(args.rootSecret);
|
|
411
|
+
const identity = await deriveDeviceIdentity(rootSecret);
|
|
412
|
+
const offsetState = { serverClockOffsetMs: 0 };
|
|
413
|
+
let bootstrap;
|
|
414
|
+
while (true) {
|
|
415
|
+
try {
|
|
416
|
+
bootstrap = await fetchDeviceBootstrap({
|
|
417
|
+
trellisUrl: args.trellisUrl,
|
|
418
|
+
deviceIdentity: identity,
|
|
419
|
+
rootSecret,
|
|
420
|
+
participant: args.participant,
|
|
421
|
+
now: deps.now,
|
|
422
|
+
offsetState,
|
|
423
|
+
});
|
|
424
|
+
break;
|
|
425
|
+
}
|
|
426
|
+
catch (error) {
|
|
427
|
+
if (!(error instanceof TrellisHttpError) || error.status !== 503 ||
|
|
428
|
+
error.code !== "resource_pending") {
|
|
429
|
+
throw error;
|
|
430
|
+
}
|
|
431
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
const connectInfo = bootstrap.connectInfo;
|
|
435
|
+
const transport = await deps.loadTransport();
|
|
436
|
+
const authorizationContexts = new AuthorizationContextCache(args.trellisUrl, (input, init) => globalThis.fetch(input, init), deps.now);
|
|
437
|
+
authorizationContexts.setServerClockOffsetMs(offsetState.serverClockOffsetMs);
|
|
438
|
+
await authorizationContexts.install(connectInfo.authorizationContext, {
|
|
439
|
+
bootstrapJwt: connectInfo.transport.jwt,
|
|
440
|
+
bootstrapJwtExpiresAt: connectInfo.transport.jwtExpiresAt,
|
|
441
|
+
});
|
|
442
|
+
const verifiedContext = authorizationContexts.current();
|
|
443
|
+
if (verifiedContext.context.participantId !== args.participant.identity) {
|
|
444
|
+
throw new Error("device authorization context belongs to another participant");
|
|
445
|
+
}
|
|
446
|
+
const deploymentId = verifiedContext.context.deploymentId;
|
|
447
|
+
const instanceId = verifiedContext.context.instanceId;
|
|
448
|
+
if (!deploymentId || !instanceId) {
|
|
449
|
+
throw new Error("device authorization context is missing its deployment assignment");
|
|
450
|
+
}
|
|
451
|
+
const sessionOptions = await bootstrap.sessionAuth.natsConnectOptions({
|
|
452
|
+
sessionId: connectInfo.connectionId,
|
|
453
|
+
contextDigest: () => authorizationContexts.transportCurrent().contextDigest,
|
|
454
|
+
jwt: () => authorizationContexts.transportRoutingJwt(),
|
|
455
|
+
authorizationUsable: () => authorizationProviderCache?.transportUsable() ?? true,
|
|
456
|
+
});
|
|
457
|
+
let nc;
|
|
458
|
+
let authorizationProviderCache;
|
|
459
|
+
try {
|
|
460
|
+
nc = await transport.connect({
|
|
461
|
+
servers: selectRuntimeTransportServers(connectInfo.transports),
|
|
462
|
+
maxReconnectAttempts: DEFAULT_RUNTIME_MAX_RECONNECT_ATTEMPTS,
|
|
463
|
+
ignoreAuthErrorAbort: true,
|
|
464
|
+
timeout: 10_000,
|
|
465
|
+
inboxPrefix: connectInfo.transport.inboxPrefix,
|
|
466
|
+
authenticator: sessionOptions.authenticator,
|
|
467
|
+
});
|
|
468
|
+
const connectedNats = nc;
|
|
469
|
+
authorizationProviderCache = await AuthorizationProviderCache.attach(connectedNats, authorizationContexts.bundle().authorizationRegistry, connectInfo.transport.inboxPrefix, authorizationContexts);
|
|
470
|
+
authorizationProviderCache.start();
|
|
471
|
+
await authorizationProviderCache.waitReady();
|
|
472
|
+
await authorizationProviderCache.retainOwnContext();
|
|
473
|
+
void connectedNats.closed().finally(() => {
|
|
474
|
+
authorizationProviderCache?.stop();
|
|
475
|
+
});
|
|
476
|
+
}
|
|
477
|
+
catch (cause) {
|
|
478
|
+
authorizationProviderCache?.stop();
|
|
479
|
+
if (nc && !nc.isClosed())
|
|
480
|
+
await nc.close();
|
|
481
|
+
throw createTransportError({
|
|
482
|
+
code: "trellis.runtime.connect_failed",
|
|
483
|
+
message: "Trellis could not open the device runtime connection.",
|
|
484
|
+
hint: "Retry the connection. If it keeps failing, check Trellis transport availability.",
|
|
485
|
+
cause,
|
|
486
|
+
context: { participantId: args.participant.identity },
|
|
487
|
+
});
|
|
488
|
+
}
|
|
489
|
+
if (!nc || !authorizationProviderCache) {
|
|
490
|
+
throw new Error("Trellis device runtime connection was not established");
|
|
491
|
+
}
|
|
492
|
+
const connection = observeNatsTrellisConnection({
|
|
493
|
+
kind: "device",
|
|
494
|
+
nc,
|
|
495
|
+
availability: participantAvailability(args.participant, connectInfo.apiBindings, connectInfo.resourceBindings),
|
|
496
|
+
onTransportEvent: (event) => authorizationProviderCache.observeTransportEvent(event),
|
|
497
|
+
log: false,
|
|
498
|
+
lifecycleLog: {
|
|
499
|
+
log,
|
|
500
|
+
context: { participantId: args.participant.identity },
|
|
501
|
+
},
|
|
502
|
+
});
|
|
503
|
+
let installedAvailability = participantAvailability(args.participant, connectInfo.apiBindings, connectInfo.resourceBindings);
|
|
504
|
+
authorizationProviderCache.onOwnInvalidated(() => installConnectionAvailability(connection, participantAvailability(args.participant, {}, {}, [])));
|
|
505
|
+
authorizationProviderCache.onOwnResumed(() => {
|
|
506
|
+
installConnectionAvailability(connection, installedAvailability);
|
|
507
|
+
});
|
|
508
|
+
connection.subscribe((status) => authorizationProviderCache.observeConnectionPhase(status.phase));
|
|
509
|
+
const runtimeApi = bindApiRoutes(getParticipantRuntime(args.participant).api, connectInfo.apiBindings);
|
|
510
|
+
const stopContextRefresh = startAuthorizationContextRefresh({
|
|
511
|
+
trellisUrl: args.trellisUrl,
|
|
512
|
+
sessionId: connectInfo.connectionId,
|
|
513
|
+
auth: bootstrap.sessionAuth,
|
|
514
|
+
cache: authorizationContexts,
|
|
515
|
+
refresh: async (shouldInstall) => {
|
|
516
|
+
try {
|
|
517
|
+
const next = await fetchDeviceBootstrap({
|
|
518
|
+
trellisUrl: args.trellisUrl,
|
|
519
|
+
deviceIdentity: identity,
|
|
520
|
+
rootSecret,
|
|
521
|
+
participant: args.participant,
|
|
522
|
+
now: deps.now,
|
|
523
|
+
offsetState,
|
|
524
|
+
sessionAuth: bootstrap.sessionAuth,
|
|
525
|
+
connectionId: connectInfo.connectionId,
|
|
526
|
+
});
|
|
527
|
+
authorizationContexts.setServerClockOffsetMs(offsetState.serverClockOffsetMs);
|
|
528
|
+
const context = await authorizationContexts.prepare(next.connectInfo.authorizationContext, {
|
|
529
|
+
bootstrapJwt: next.connectInfo.transport.jwt,
|
|
530
|
+
bootstrapJwtExpiresAt: next.connectInfo.transport.jwtExpiresAt,
|
|
531
|
+
}, undefined, shouldInstall, undefined, () => () => {
|
|
532
|
+
installedAvailability = participantAvailability(args.participant, next.connectInfo.apiBindings, next.connectInfo.resourceBindings);
|
|
533
|
+
installConnectionAvailability(connection, installedAvailability);
|
|
534
|
+
refreshApiRoutes(runtimeApi, next.connectInfo.apiBindings);
|
|
535
|
+
});
|
|
536
|
+
return context;
|
|
537
|
+
}
|
|
538
|
+
catch (error) {
|
|
539
|
+
if (error instanceof TrellisHttpError) {
|
|
540
|
+
throw new AuthorizationContextRefreshError(error.status, error.code);
|
|
541
|
+
}
|
|
542
|
+
throw error;
|
|
543
|
+
}
|
|
544
|
+
},
|
|
545
|
+
onRefresh: async (context) => {
|
|
546
|
+
if (connection.status.phase === "connected")
|
|
547
|
+
await nc.reconnect();
|
|
548
|
+
await authorizationProviderCache.waitReady({ timeoutMs: 30_000 });
|
|
549
|
+
const generation = authorizationProviderCache.connectionGeneration();
|
|
550
|
+
await authorizationProviderCache.retainOwnCandidate(context.contextDigest, generation);
|
|
551
|
+
authorizationProviderCache.promoteOwnCandidate(context.contextDigest, generation);
|
|
552
|
+
},
|
|
553
|
+
onTerminalFailure: async () => {
|
|
554
|
+
if (!nc.isClosed())
|
|
555
|
+
await nc.close();
|
|
556
|
+
},
|
|
557
|
+
});
|
|
558
|
+
void nc.closed().then(stopContextRefresh, stopContextRefresh);
|
|
559
|
+
const trellis = new Trellis(args.participant.identity, nc, {
|
|
560
|
+
sessionKey: bootstrap.sessionAuth.sessionKey,
|
|
561
|
+
sign: bootstrap.sessionAuth.sign,
|
|
562
|
+
contextDigest: () => authorizationContexts.current().contextDigest,
|
|
563
|
+
authorizationProviderCache,
|
|
564
|
+
}, {
|
|
565
|
+
log,
|
|
566
|
+
api: runtimeApi,
|
|
567
|
+
state: getParticipantRuntime(args.participant).state,
|
|
568
|
+
connection,
|
|
569
|
+
}, connectInfo.transport.inboxPrefix);
|
|
570
|
+
const health = new ServiceHealthRuntime({
|
|
571
|
+
serviceName: args.participant.identity,
|
|
572
|
+
kind: "device",
|
|
573
|
+
instanceId,
|
|
574
|
+
contractId: connectInfo.participantId,
|
|
575
|
+
contractDigest: connectInfo.participantDigest,
|
|
576
|
+
publishIntervalMs: 30_000,
|
|
577
|
+
});
|
|
578
|
+
health.setInfo({
|
|
579
|
+
info: {
|
|
580
|
+
deploymentId,
|
|
581
|
+
},
|
|
582
|
+
});
|
|
583
|
+
health.add("nats", () => ({
|
|
584
|
+
status: nc.isClosed() ? "failed" : "ok",
|
|
585
|
+
...(nc.isClosed() ? { summary: "NATS connection closed" } : {}),
|
|
586
|
+
}));
|
|
587
|
+
let heartbeatTimer;
|
|
588
|
+
let publishingHeartbeat = false;
|
|
589
|
+
const stopHeartbeat = () => {
|
|
590
|
+
if (heartbeatTimer !== undefined) {
|
|
591
|
+
clearInterval(heartbeatTimer);
|
|
592
|
+
heartbeatTimer = undefined;
|
|
593
|
+
}
|
|
594
|
+
};
|
|
595
|
+
const publishHeartbeat = async () => {
|
|
596
|
+
if (publishingHeartbeat) {
|
|
597
|
+
return;
|
|
598
|
+
}
|
|
599
|
+
publishingHeartbeat = true;
|
|
600
|
+
try {
|
|
601
|
+
await publishHealthHeartbeatSample({
|
|
602
|
+
nc,
|
|
603
|
+
identity: {
|
|
604
|
+
sessionKey: bootstrap.sessionAuth.sessionKey,
|
|
605
|
+
participantKind: "device",
|
|
606
|
+
contractId: connectInfo.participantId,
|
|
607
|
+
contractDigest: connectInfo.participantDigest,
|
|
608
|
+
deploymentId,
|
|
609
|
+
instanceId,
|
|
610
|
+
},
|
|
611
|
+
sample: await health.sample(),
|
|
612
|
+
});
|
|
613
|
+
}
|
|
614
|
+
catch (error) {
|
|
615
|
+
log.warn({ error }, "Failed to build or publish health heartbeat");
|
|
616
|
+
}
|
|
617
|
+
finally {
|
|
618
|
+
publishingHeartbeat = false;
|
|
619
|
+
}
|
|
620
|
+
};
|
|
621
|
+
await publishHeartbeat();
|
|
622
|
+
heartbeatTimer = setInterval(() => {
|
|
623
|
+
void publishHeartbeat();
|
|
624
|
+
}, health.publishIntervalMs);
|
|
625
|
+
void nc.closed().finally(stopHeartbeat);
|
|
626
|
+
let companionConnection;
|
|
627
|
+
if (bootstrap.companion) {
|
|
628
|
+
const companion = args.participant.companion;
|
|
629
|
+
if (!companion ||
|
|
630
|
+
companion.participant.identity !== bootstrap.companion.participantId) {
|
|
631
|
+
throw new Error("device bootstrap companion does not match its descriptor");
|
|
632
|
+
}
|
|
633
|
+
try {
|
|
634
|
+
const installation = bootstrap.companion.bootstrap;
|
|
635
|
+
companionConnection = await connectClientWithDeps({
|
|
636
|
+
trellisUrl: args.trellisUrl,
|
|
637
|
+
participant: companion.participant,
|
|
638
|
+
auth: {
|
|
639
|
+
mode: "session_key",
|
|
640
|
+
sessionKeySeed: bootstrap.companion.installationSeedBase64url,
|
|
641
|
+
sessionId: bootstrap.companion.loginSessionId,
|
|
642
|
+
redirectTo: new URL(args.trellisUrl).origin,
|
|
643
|
+
},
|
|
644
|
+
}, {
|
|
645
|
+
...deps,
|
|
646
|
+
runtimeSessionKeySeed: bootstrap.companion.sessionSeedBase64url,
|
|
647
|
+
initialBootstrap: {
|
|
648
|
+
status: "ready",
|
|
649
|
+
serverNow: Math.floor(installation.serverNow / 1_000),
|
|
650
|
+
serverClockOffsetMs: installation.serverNow - deps.now(),
|
|
651
|
+
connectInfo: {
|
|
652
|
+
connectionId: installation.runtime.connectionId,
|
|
653
|
+
sessionId: bootstrap.companion.loginSessionId,
|
|
654
|
+
participantId: installation.runtime.participantId,
|
|
655
|
+
participantDigest: installation.authorization.participantDigest,
|
|
656
|
+
transports: installation.transports,
|
|
657
|
+
transport: {
|
|
658
|
+
jwt: installation.routing.bootstrapJwt,
|
|
659
|
+
jwtExpiresAt: installation.routing.bootstrapJwtExpiresAt,
|
|
660
|
+
inboxPrefix: installation.runtime.inboxPrefix,
|
|
661
|
+
},
|
|
662
|
+
authorizationContext: installation.authorizationContext,
|
|
663
|
+
},
|
|
664
|
+
apiBindings: installation.apiBindings,
|
|
665
|
+
resourceBindings: installation.authorization.resourceRuntime,
|
|
666
|
+
},
|
|
667
|
+
});
|
|
668
|
+
}
|
|
669
|
+
catch (error) {
|
|
670
|
+
if (bootstrap.companion.required)
|
|
671
|
+
throw error;
|
|
672
|
+
log.warn({ error }, "Optional device companion could not connect");
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
else if (args.participant.companion?.availability === "required") {
|
|
676
|
+
throw new Error("required device companion is unavailable");
|
|
677
|
+
}
|
|
678
|
+
return Object.assign(createCallerRuntime(trellis, args.participant), {
|
|
679
|
+
health,
|
|
680
|
+
companion: companionConnection,
|
|
681
|
+
});
|
|
682
|
+
}
|
|
683
|
+
export const TrellisDevice = {
|
|
684
|
+
connect(args) {
|
|
685
|
+
return deviceConnectResult(connectDeviceWithDeps(args, defaultDeps));
|
|
686
|
+
},
|
|
687
|
+
};
|
package/env.d.ts
ADDED