@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.
Files changed (264) hide show
  1. package/README.md +59 -0
  2. package/auth/authorization/client_context.d.ts +61 -0
  3. package/auth/authorization/client_context.js +266 -0
  4. package/auth/authorization/nats_registry.d.ts +41 -0
  5. package/auth/authorization/nats_registry.js +283 -0
  6. package/auth/authorization/provider_cache.d.ts +90 -0
  7. package/auth/authorization/provider_cache.js +783 -0
  8. package/auth/authorization/refresh.d.ts +46 -0
  9. package/auth/authorization/refresh.js +246 -0
  10. package/auth/authorization/types.d.ts +207 -0
  11. package/auth/authorization/types.js +60 -0
  12. package/auth/authorization_context.d.ts +11 -0
  13. package/auth/authorization_context.js +10 -0
  14. package/auth/browser/flow_types.d.ts +84 -0
  15. package/auth/browser/flow_types.js +70 -0
  16. package/auth/browser/portal.d.ts +21 -0
  17. package/auth/browser/portal.js +307 -0
  18. package/auth/browser/storage.d.ts +26 -0
  19. package/auth/browser/storage.js +224 -0
  20. package/auth/browser.d.ts +11 -0
  21. package/auth/browser.js +10 -0
  22. package/auth/browser_recovery.d.ts +13 -0
  23. package/auth/browser_recovery.js +38 -0
  24. package/auth/device_activation.d.ts +134 -0
  25. package/auth/device_activation.js +376 -0
  26. package/auth/http_error.d.ts +12 -0
  27. package/auth/http_error.js +32 -0
  28. package/auth/keys.d.ts +5 -0
  29. package/auth/keys.js +63 -0
  30. package/auth/mod.d.ts +16 -0
  31. package/auth/mod.js +16 -0
  32. package/auth/proof.d.ts +31 -0
  33. package/auth/proof.js +95 -0
  34. package/auth/protocol_wasm/trellis_protocol_wasm.d.ts +138 -0
  35. package/auth/protocol_wasm/trellis_protocol_wasm.js +605 -0
  36. package/auth/protocol_wasm/trellis_protocol_wasm_bg.wasm +0 -0
  37. package/auth/protocol_wasm/trellis_protocol_wasm_bytes.d.ts +1 -0
  38. package/auth/protocol_wasm/trellis_protocol_wasm_bytes.js +2 -0
  39. package/auth/protocol_wasm.d.ts +179 -0
  40. package/auth/protocol_wasm.js +133 -0
  41. package/auth/session_auth.d.ts +30 -0
  42. package/auth/session_auth.js +77 -0
  43. package/auth/session_proof.d.ts +53 -0
  44. package/auth/session_proof.js +43 -0
  45. package/auth/time.d.ts +6 -0
  46. package/auth/time.js +7 -0
  47. package/auth/trellis_id.d.ts +1 -0
  48. package/auth/trellis_id.js +5 -0
  49. package/auth/utils.d.ts +10 -0
  50. package/auth/utils.js +69 -0
  51. package/auth.d.ts +1 -0
  52. package/auth.js +1 -0
  53. package/caller.d.ts +111 -0
  54. package/caller.js +85 -0
  55. package/client.d.ts +14 -0
  56. package/client.js +1 -0
  57. package/client_connect.d.ts +143 -0
  58. package/client_connect.js +1097 -0
  59. package/codec.d.ts +13 -0
  60. package/codec.js +192 -0
  61. package/connection.d.ts +107 -0
  62. package/connection.js +396 -0
  63. package/device/activation.d.ts +42 -0
  64. package/device/activation.js +319 -0
  65. package/device.d.ts +80 -0
  66. package/device.js +687 -0
  67. package/env.d.ts +2 -0
  68. package/env.js +10 -0
  69. package/errors/AuthError.d.ts +30 -0
  70. package/errors/AuthError.js +34 -0
  71. package/errors/KVError.d.ts +30 -0
  72. package/errors/KVError.js +36 -0
  73. package/errors/OperationLifecycleError.d.ts +110 -0
  74. package/errors/OperationLifecycleError.js +142 -0
  75. package/errors/RemoteError.d.ts +61 -0
  76. package/errors/RemoteError.js +72 -0
  77. package/errors/SchemaValidationError.d.ts +48 -0
  78. package/errors/SchemaValidationError.js +49 -0
  79. package/errors/StoreError.d.ts +21 -0
  80. package/errors/StoreError.js +31 -0
  81. package/errors/TransferError.d.ts +21 -0
  82. package/errors/TransferError.js +31 -0
  83. package/errors/TransportError.d.ts +29 -0
  84. package/errors/TransportError.js +33 -0
  85. package/errors/TrellisError.d.ts +15 -0
  86. package/errors/TrellisError.js +15 -0
  87. package/errors/ValidationError.d.ts +50 -0
  88. package/errors/ValidationError.js +59 -0
  89. package/errors/index.d.ts +254 -0
  90. package/errors/index.js +238 -0
  91. package/generated.d.ts +4 -0
  92. package/generated.js +3 -0
  93. package/generated_support.d.ts +120 -0
  94. package/generated_support.js +359 -0
  95. package/globals.d.ts +9 -0
  96. package/globals.js +16 -0
  97. package/health_transport.d.ts +18 -0
  98. package/health_transport.js +32 -0
  99. package/helpers.d.ts +11 -0
  100. package/helpers.js +53 -0
  101. package/index.d.ts +2 -0
  102. package/index.js +2 -0
  103. package/internal_sdk/generated/apis/auth/mod.d.ts +805 -0
  104. package/internal_sdk/generated/apis/auth/mod.js +958 -0
  105. package/internal_sdk/generated/apis/core/mod.d.ts +79 -0
  106. package/internal_sdk/generated/apis/core/mod.js +93 -0
  107. package/internal_sdk/generated/apis/events/mod.d.ts +224 -0
  108. package/internal_sdk/generated/apis/events/mod.js +313 -0
  109. package/internal_sdk/generated/apis/health/mod.d.ts +106 -0
  110. package/internal_sdk/generated/apis/health/mod.js +132 -0
  111. package/internal_sdk/generated/apis/index.d.ts +7 -0
  112. package/internal_sdk/generated/apis/index.js +8 -0
  113. package/internal_sdk/generated/apis/jobs/mod.d.ts +176 -0
  114. package/internal_sdk/generated/apis/jobs/mod.js +209 -0
  115. package/internal_sdk/generated/apis/state/mod.d.ts +104 -0
  116. package/internal_sdk/generated/apis/state/mod.js +139 -0
  117. package/internal_sdk/generated/index.d.ts +4 -0
  118. package/internal_sdk/generated/index.js +5 -0
  119. package/internal_sdk/generated/package.json +18 -0
  120. package/internal_sdk/generated/participants/cli/mod.d.ts +402 -0
  121. package/internal_sdk/generated/participants/cli/mod.js +460 -0
  122. package/internal_sdk/generated/participants/cli/types.d.ts +2 -0
  123. package/internal_sdk/generated/participants/cli/types.js +3 -0
  124. package/internal_sdk/generated/participants/console/mod.d.ts +489 -0
  125. package/internal_sdk/generated/participants/console/mod.js +579 -0
  126. package/internal_sdk/generated/participants/console/types.d.ts +2 -0
  127. package/internal_sdk/generated/participants/console/types.js +3 -0
  128. package/internal_sdk/generated/participants/eventsRuntime/mod.d.ts +64 -0
  129. package/internal_sdk/generated/participants/eventsRuntime/mod.js +42 -0
  130. package/internal_sdk/generated/participants/eventsRuntime/types.d.ts +2 -0
  131. package/internal_sdk/generated/participants/eventsRuntime/types.js +3 -0
  132. package/internal_sdk/generated/participants/healthRuntime/mod.d.ts +58 -0
  133. package/internal_sdk/generated/participants/healthRuntime/mod.js +36 -0
  134. package/internal_sdk/generated/participants/healthRuntime/types.d.ts +2 -0
  135. package/internal_sdk/generated/participants/healthRuntime/types.js +3 -0
  136. package/internal_sdk/generated/participants/index.d.ts +8 -0
  137. package/internal_sdk/generated/participants/index.js +9 -0
  138. package/internal_sdk/generated/participants/jobsRuntime/mod.d.ts +64 -0
  139. package/internal_sdk/generated/participants/jobsRuntime/mod.js +42 -0
  140. package/internal_sdk/generated/participants/jobsRuntime/types.d.ts +2 -0
  141. package/internal_sdk/generated/participants/jobsRuntime/types.js +3 -0
  142. package/internal_sdk/generated/participants/platform/mod.d.ts +164 -0
  143. package/internal_sdk/generated/participants/platform/mod.js +149 -0
  144. package/internal_sdk/generated/participants/platform/types.d.ts +7 -0
  145. package/internal_sdk/generated/participants/platform/types.js +5 -0
  146. package/internal_sdk/generated/participants/portal/mod.d.ts +131 -0
  147. package/internal_sdk/generated/participants/portal/mod.js +109 -0
  148. package/internal_sdk/generated/participants/portal/types.d.ts +2 -0
  149. package/internal_sdk/generated/participants/portal/types.js +3 -0
  150. package/internal_sdk/generated/types/_internal/p0.d.ts +13511 -0
  151. package/internal_sdk/generated/types/_internal/p0.js +6105 -0
  152. package/internal_sdk/generated/types/index.d.ts +2951 -0
  153. package/internal_sdk/generated/types/index.js +1477 -0
  154. package/jobs.d.ts +309 -0
  155. package/jobs.js +389 -0
  156. package/kv.d.ts +85 -0
  157. package/kv.js +330 -0
  158. package/operations.d.ts +189 -0
  159. package/operations.js +847 -0
  160. package/package.json +73 -0
  161. package/pagination.d.ts +25 -0
  162. package/pagination.js +117 -0
  163. package/participant.d.ts +6 -0
  164. package/participant.js +6 -0
  165. package/participant_runtime/api.d.ts +131 -0
  166. package/participant_runtime/api.js +42 -0
  167. package/participant_runtime/json.d.ts +16 -0
  168. package/participant_runtime/json.js +193 -0
  169. package/participant_runtime/metadata.d.ts +34 -0
  170. package/participant_runtime/metadata.js +4 -0
  171. package/participant_runtime/participant.d.ts +200 -0
  172. package/participant_runtime/participant.js +330 -0
  173. package/participant_runtime/schemas.d.ts +347 -0
  174. package/participant_runtime/schemas.js +211 -0
  175. package/participant_runtime/surface_names.d.ts +16 -0
  176. package/participant_runtime/surface_names.js +34 -0
  177. package/portable.d.ts +30 -0
  178. package/portable.js +14 -0
  179. package/provider.d.ts +69 -0
  180. package/provider.js +68 -0
  181. package/runtime_transport.d.ts +25 -0
  182. package/runtime_transport.js +48 -0
  183. package/service/mod.d.ts +14 -0
  184. package/service/mod.js +13 -0
  185. package/service/outbox_inbox.d.ts +259 -0
  186. package/service/outbox_inbox.js +908 -0
  187. package/service/runtime/bootstrap.d.ts +56 -0
  188. package/service/runtime/bootstrap.js +226 -0
  189. package/service/runtime/core.d.ts +46 -0
  190. package/service/runtime/core.js +1747 -0
  191. package/service/runtime/health.d.ts +97 -0
  192. package/service/runtime/health.js +153 -0
  193. package/service/runtime/health_schemas.d.ts +23 -0
  194. package/service/runtime/health_schemas.js +20 -0
  195. package/service/runtime/internal_jobs/active-job.d.ts +40 -0
  196. package/service/runtime/internal_jobs/active-job.js +91 -0
  197. package/service/runtime/internal_jobs/bindings.d.ts +40 -0
  198. package/service/runtime/internal_jobs/bindings.js +1 -0
  199. package/service/runtime/internal_jobs/cancellation-registry.d.ts +13 -0
  200. package/service/runtime/internal_jobs/cancellation-registry.js +55 -0
  201. package/service/runtime/internal_jobs/heartbeat.d.ts +33 -0
  202. package/service/runtime/internal_jobs/heartbeat.js +65 -0
  203. package/service/runtime/internal_jobs/job-manager.d.ts +126 -0
  204. package/service/runtime/internal_jobs/job-manager.js +821 -0
  205. package/service/runtime/internal_jobs/key-coordinator.d.ts +280 -0
  206. package/service/runtime/internal_jobs/key-coordinator.js +747 -0
  207. package/service/runtime/internal_jobs/projection.d.ts +3 -0
  208. package/service/runtime/internal_jobs/projection.js +42 -0
  209. package/service/runtime/internal_jobs/runtime-worker.d.ts +126 -0
  210. package/service/runtime/internal_jobs/runtime-worker.js +454 -0
  211. package/service/runtime/internal_jobs/types.d.ts +246 -0
  212. package/service/runtime/internal_jobs/types.js +163 -0
  213. package/service/runtime/logger.d.ts +2 -0
  214. package/service/runtime/logger.js +7 -0
  215. package/service/runtime/runtime.d.ts +16 -0
  216. package/service/runtime/runtime.js +1 -0
  217. package/service/runtime/service.d.ts +372 -0
  218. package/service/runtime/service.js +1896 -0
  219. package/service/runtime/transfer/download.d.ts +27 -0
  220. package/service/runtime/transfer/download.js +1 -0
  221. package/service/runtime/transfer/protocol.d.ts +15 -0
  222. package/service/runtime/transfer/protocol.js +46 -0
  223. package/service/runtime/transfer/queue.d.ts +18 -0
  224. package/service/runtime/transfer/queue.js +136 -0
  225. package/service/runtime/transfer/upload.d.ts +40 -0
  226. package/service/runtime/transfer/upload.js +27 -0
  227. package/service/runtime/transfer.d.ts +62 -0
  228. package/service/runtime/transfer.js +763 -0
  229. package/session.d.ts +862 -0
  230. package/session.js +3407 -0
  231. package/store.d.ts +71 -0
  232. package/store.js +412 -0
  233. package/store_entry.d.ts +20 -0
  234. package/store_entry.js +75 -0
  235. package/tasks.d.ts +10 -0
  236. package/tasks.js +28 -0
  237. package/telemetry/carrier.d.ts +8 -0
  238. package/telemetry/carrier.js +20 -0
  239. package/telemetry/core.d.ts +7 -0
  240. package/telemetry/core.js +14 -0
  241. package/telemetry/env.d.ts +2 -0
  242. package/telemetry/env.js +10 -0
  243. package/telemetry/init.d.ts +2 -0
  244. package/telemetry/init.js +7 -0
  245. package/telemetry/metrics.d.ts +81 -0
  246. package/telemetry/metrics.js +267 -0
  247. package/telemetry/mod.d.ts +13 -0
  248. package/telemetry/mod.js +8 -0
  249. package/telemetry/nats.d.ts +6 -0
  250. package/telemetry/nats.js +6 -0
  251. package/telemetry/result.d.ts +1 -0
  252. package/telemetry/result.js +18 -0
  253. package/telemetry/runtime.d.ts +1 -0
  254. package/telemetry/runtime.js +141 -0
  255. package/telemetry/trace.d.ts +4 -0
  256. package/telemetry/trace.js +26 -0
  257. package/telemetry/trellis.d.ts +1 -0
  258. package/telemetry/trellis.js +1 -0
  259. package/telemetry.d.ts +1 -0
  260. package/telemetry.js +1 -0
  261. package/transfer.d.ts +112 -0
  262. package/transfer.js +525 -0
  263. package/transfer_protocol.d.ts +1 -0
  264. package/transfer_protocol.js +13 -0
@@ -0,0 +1,1097 @@
1
+ import { wsconnect, } from "@nats-io/nats-core";
2
+ import { AsyncResult, BaseError, Result, UnexpectedError, } from "@oatscenter/result";
3
+ import { Type } from "typebox";
4
+ import { Value } from "typebox/value";
5
+ import { ulid } from "ulid";
6
+ import { AuthorizationContextBundleSchema, AuthorizationContextCache, AuthorizationContextRefreshError, AuthorizationProviderCache, refreshAuthorizationContextWithMetadata, startAuthorizationContextRefresh, } from "./auth/authorization_context.js";
7
+ import { base64urlDecode, base64urlEncode, BrowserSessionStore, toArrayBuffer, } from "./auth/browser.js";
8
+ import { browserInstallationScope, } from "./auth/browser/storage.js";
9
+ import { decodeTrellisHttpError } from "./auth/http_error.js";
10
+ import { importEd25519PrivateKeyFromSeedBase64url, publicKeyBase64urlFromSeed, } from "./auth/keys.js";
11
+ import { createAuth } from "./auth/session_auth.js";
12
+ import { estimateMidpointClockOffsetMs } from "./auth/time.js";
13
+ import { createCallerRuntime } from "./caller.js";
14
+ import { installConnectionAvailability, observeNatsTrellisConnection, } from "./connection.js";
15
+ import { bindApiRoutes, getParticipantRuntime, participantAvailability, refreshApiRoutes, } from "./participant_runtime/participant.js";
16
+ import { TransportError } from "./errors/index.js";
17
+ import { TypedKV } from "./kv.js";
18
+ import { DEFAULT_RUNTIME_MAX_RECONNECT_ATTEMPTS, } from "./runtime_transport.js";
19
+ import { ResourceUnavailableError, Trellis, } from "./session.js";
20
+ import { TypedStore } from "./store.js";
21
+ import { recordTrellisDuration } from "./telemetry/mod.js";
22
+ function createConnectedClient(args) {
23
+ const trellis = new Trellis(args.name, args.nc, {
24
+ sessionKey: args.sessionKey,
25
+ sign: args.sign,
26
+ contextDigest: args.contextDigest,
27
+ authorizationProviderCache: args.authorizationProviderCache,
28
+ }, {
29
+ ...args.opts,
30
+ connection: args.connection,
31
+ }, args.inboxPrefix);
32
+ return trellis;
33
+ }
34
+ function clientConnectResult(promise) {
35
+ return AsyncResult.from(promise.then((value) => Result.ok(value), (cause) => Result.err(cause instanceof TransportError ||
36
+ cause instanceof ClientAuthHandledError
37
+ ? cause
38
+ : new UnexpectedError({ cause }))));
39
+ }
40
+ /**
41
+ * Error raised when client authentication was delegated to caller-owned routing.
42
+ */
43
+ export class ClientAuthHandledError extends BaseError {
44
+ name = "ClientAuthHandledError";
45
+ constructor() {
46
+ super("Client authentication was handled by the caller");
47
+ }
48
+ toSerializable() {
49
+ return this.baseSerializable();
50
+ }
51
+ }
52
+ async function resolveClientResources(args) {
53
+ const signature = JSON.stringify({
54
+ participantDigest: args.participantDigest,
55
+ bindings: args.bindings,
56
+ });
57
+ if (args.previous?.signature === signature)
58
+ return args.previous;
59
+ const generation = (args.previous?.generation ?? 0) + 1;
60
+ const active = { value: true };
61
+ const isCurrent = () => active.value;
62
+ const kv = {};
63
+ const store = {};
64
+ for (const [name, descriptor] of Object.entries(args.participant.resources)) {
65
+ if (descriptor.kind === "kv") {
66
+ const binding = args.bindings.kv?.[name];
67
+ if (!binding) {
68
+ if (descriptor.availability === "required") {
69
+ throw new Error(`Required KV resource '${name}' is unavailable`);
70
+ }
71
+ kv[name] = undefined;
72
+ continue;
73
+ }
74
+ kv[name] = await TypedKV.open(args.nc, binding.bucket, descriptor, {
75
+ bindOnly: true,
76
+ history: binding.history,
77
+ ttl: binding.ttlMs,
78
+ maxValueBytes: binding.maxValueBytes,
79
+ migrations: args.migrations?.kv?.[name],
80
+ isCurrent,
81
+ }).orThrow();
82
+ }
83
+ else if (descriptor.kind === "store") {
84
+ const binding = args.bindings.store?.[name];
85
+ if (!binding) {
86
+ if (descriptor.availability === "required") {
87
+ throw new Error(`Required Store resource '${name}' is unavailable`);
88
+ }
89
+ store[name] = undefined;
90
+ continue;
91
+ }
92
+ store[name] = await TypedStore.open(args.nc, binding.name, {
93
+ bindOnly: true,
94
+ ttlMs: binding.ttlMs,
95
+ maxObjectBytes: binding.maxObjectBytes,
96
+ maxTotalBytes: binding.maxTotalBytes,
97
+ isCurrent,
98
+ }).orThrow();
99
+ }
100
+ }
101
+ return { generation, signature, active, kv, store };
102
+ }
103
+ function clientResourceFacades(state) {
104
+ const facade = (kind) => {
105
+ const result = {};
106
+ for (const name of Object.keys(state.current[kind])) {
107
+ Object.defineProperty(result, name, {
108
+ enumerable: true,
109
+ get: () => {
110
+ const handle = state.current[kind][name];
111
+ if (!handle)
112
+ throw new ResourceUnavailableError(kind, name);
113
+ return handle;
114
+ },
115
+ });
116
+ }
117
+ return result;
118
+ };
119
+ return { kv: facade("kv"), store: facade("store") };
120
+ }
121
+ const ClientTransportEndpointsSchema = Type.Object({
122
+ natsServers: Type.Array(Type.String({ minLength: 1 }), { minItems: 1 }),
123
+ });
124
+ const ClientTransportsSchema = Type.Object({
125
+ native: Type.Optional(ClientTransportEndpointsSchema),
126
+ websocket: Type.Optional(ClientTransportEndpointsSchema),
127
+ });
128
+ const ClientBootstrapReadySchema = Type.Object({
129
+ status: Type.Literal("ready"),
130
+ serverNow: Type.Integer(),
131
+ connectInfo: Type.Object({
132
+ connectionId: Type.Optional(Type.String({ minLength: 1 })),
133
+ sessionId: Type.String({ minLength: 1 }),
134
+ participantId: Type.String({ minLength: 1 }),
135
+ participantDigest: Type.String({ minLength: 1 }),
136
+ transports: ClientTransportsSchema,
137
+ transport: Type.Object({
138
+ inboxPrefix: Type.String({ minLength: 1 }),
139
+ jwt: Type.String({ minLength: 1 }),
140
+ jwtExpiresAt: Type.Integer({ minimum: 1 }),
141
+ }),
142
+ authorizationContext: AuthorizationContextBundleSchema,
143
+ }),
144
+ }, { additionalProperties: false });
145
+ function isBrowserRuntime() {
146
+ const browserGlobal = globalThis;
147
+ return typeof browserGlobal.window !== "undefined" &&
148
+ typeof browserGlobal.document !== "undefined";
149
+ }
150
+ function selectClientRuntimeTransportServers(transports) {
151
+ if (isBrowserRuntime()) {
152
+ if (transports.websocket?.natsServers?.length) {
153
+ return transports.websocket.natsServers;
154
+ }
155
+ throw new Error("Browser authorization runtime has no WebSocket NATS endpoints");
156
+ }
157
+ if (transports.native?.natsServers?.length) {
158
+ return transports.native.natsServers;
159
+ }
160
+ throw new Error("Authorization runtime has no native NATS endpoints");
161
+ }
162
+ const defaultDeps = {
163
+ loadTransport: async () => {
164
+ if (isBrowserRuntime()) {
165
+ return { connect: wsconnect };
166
+ }
167
+ const mod = await import("./runtime_transport.js");
168
+ return await mod.loadDefaultRuntimeTransport();
169
+ },
170
+ now: () => Date.now(),
171
+ };
172
+ function transportCauseContext(cause) {
173
+ if (cause instanceof Error) {
174
+ return { causeName: cause.name, causeMessage: cause.message };
175
+ }
176
+ return { cause: String(cause) };
177
+ }
178
+ function createTransportError(args) {
179
+ return new TransportError({
180
+ code: args.code,
181
+ message: args.message,
182
+ hint: args.hint,
183
+ cause: args.cause,
184
+ context: {
185
+ ...(args.context ?? {}),
186
+ ...(args.cause === undefined ? {} : transportCauseContext(args.cause)),
187
+ },
188
+ });
189
+ }
190
+ const BindWireSchema = Type.Object({
191
+ serverNow: Type.Integer(),
192
+ session: Type.Object({
193
+ sessionId: Type.String({ minLength: 1 }),
194
+ principalId: Type.String({ minLength: 1 }),
195
+ participantId: Type.String({ minLength: 1 }),
196
+ sessionKey: Type.String({ minLength: 1 }),
197
+ expiresAt: Type.Union([Type.Integer(), Type.Null()]),
198
+ }),
199
+ });
200
+ async function readJsonResponse(response, args) {
201
+ try {
202
+ return await response.json();
203
+ }
204
+ catch (cause) {
205
+ throw createTransportError({
206
+ ...args,
207
+ cause,
208
+ });
209
+ }
210
+ }
211
+ function normalizeTrellisUrl(trellisUrl) {
212
+ return new URL(trellisUrl).toString().replace(/\/$/, "");
213
+ }
214
+ function resolveCurrentUrl(auth) {
215
+ const currentUrl = typeof auth?.currentUrl === "function"
216
+ ? auth.currentUrl()
217
+ : auth?.currentUrl;
218
+ if (currentUrl instanceof URL)
219
+ return currentUrl;
220
+ if (typeof currentUrl === "string")
221
+ return new URL(currentUrl);
222
+ return typeof globalThis.location === "undefined"
223
+ ? null
224
+ : new URL(globalThis.location.href);
225
+ }
226
+ function resolveRedirectTo(auth, currentUrl) {
227
+ const redirectTo = typeof auth.redirectTo === "function"
228
+ ? auth.redirectTo()
229
+ : auth.redirectTo;
230
+ if (redirectTo) {
231
+ return new URL(redirectTo, currentUrl.origin).toString();
232
+ }
233
+ const queryRedirect = currentUrl.searchParams.get("redirectTo");
234
+ if (queryRedirect) {
235
+ return new URL(queryRedirect, currentUrl.origin).toString();
236
+ }
237
+ if (auth.landingPath) {
238
+ return new URL(auth.landingPath, currentUrl.origin).toString();
239
+ }
240
+ return currentUrl.toString();
241
+ }
242
+ function resolveConfiguredRedirectTo(redirectTo) {
243
+ return typeof redirectTo === "function" ? redirectTo() : redirectTo;
244
+ }
245
+ async function createSessionKeyRuntimeIdentity(sessionKeySeed, sessionId, mode = "session_key", browserCredential, runtimeSessionKeySeed) {
246
+ const seed = base64urlDecode(sessionKeySeed);
247
+ const runtimeSeed = runtimeSessionKeySeed
248
+ ? base64urlDecode(runtimeSessionKeySeed)
249
+ : seed;
250
+ const privateKey = await importEd25519PrivateKeyFromSeedBase64url(base64urlEncode(runtimeSeed));
251
+ const sessionKey = publicKeyBase64urlFromSeed(runtimeSeed);
252
+ const runtimeAuth = await createAuth({
253
+ sessionKeySeed: base64urlEncode(runtimeSeed),
254
+ });
255
+ const sign = async (data) => {
256
+ const signature = await crypto.subtle.sign("Ed25519", privateKey, toArrayBuffer(data));
257
+ return new Uint8Array(signature);
258
+ };
259
+ const identity = {
260
+ mode,
261
+ sessionKey,
262
+ sessionNkey: runtimeAuth.sessionNkey,
263
+ seed,
264
+ auth: runtimeAuth,
265
+ sessionId,
266
+ ...(browserCredential === undefined ? {} : { browserCredential }),
267
+ sign,
268
+ };
269
+ return identity;
270
+ }
271
+ async function resolveClientIdentity(auth, installation) {
272
+ if (auth?.mode === "session_key") {
273
+ return await createSessionKeyRuntimeIdentity(auth.sessionKeySeed, auth.sessionId);
274
+ }
275
+ if (!installation) {
276
+ throw new Error("browser installation storage is unavailable");
277
+ }
278
+ const stored = await installation.readLogin() ??
279
+ await installation.getOrCreateCredential();
280
+ return await createSessionKeyRuntimeIdentity(base64urlEncode(stored.seed), stored.loginSessionId, "browser", stored);
281
+ }
282
+ async function clearBrowserLogin(installation, identity) {
283
+ if (!installation || !identity.browserCredential)
284
+ return false;
285
+ return await installation.clearLogin({
286
+ generation: identity.browserCredential.generation,
287
+ sessionKey: identity.browserCredential.sessionKey,
288
+ ...(identity.sessionId === undefined
289
+ ? {}
290
+ : { loginSessionId: identity.sessionId }),
291
+ });
292
+ }
293
+ async function bindClientFlow(args) {
294
+ const startedAt = performance.now();
295
+ const requestStartedAtMs = Date.now();
296
+ const requestId = ulid();
297
+ const issuedAt = Date.now();
298
+ const unsigned = {
299
+ requestId,
300
+ issuedAt,
301
+ };
302
+ const url = `${args.trellisUrl}/auth/flow/${encodeURIComponent(args.flowId)}/bind`;
303
+ const init = {
304
+ method: "POST",
305
+ headers: {
306
+ "Content-Type": "application/json",
307
+ Origin: args.origin,
308
+ },
309
+ body: JSON.stringify({
310
+ ...unsigned,
311
+ proof: await args.identity.auth.signSessionProof({
312
+ purpose: "userAuthBind",
313
+ origin: args.trellisUrl,
314
+ flowId: args.flowId,
315
+ sessionPublicKey: args.identity.sessionKey,
316
+ unsignedRequest: unsigned,
317
+ }),
318
+ }),
319
+ };
320
+ let response = await fetch(url, init);
321
+ for (const delay of [100, 200, 400, 800]) {
322
+ if (response.status !== 503)
323
+ break;
324
+ const error = await decodeTrellisHttpError(response.clone());
325
+ if (error.code !== "authorization_pending")
326
+ break;
327
+ await new Promise((resolve) => setTimeout(resolve, delay));
328
+ response = await fetch(url, init);
329
+ }
330
+ if (!response.ok) {
331
+ const error = await decodeTrellisHttpError(response);
332
+ throw createTransportError({
333
+ code: error.code,
334
+ message: "Trellis could not finish the sign-in step.",
335
+ hint: "Start the sign-in flow again.",
336
+ cause: error,
337
+ context: { status: error.status, trellisUrl: args.trellisUrl },
338
+ });
339
+ }
340
+ const payload = await readJsonResponse(response, {
341
+ code: "bind_invalid_response",
342
+ message: "Trellis returned an invalid sign-in response.",
343
+ hint: "Start the sign-in flow again.",
344
+ context: { flowId: args.flowId },
345
+ });
346
+ let parsed;
347
+ try {
348
+ parsed = Value.Parse(BindWireSchema, payload);
349
+ }
350
+ catch (cause) {
351
+ throw createTransportError({
352
+ code: "bind_invalid_response",
353
+ message: "Trellis returned an invalid sign-in response.",
354
+ hint: "Start the sign-in flow again.",
355
+ cause,
356
+ context: { flowId: args.flowId },
357
+ });
358
+ }
359
+ if (parsed.session.sessionKey !== args.identity.sessionKey ||
360
+ parsed.session.participantId !== args.participant.identity) {
361
+ throw new Error("Trellis returned a login for another installation");
362
+ }
363
+ const serverClockOffsetMs = estimateMidpointClockOffsetMs({
364
+ requestStartedAtMs,
365
+ responseReceivedAtMs: Date.now(),
366
+ serverNowSeconds: parsed.serverNow / 1_000,
367
+ });
368
+ recordTrellisDuration("trellis.connect.duration", performance.now() - startedAt, {
369
+ phase: "bootstrap",
370
+ participantKind: "client",
371
+ outcome: "ok",
372
+ });
373
+ return {
374
+ sessionId: parsed.session.sessionId,
375
+ expiresAt: parsed.session.expiresAt,
376
+ serverNow: parsed.serverNow / 1_000,
377
+ serverClockOffsetMs,
378
+ };
379
+ }
380
+ async function recoverClientBootstrapWithRetry(args) {
381
+ if (!args.identity.sessionId) {
382
+ return {
383
+ status: "auth_required",
384
+ serverNow: args.deps.now() / 1_000,
385
+ };
386
+ }
387
+ for (let attempt = 0; attempt < 10; attempt += 1) {
388
+ const attemptStartedAt = performance.now();
389
+ const requestStartedAtMs = args.deps.now();
390
+ try {
391
+ const result = await refreshAuthorizationContextWithMetadata({
392
+ trellisUrl: args.trellisUrl,
393
+ sessionId: args.identity.sessionId,
394
+ auth: args.identity.auth,
395
+ sessionKey: args.identity.sessionKey,
396
+ cache: args.cache,
397
+ requiredTransport: "websocket",
398
+ });
399
+ const responseReceivedAtMs = args.deps.now();
400
+ const serverClockOffsetMs = estimateMidpointClockOffsetMs({
401
+ requestStartedAtMs,
402
+ responseReceivedAtMs,
403
+ serverNowSeconds: result.response.serverNow / 1_000,
404
+ });
405
+ args.offsetState.serverClockOffsetMs = serverClockOffsetMs;
406
+ const session = result.response.runtime;
407
+ const nats = result.response;
408
+ if (!nats.transports.native && !nats.transports.websocket) {
409
+ throw createTransportError({
410
+ code: "trellis.bootstrap.invalid_response",
411
+ message: "Trellis returned incomplete client recovery metadata.",
412
+ hint: "Retry the connection. If it keeps happening, check Trellis.",
413
+ context: { trellisUrl: args.trellisUrl },
414
+ });
415
+ }
416
+ recordTrellisDuration("trellis.connect.duration", performance.now() - attemptStartedAt, {
417
+ phase: "bootstrap",
418
+ participantKind: "client",
419
+ outcome: "ok",
420
+ });
421
+ return {
422
+ status: "ready",
423
+ serverNow: result.response.serverNow / 1_000,
424
+ apiBindings: result.response.apiBindings,
425
+ resourceBindings: result.response.authorization.resourceRuntime,
426
+ connectInfo: {
427
+ sessionId: session.loginSessionId,
428
+ participantId: session.participantId,
429
+ participantDigest: result.response.authorization.participantDigest,
430
+ transports: nats.transports,
431
+ transport: {
432
+ inboxPrefix: session.inboxPrefix,
433
+ jwt: nats.routing.bootstrapJwt,
434
+ jwtExpiresAt: nats.routing.bootstrapJwtExpiresAt,
435
+ },
436
+ authorizationContext: result.response.authorizationContext,
437
+ },
438
+ };
439
+ }
440
+ catch (error) {
441
+ if (error instanceof AuthorizationContextRefreshError &&
442
+ error.terminal) {
443
+ await args.onTerminalSession?.();
444
+ return {
445
+ status: "auth_required",
446
+ serverNow: args.deps.now() / 1_000,
447
+ };
448
+ }
449
+ if (error instanceof AuthorizationContextRefreshError &&
450
+ attempt < 9) {
451
+ await new Promise((resolve) => setTimeout(resolve, 100));
452
+ continue;
453
+ }
454
+ if (attempt === 0) {
455
+ continue;
456
+ }
457
+ throw error;
458
+ }
459
+ }
460
+ throw createTransportError({
461
+ code: "trellis.bootstrap.time_sync_failed",
462
+ message: "Trellis could not confirm the client time window.",
463
+ hint: "Retry the connection. If it keeps happening, check the client and Trellis clocks.",
464
+ context: { trellisUrl: args.trellisUrl },
465
+ });
466
+ }
467
+ async function createRuntimeUserAuthenticator(args) {
468
+ const options = await args.identity.auth.natsConnectOptions({
469
+ sessionId: args.sessionId,
470
+ contextDigest: args.contextDigest,
471
+ jwt: args.jwt,
472
+ authorizationUsable: args.authorizationUsable,
473
+ });
474
+ return {
475
+ authenticators: Array.isArray(options.authenticator)
476
+ ? options.authenticator
477
+ : [options.authenticator],
478
+ stop: () => { },
479
+ };
480
+ }
481
+ function cleanupBrowserCallbackUrl(currentUrl) {
482
+ if (!isBrowserRuntime())
483
+ return;
484
+ if (!currentUrl.searchParams.has("flowId") &&
485
+ !currentUrl.searchParams.has("authError")) {
486
+ return;
487
+ }
488
+ currentUrl.searchParams.delete("flowId");
489
+ currentUrl.searchParams.delete("authError");
490
+ globalThis.history.replaceState({}, "", currentUrl.pathname + currentUrl.search + currentUrl.hash);
491
+ }
492
+ function isExpiredBindError(error) {
493
+ return error instanceof TransportError &&
494
+ error.code === "flow_expired";
495
+ }
496
+ function needsReauth(bootstrap) {
497
+ return bootstrap.status === "auth_required" ||
498
+ (bootstrap.status === "not_ready" &&
499
+ (bootstrap.reason === "insufficient_permissions" ||
500
+ bootstrap.reason === "contract_not_active"));
501
+ }
502
+ function bootstrapTargetsRequestedContract(bootstrap, args) {
503
+ return bootstrap.status === "ready" &&
504
+ bootstrap.connectInfo.participantId === args.participant.identity;
505
+ }
506
+ async function buildSessionKeyLoginUrl(args) {
507
+ const startedAt = performance.now();
508
+ const requestId = ulid();
509
+ const issuedAt = Date.now();
510
+ const unsigned = {
511
+ requestId,
512
+ issuedAt,
513
+ sessionPublicKey: args.identity.sessionKey,
514
+ participantId: args.participant.identity,
515
+ redirectTarget: args.redirectTo,
516
+ };
517
+ const response = await fetch(`${args.trellisUrl}/auth/requests`, {
518
+ method: "POST",
519
+ headers: { "Content-Type": "application/json" },
520
+ body: JSON.stringify({
521
+ ...unsigned,
522
+ proof: await args.identity.auth.signSessionProof({
523
+ purpose: "userAuthRequest",
524
+ origin: args.trellisUrl,
525
+ unsignedRequest: unsigned,
526
+ }),
527
+ }),
528
+ });
529
+ if (!response.ok) {
530
+ const error = await decodeTrellisHttpError(response);
531
+ throw createTransportError({
532
+ code: error.code,
533
+ message: "Trellis could not start sign-in.",
534
+ hint: "Retry sign-in. If it keeps failing, check Trellis availability and access.",
535
+ cause: error,
536
+ context: { status: error.status, trellisUrl: args.trellisUrl },
537
+ });
538
+ }
539
+ const payload = await readJsonResponse(response, {
540
+ code: "auth_request_invalid_response",
541
+ message: "Trellis returned an invalid sign-in response.",
542
+ hint: "Retry sign-in. If it keeps happening, start the sign-in flow again.",
543
+ context: { trellisUrl: args.trellisUrl },
544
+ });
545
+ const start = Value.Parse(Type.Object({
546
+ flowId: Type.String({ minLength: 1 }),
547
+ loginUrl: Type.String({ minLength: 1 }),
548
+ }, { additionalProperties: false }), payload);
549
+ if (start) {
550
+ recordTrellisDuration("trellis.connect.duration", performance.now() - startedAt, {
551
+ phase: "bootstrap",
552
+ participantKind: "client",
553
+ outcome: "ok",
554
+ });
555
+ return {
556
+ status: "auth_required",
557
+ flowId: start.flowId,
558
+ loginUrl: start.loginUrl,
559
+ };
560
+ }
561
+ throw createTransportError({
562
+ code: "auth_request_invalid_response",
563
+ message: "Trellis returned an invalid sign-in response.",
564
+ hint: "Retry sign-in. If it keeps happening, start the sign-in flow again.",
565
+ context: { trellisUrl: args.trellisUrl },
566
+ });
567
+ }
568
+ export async function connectClientWithDeps(args, deps) {
569
+ const totalStartedAt = performance.now();
570
+ const trellisUrl = normalizeTrellisUrl(args.trellisUrl);
571
+ const trustScope = browserInstallationScope(trellisUrl, args.participant.identity);
572
+ const browserInstallation = args.auth?.mode === "session_key"
573
+ ? undefined
574
+ : new BrowserSessionStore(trustScope, args.auth?.persistence ?? "remembered");
575
+ let identity = deps.runtimeSessionKeySeed && args.auth?.mode === "session_key"
576
+ ? await createSessionKeyRuntimeIdentity(args.auth.sessionKeySeed, args.auth.sessionId, "session_key", undefined, deps.runtimeSessionKeySeed)
577
+ : await resolveClientIdentity(args.auth, browserInstallation);
578
+ const currentUrl = resolveCurrentUrl(args.auth);
579
+ const browserAuth = args.auth?.mode === "session_key" ? undefined : args.auth;
580
+ const callbackFlowId = args.auth?.mode === "session_key"
581
+ ? args.auth.flowId
582
+ : browserAuth?.flowId ?? currentUrl?.searchParams.get("flowId") ??
583
+ undefined;
584
+ const callbackAuthError = args.auth?.mode === "session_key"
585
+ ? undefined
586
+ : currentUrl?.searchParams.get("authError") ?? undefined;
587
+ const offsetState = { serverClockOffsetMs: 0 };
588
+ const authorizationContexts = new AuthorizationContextCache(trellisUrl, (input, init) => globalThis.fetch(input, init), deps.now);
589
+ if (callbackAuthError) {
590
+ if (currentUrl)
591
+ cleanupBrowserCallbackUrl(currentUrl);
592
+ throw createTransportError({
593
+ code: callbackAuthError,
594
+ message: "Trellis sign-in did not complete.",
595
+ hint: "Start sign-in again if you want to approve access.",
596
+ context: { reason: callbackAuthError, trellisUrl },
597
+ });
598
+ }
599
+ let callbackBootstrap;
600
+ if (callbackFlowId) {
601
+ try {
602
+ const bound = await bindClientFlow({
603
+ trellisUrl,
604
+ origin: currentUrl?.origin ?? new URL(trellisUrl).origin,
605
+ flowId: callbackFlowId,
606
+ identity,
607
+ participant: args.participant,
608
+ });
609
+ if (browserInstallation) {
610
+ const current = identity.browserCredential;
611
+ if (!current || !await browserInstallation.completeBind({
612
+ generation: current.generation,
613
+ sessionKey: current.sessionKey,
614
+ pendingFlowId: callbackFlowId,
615
+ }, {
616
+ loginSessionId: bound.sessionId,
617
+ expiresAt: bound.expiresAt,
618
+ })) {
619
+ globalThis.location?.reload();
620
+ throw new Error("browser installation changed in another tab");
621
+ }
622
+ identity.browserCredential = {
623
+ generation: current.generation,
624
+ seed: current.seed,
625
+ sessionKey: current.sessionKey,
626
+ loginSessionId: bound.sessionId,
627
+ expiresAt: bound.expiresAt,
628
+ };
629
+ }
630
+ identity.sessionId = bound.sessionId;
631
+ offsetState.serverClockOffsetMs = bound.serverClockOffsetMs;
632
+ callbackBootstrap = await recoverClientBootstrapWithRetry({
633
+ trellisUrl,
634
+ identity,
635
+ cache: authorizationContexts,
636
+ deps,
637
+ offsetState,
638
+ });
639
+ }
640
+ catch (error) {
641
+ if (currentUrl && isExpiredBindError(error)) {
642
+ cleanupBrowserCallbackUrl(currentUrl);
643
+ }
644
+ else {
645
+ throw error;
646
+ }
647
+ }
648
+ }
649
+ const initialBootstrapStartedAt = performance.now();
650
+ const initialBootstrap = deps.initialBootstrap ?? callbackBootstrap ??
651
+ await recoverClientBootstrapWithRetry({
652
+ trellisUrl,
653
+ identity,
654
+ cache: authorizationContexts,
655
+ deps,
656
+ offsetState,
657
+ onTerminalSession: async () => {
658
+ if (browserInstallation &&
659
+ !await clearBrowserLogin(browserInstallation, identity)) {
660
+ globalThis.location?.reload();
661
+ throw new Error("browser installation changed in another tab");
662
+ }
663
+ },
664
+ });
665
+ recordTrellisDuration("trellis.connect.duration", performance.now() - initialBootstrapStartedAt, {
666
+ phase: "bootstrap",
667
+ participantKind: "client",
668
+ outcome: "ok",
669
+ });
670
+ const authStartedAt = performance.now();
671
+ let bootstrap = initialBootstrap;
672
+ if (needsReauth(initialBootstrap) ||
673
+ !bootstrapTargetsRequestedContract(initialBootstrap, args)) {
674
+ if (initialBootstrap.status === "ready" &&
675
+ !bootstrapTargetsRequestedContract(initialBootstrap, args)) {
676
+ if (browserInstallation &&
677
+ !await clearBrowserLogin(browserInstallation, identity)) {
678
+ globalThis.location?.reload();
679
+ throw new Error("browser installation changed in another tab");
680
+ }
681
+ }
682
+ if (browserInstallation) {
683
+ identity = await resolveClientIdentity(args.auth, browserInstallation);
684
+ }
685
+ bootstrap = await resolveAuthRequired(args, identity, browserInstallation, currentUrl, authorizationContexts, deps, offsetState);
686
+ }
687
+ recordTrellisDuration("trellis.connect.duration", performance.now() - authStartedAt, {
688
+ phase: "auth_resolution",
689
+ participantKind: "client",
690
+ outcome: "ok",
691
+ });
692
+ if (bootstrap.status !== "ready") {
693
+ if (bootstrap.status === "not_ready") {
694
+ throw createTransportError({
695
+ code: "trellis.bootstrap.not_ready",
696
+ message: "Trellis is not ready to connect this client.",
697
+ hint: "Wait for the requested app access to become available, then try again.",
698
+ context: { reason: bootstrap.reason },
699
+ });
700
+ }
701
+ throw createTransportError({
702
+ code: "trellis.bootstrap.auth_required",
703
+ message: "Trellis still requires sign-in before connecting this client.",
704
+ hint: "Complete sign-in, then try again.",
705
+ });
706
+ }
707
+ const transport = await deps.loadTransport();
708
+ identity.auth.setServerClockOffsetMs((bootstrap.serverClockOffsetMs ?? offsetState.serverClockOffsetMs) +
709
+ deps.now() - Date.now());
710
+ offsetState.serverClockOffsetMs = bootstrap.serverClockOffsetMs ??
711
+ offsetState.serverClockOffsetMs;
712
+ authorizationContexts.setServerClockOffsetMs(offsetState.serverClockOffsetMs);
713
+ if (deps.initialBootstrap) {
714
+ await authorizationContexts.install(bootstrap.connectInfo.authorizationContext, {
715
+ bootstrapJwt: bootstrap.connectInfo.transport.jwt,
716
+ bootstrapJwtExpiresAt: bootstrap.connectInfo.transport.jwtExpiresAt,
717
+ }, bootstrap.serverNow, undefined, {
718
+ connectionId: bootstrap.connectInfo.connectionId,
719
+ loginSessionId: bootstrap.connectInfo.sessionId,
720
+ participantId: bootstrap.connectInfo.participantId,
721
+ inboxPrefix: bootstrap.connectInfo.transport.inboxPrefix,
722
+ transports: bootstrap.connectInfo.transports,
723
+ });
724
+ }
725
+ selectClientRuntimeTransportServers(bootstrap.connectInfo.transports);
726
+ identity.sessionId = bootstrap.connectInfo.sessionId;
727
+ if (callbackFlowId && currentUrl)
728
+ cleanupBrowserCallbackUrl(currentUrl);
729
+ const runtimeState = {
730
+ participantDigest: bootstrap.connectInfo.participantDigest,
731
+ sessionId: bootstrap.connectInfo.sessionId,
732
+ jwt: () => authorizationContexts.transportRoutingJwt(),
733
+ contextDigest: () => authorizationContexts.transportCurrent().contextDigest,
734
+ };
735
+ let endingSession = false;
736
+ const handleSessionNotFound = identity.mode === "browser"
737
+ ? async () => {
738
+ if (endingSession)
739
+ return;
740
+ if (!await clearBrowserLogin(browserInstallation, identity)) {
741
+ if (nc && !nc.isClosed())
742
+ await nc.close();
743
+ globalThis.location?.reload();
744
+ return;
745
+ }
746
+ const replacementIdentity = await resolveClientIdentity(browserAuth, browserInstallation);
747
+ const latestCurrentUrl = resolveCurrentUrl(browserAuth);
748
+ try {
749
+ await resolveAuthRequired(args, replacementIdentity, browserInstallation, latestCurrentUrl, authorizationContexts, deps, offsetState);
750
+ }
751
+ catch (error) {
752
+ if (error instanceof ClientAuthHandledError) {
753
+ return;
754
+ }
755
+ throw error;
756
+ }
757
+ }
758
+ : undefined;
759
+ const runtimeAuth = await createRuntimeUserAuthenticator({
760
+ identity,
761
+ sessionId: runtimeState.sessionId,
762
+ contextDigest: runtimeState.contextDigest,
763
+ jwt: runtimeState.jwt,
764
+ authorizationUsable: () => authorizationProviderCache?.transportUsable() ?? true,
765
+ });
766
+ let nc;
767
+ let authorizationProviderCache;
768
+ try {
769
+ const natsStartedAt = performance.now();
770
+ nc = await transport.connect({
771
+ servers: selectClientRuntimeTransportServers(bootstrap.connectInfo.transports),
772
+ maxReconnectAttempts: DEFAULT_RUNTIME_MAX_RECONNECT_ATTEMPTS,
773
+ ignoreAuthErrorAbort: true,
774
+ timeout: args.timeout ?? 10_000,
775
+ inboxPrefix: bootstrap.connectInfo.transport.inboxPrefix,
776
+ authenticator: runtimeAuth.authenticators,
777
+ });
778
+ const connectedNats = nc;
779
+ authorizationProviderCache = await AuthorizationProviderCache.attach(connectedNats, authorizationContexts.bundle().authorizationRegistry, bootstrap.connectInfo.transport.inboxPrefix, authorizationContexts);
780
+ authorizationProviderCache.start();
781
+ await authorizationProviderCache.waitReady();
782
+ await authorizationProviderCache.retainOwnContext();
783
+ void connectedNats.closed().then(() => authorizationProviderCache?.stop(), () => authorizationProviderCache?.stop());
784
+ recordTrellisDuration("trellis.connect.duration", performance.now() - natsStartedAt, {
785
+ phase: "nats_connect",
786
+ participantKind: "client",
787
+ outcome: "ok",
788
+ });
789
+ }
790
+ catch (error) {
791
+ authorizationProviderCache?.stop();
792
+ if (nc && !nc.isClosed())
793
+ await nc.close();
794
+ runtimeAuth.stop();
795
+ throw createTransportError({
796
+ code: "trellis.runtime.connect_failed",
797
+ message: "Trellis could not open the runtime connection.",
798
+ hint: "Retry the connection. If it keeps failing, check Trellis transport availability.",
799
+ cause: error,
800
+ context: { trellisUrl },
801
+ });
802
+ }
803
+ if (!nc || !authorizationProviderCache) {
804
+ throw new Error("Trellis client runtime connection was not established");
805
+ }
806
+ void nc.closed().then(() => runtimeAuth.stop(), () => runtimeAuth.stop());
807
+ const clientOpts = {
808
+ ...(typeof args.name === "string" ? { name: args.name } : {}),
809
+ ...(args.log ? { log: args.log } : {}),
810
+ ...(typeof args.timeout === "number" ? { timeout: args.timeout } : {}),
811
+ ...(typeof args.stream === "string" ? { stream: args.stream } : {}),
812
+ ...(args.noResponderRetry
813
+ ? { noResponderRetry: args.noResponderRetry }
814
+ : {}),
815
+ };
816
+ const connection = observeNatsTrellisConnection({
817
+ kind: "client",
818
+ nc,
819
+ log: false,
820
+ availability: participantAvailability(args.participant, bootstrap.apiBindings, bootstrap.resourceBindings, authorizationContexts.current().context.grants.permissions),
821
+ onTransportEvent: (event) => authorizationProviderCache?.observeTransportEvent(event),
822
+ ...(args.log
823
+ ? {
824
+ lifecycleLog: {
825
+ log: args.log,
826
+ context: { client: clientOpts.name ?? "client" },
827
+ },
828
+ }
829
+ : {}),
830
+ });
831
+ connection.subscribe((status) => authorizationProviderCache.observeConnectionPhase(status.phase));
832
+ const api = bindApiRoutes(getParticipantRuntime(args.participant).usedApi, bootstrap.apiBindings);
833
+ const resourceState = {
834
+ current: await resolveClientResources({
835
+ nc,
836
+ participant: args.participant,
837
+ participantDigest: runtimeState.participantDigest,
838
+ bindings: bootstrap.resourceBindings,
839
+ migrations: args.resourceMigrations,
840
+ }),
841
+ };
842
+ const resourceFacades = clientResourceFacades(resourceState);
843
+ let installedAvailability = participantAvailability(args.participant, bootstrap.apiBindings, bootstrap.resourceBindings, authorizationContexts.current().context.grants.permissions);
844
+ authorizationProviderCache.onOwnInvalidated(() => {
845
+ resourceState.current.active.value = false;
846
+ installConnectionAvailability(connection, participantAvailability(args.participant, {}, {}, []));
847
+ });
848
+ authorizationProviderCache.onOwnResumed(() => {
849
+ resourceState.current.active.value = true;
850
+ installConnectionAvailability(connection, installedAvailability);
851
+ });
852
+ const stopContextRefresh = startAuthorizationContextRefresh({
853
+ trellisUrl: args.trellisUrl,
854
+ sessionId: runtimeState.sessionId,
855
+ auth: identity.auth,
856
+ sessionKey: identity.sessionKey,
857
+ cache: authorizationContexts,
858
+ refresh: async (shouldInstall) => {
859
+ const result = await refreshAuthorizationContextWithMetadata({
860
+ trellisUrl: args.trellisUrl,
861
+ sessionId: runtimeState.sessionId,
862
+ auth: identity.auth,
863
+ sessionKey: identity.sessionKey,
864
+ cache: authorizationContexts,
865
+ shouldInstall,
866
+ prepareOnly: true,
867
+ prepareInstall: async (response) => {
868
+ const nextResources = await resolveClientResources({
869
+ nc,
870
+ participant: args.participant,
871
+ participantDigest: response.authorization.participantDigest,
872
+ bindings: response.authorization.resourceRuntime,
873
+ previous: resourceState.current,
874
+ migrations: args.resourceMigrations,
875
+ });
876
+ return (verified) => {
877
+ if (nextResources !== resourceState.current) {
878
+ resourceState.current.active.value = false;
879
+ resourceState.current = nextResources;
880
+ }
881
+ else {
882
+ resourceState.current.active.value = true;
883
+ }
884
+ installedAvailability = participantAvailability(args.participant, response.apiBindings, response.authorization.resourceRuntime, verified.context.grants.permissions);
885
+ installConnectionAvailability(connection, installedAvailability);
886
+ refreshApiRoutes(api, response.apiBindings);
887
+ };
888
+ },
889
+ });
890
+ return result.context;
891
+ },
892
+ onRefresh: async (context) => {
893
+ nc.setServers(selectClientRuntimeTransportServers(authorizationContexts.transportRuntimeBinding().transports));
894
+ if (connection.status.phase === "connected")
895
+ await nc.reconnect();
896
+ await authorizationProviderCache.waitReady({ timeoutMs: 30_000 });
897
+ const generation = authorizationProviderCache.connectionGeneration();
898
+ await authorizationProviderCache.retainOwnCandidate(context.contextDigest, generation);
899
+ authorizationProviderCache.promoteOwnCandidate(context.contextDigest, generation);
900
+ },
901
+ onTerminalFailure: async (error) => {
902
+ if (!nc.isClosed()) {
903
+ try {
904
+ await nc.close();
905
+ }
906
+ catch {
907
+ await nc.closed().catch(() => undefined);
908
+ }
909
+ }
910
+ // A revoked or expired authority context is not a durable login failure.
911
+ if (error instanceof AuthorizationContextRefreshError && !error.loginInvalid)
912
+ return;
913
+ await handleSessionNotFound?.();
914
+ },
915
+ });
916
+ void nc.closed().then(stopContextRefresh, stopContextRefresh);
917
+ const state = getParticipantRuntime(args.participant).state;
918
+ const client = createConnectedClient({
919
+ name: clientOpts.name ?? "client",
920
+ nc,
921
+ connection,
922
+ inboxPrefix: bootstrap.connectInfo.transport.inboxPrefix,
923
+ sessionKey: identity.sessionKey,
924
+ sign: identity.sign,
925
+ contextDigest: () => authorizationContexts.current().contextDigest,
926
+ authorizationProviderCache,
927
+ opts: {
928
+ log: clientOpts.log,
929
+ timeout: clientOpts.timeout,
930
+ stream: clientOpts.stream,
931
+ noResponderRetry: clientOpts.noResponderRetry,
932
+ api,
933
+ state,
934
+ stateMigrations: args.resourceMigrations?.state,
935
+ resourceGeneration: () => resourceState.current.generation,
936
+ resourceAvailability: (name) => connection.availability().resources[name] ?? true,
937
+ onSessionNotFound: handleSessionNotFound,
938
+ },
939
+ });
940
+ recordTrellisDuration("trellis.connect.duration", performance.now() - totalStartedAt, {
941
+ phase: "total",
942
+ participantKind: "client",
943
+ outcome: "ok",
944
+ });
945
+ const caller = createCallerRuntime(client, args.participant, resourceFacades);
946
+ return Object.assign(caller, {
947
+ logout: async () => {
948
+ endingSession = true;
949
+ try {
950
+ const logout = Reflect.get(caller, "sessionsLogout");
951
+ if (typeof logout !== "function") {
952
+ throw new Error("the participant contract must use Auth.Sessions.Logout");
953
+ }
954
+ const result = Reflect.apply(logout, caller, [{}]);
955
+ if (!(result instanceof AsyncResult)) {
956
+ throw new Error("Auth.Sessions.Logout returned an invalid result");
957
+ }
958
+ const operationCompleted = await Promise.race([
959
+ result.orThrow().then(() => true),
960
+ nc.closed().then(() => false),
961
+ ]);
962
+ if (!operationCompleted) {
963
+ try {
964
+ await refreshAuthorizationContextWithMetadata({
965
+ trellisUrl,
966
+ sessionId: identity.sessionId,
967
+ auth: identity.auth,
968
+ cache: authorizationContexts,
969
+ requiredTransport: "websocket",
970
+ });
971
+ throw new Error("logout did not revoke the current session");
972
+ }
973
+ catch (error) {
974
+ if (!(error instanceof AuthorizationContextRefreshError) ||
975
+ !error.terminal) {
976
+ throw error;
977
+ }
978
+ }
979
+ }
980
+ }
981
+ finally {
982
+ try {
983
+ await clearBrowserLogin(browserInstallation, identity);
984
+ }
985
+ finally {
986
+ try {
987
+ if (!nc.isClosed())
988
+ await nc.close();
989
+ }
990
+ finally {
991
+ endingSession = false;
992
+ }
993
+ }
994
+ }
995
+ },
996
+ });
997
+ }
998
+ async function resolveAuthRequired(args, identity, browserInstallation, currentUrl, cache, deps, offsetState) {
999
+ const browserAuth = args.auth?.mode === "session_key" ? {} : args.auth ?? {};
1000
+ const redirectTo = args.auth?.mode === "session_key"
1001
+ ? args.auth.redirectTo
1002
+ : currentUrl
1003
+ ? resolveRedirectTo(browserAuth, currentUrl)
1004
+ : resolveConfiguredRedirectTo(browserAuth.redirectTo);
1005
+ if (!redirectTo) {
1006
+ throw new Error("Client authentication requires a redirectTo URL");
1007
+ }
1008
+ const authStart = await buildSessionKeyLoginUrl({
1009
+ trellisUrl: normalizeTrellisUrl(args.trellisUrl),
1010
+ redirectTo,
1011
+ identity,
1012
+ participant: args.participant,
1013
+ });
1014
+ if (browserInstallation) {
1015
+ const current = identity.browserCredential;
1016
+ if (!current || !await browserInstallation.rememberFlow({
1017
+ generation: current.generation,
1018
+ sessionKey: current.sessionKey,
1019
+ }, authStart.flowId)) {
1020
+ globalThis.location?.reload();
1021
+ throw new Error("browser installation changed in another tab");
1022
+ }
1023
+ identity.browserCredential = {
1024
+ ...current,
1025
+ pendingFlowId: authStart.flowId,
1026
+ };
1027
+ }
1028
+ const loginUrl = authStart.loginUrl;
1029
+ const continuationStartedAt = performance.now();
1030
+ const continuation = await args.onAuthRequired?.({
1031
+ loginUrl,
1032
+ sessionKey: identity.sessionKey,
1033
+ mode: identity.mode,
1034
+ });
1035
+ recordTrellisDuration("trellis.connect.duration", performance.now() - continuationStartedAt, {
1036
+ phase: "bootstrap",
1037
+ participantKind: "client",
1038
+ outcome: "ok",
1039
+ });
1040
+ if (continuation && continuation.status === "handled") {
1041
+ throw new ClientAuthHandledError();
1042
+ }
1043
+ if (continuation && continuation.status === "bound") {
1044
+ const bindStartedAt = performance.now();
1045
+ const bound = await bindClientFlow({
1046
+ trellisUrl: normalizeTrellisUrl(args.trellisUrl),
1047
+ origin: new URL(redirectTo).origin,
1048
+ flowId: continuation.flowId,
1049
+ identity,
1050
+ participant: args.participant,
1051
+ });
1052
+ if (browserInstallation) {
1053
+ const current = identity.browserCredential;
1054
+ if (!current || !await browserInstallation.completeBind({
1055
+ generation: current.generation,
1056
+ sessionKey: current.sessionKey,
1057
+ pendingFlowId: continuation.flowId,
1058
+ }, {
1059
+ loginSessionId: bound.sessionId,
1060
+ expiresAt: bound.expiresAt,
1061
+ })) {
1062
+ throw new Error("browser installation changed in another tab");
1063
+ }
1064
+ identity.browserCredential = {
1065
+ generation: current.generation,
1066
+ seed: current.seed,
1067
+ sessionKey: current.sessionKey,
1068
+ loginSessionId: bound.sessionId,
1069
+ expiresAt: bound.expiresAt,
1070
+ };
1071
+ }
1072
+ identity.sessionId = bound.sessionId;
1073
+ recordTrellisDuration("trellis.connect.duration", performance.now() - bindStartedAt, {
1074
+ phase: "bootstrap",
1075
+ participantKind: "client",
1076
+ outcome: "ok",
1077
+ });
1078
+ return recoverClientBootstrapWithRetry({
1079
+ trellisUrl: normalizeTrellisUrl(args.trellisUrl),
1080
+ identity,
1081
+ cache,
1082
+ deps,
1083
+ offsetState,
1084
+ });
1085
+ }
1086
+ if (isBrowserRuntime()) {
1087
+ globalThis.location.href = loginUrl;
1088
+ throw new ClientAuthHandledError();
1089
+ }
1090
+ throw new Error("Client authentication required and no auth continuation was provided");
1091
+ }
1092
+ /** Connects user-facing participants to the Trellis caller runtime. */
1093
+ export class TrellisClient {
1094
+ static connect(args) {
1095
+ return clientConnectResult(connectClientWithDeps(args, defaultDeps));
1096
+ }
1097
+ }