t3code-cli 0.3.0 → 0.5.0

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 (48) hide show
  1. package/README.md +1 -1
  2. package/dist/bin.js +414 -87
  3. package/dist/index.js +2 -2
  4. package/dist/{runtime-CMPZpQaG.js → runtime-15tR27tv.js} +4764 -2045
  5. package/dist/src/application/layer.d.ts +3 -3
  6. package/dist/src/application/models.d.ts +1 -1
  7. package/dist/src/application/projects.d.ts +1 -1
  8. package/dist/src/application/threads.d.ts +1 -1
  9. package/dist/src/auth/error.d.ts +20 -1
  10. package/dist/src/auth/layer.d.ts +11 -22
  11. package/dist/src/auth/local.d.ts +22 -14
  12. package/dist/src/auth/pairing.d.ts +20 -2
  13. package/dist/src/auth/schema.d.ts +25 -34
  14. package/dist/src/auth/service.d.ts +4 -3
  15. package/dist/src/auth/transport.d.ts +13 -20
  16. package/dist/src/auth/type.d.ts +6 -1
  17. package/dist/src/domain/model-config.d.ts +3 -3
  18. package/dist/src/index.d.ts +19 -1
  19. package/dist/src/orchestration/layer.d.ts +3 -3
  20. package/dist/src/rpc/layer.d.ts +8 -8
  21. package/dist/src/rpc/ws-group.d.ts +3 -3
  22. package/dist/src/runtime.d.ts +6 -2
  23. package/dist/src/sql/node-sqlite-client.d.ts +10 -0
  24. package/dist/src/sql/service.d.ts +17 -0
  25. package/dist/upstream-t3code/packages/contracts/src/auth.d.ts +14 -12
  26. package/dist/upstream-t3code/packages/contracts/src/environmentHttp.d.ts +167 -7
  27. package/dist/upstream-t3code/packages/contracts/src/index.d.ts +1 -0
  28. package/dist/upstream-t3code/packages/contracts/src/ipc.d.ts +24 -0
  29. package/dist/upstream-t3code/packages/contracts/src/providerRuntime.d.ts +90 -0
  30. package/dist/upstream-t3code/packages/contracts/src/relay.d.ts +1262 -0
  31. package/dist/upstream-t3code/packages/contracts/src/relayClient.d.ts +48 -0
  32. package/dist/upstream-t3code/packages/contracts/src/rpc.d.ts +78 -9
  33. package/dist/upstream-t3code/packages/contracts/src/server.d.ts +3 -3
  34. package/package.json +3 -3
  35. package/src/auth/error.ts +33 -1
  36. package/src/auth/layer.ts +23 -71
  37. package/src/auth/local.ts +321 -209
  38. package/src/auth/pairing.ts +33 -2
  39. package/src/auth/schema.ts +21 -28
  40. package/src/auth/service.ts +4 -3
  41. package/src/auth/transport.ts +59 -22
  42. package/src/auth/type.ts +7 -1
  43. package/src/cli/auth.ts +3 -3
  44. package/src/index.ts +50 -1
  45. package/src/rpc/layer.ts +2 -2
  46. package/src/runtime.ts +13 -2
  47. package/src/sql/node-sqlite-client.ts +141 -0
  48. package/src/sql/service.ts +21 -0
package/src/auth/local.ts CHANGED
@@ -1,241 +1,353 @@
1
+ import {
2
+ AuthAdministrativeScopes,
3
+ AuthSessionId,
4
+ type AuthEnvironmentScope,
5
+ } from "#t3tools/contracts";
6
+ import * as Context from "effect/Context";
7
+ import * as Crypto from "effect/Crypto";
8
+ import * as DateTime from "effect/DateTime";
1
9
  import * as Effect from "effect/Effect";
10
+ import * as Encoding from "effect/Encoding";
11
+ import * as Filter from "effect/Filter";
2
12
  import * as FileSystem from "effect/FileSystem";
13
+ import * as Layer from "effect/Layer";
3
14
  import * as Path from "effect/Path";
4
- import * as Stream from "effect/Stream";
5
- import * as ChildProcess from "effect/unstable/process/ChildProcess";
6
- import { ChildProcessSpawner } from "effect/unstable/process/ChildProcessSpawner";
15
+ import * as Predicate from "effect/Predicate";
16
+ import * as SqlClient from "effect/unstable/sql/SqlClient";
7
17
 
8
18
  import { normalizeHttpBaseUrl } from "../config/url.ts";
9
- import { Environment, type EnvironmentShape } from "../environment/service.ts";
10
- import { AuthLocalError } from "./error.ts";
19
+ import { Environment } from "../environment/service.ts";
20
+ import { SqlClientFactory } from "../sql/service.ts";
11
21
  import {
12
- decodeAuthLocalRuntimeStateFromJson,
13
- decodeAuthLocalSessionIssueResultFromJson,
14
- type AuthLocalSessionIssueResult,
15
- } from "./schema.ts";
16
- import type { LocalAuthInput } from "./type.ts";
17
-
18
- export const issueLocalSession = Effect.fn("issueLocalSession")(function* (
19
- input: Pick<LocalAuthInput, "baseDir" | "t3Command" | "role" | "label" | "subject">,
20
- ) {
21
- const environment = yield* Environment;
22
- const baseDir = yield* resolveLocalBaseDir(input.baseDir, environment);
23
- if (input.label.length === 0) {
24
- return yield* Effect.fail(new AuthLocalError({ message: "local auth label cannot be empty" }));
22
+ AuthLocalDatabaseError,
23
+ AuthLocalError,
24
+ AuthLocalSecretError,
25
+ AuthLocalSigningError,
26
+ } from "./error.ts";
27
+ import { decodeAuthLocalRuntimeStateFromJson } from "./schema.ts";
28
+ import type { LocalAuthInput, LocalAuthResult } from "./type.ts";
29
+
30
+ export class T3LocalAuth extends Context.Service<
31
+ T3LocalAuth,
32
+ {
33
+ readonly local: (input: LocalAuthInput) => Effect.Effect<LocalAuthResult, AuthLocalError>;
25
34
  }
26
- if (input.subject.length === 0) {
27
- return yield* Effect.fail(
28
- new AuthLocalError({ message: "local auth subject cannot be empty" }),
29
- );
35
+ >()("t3cli/T3LocalAuth") {}
36
+
37
+ export const makeT3LocalAuth = Effect.fn("makeT3LocalAuth")(function* () {
38
+ const fs = yield* FileSystem.FileSystem;
39
+ const path = yield* Path.Path;
40
+ const environment = yield* Environment;
41
+ const crypto = yield* Crypto.Crypto;
42
+ const sqlClientFactory = yield* SqlClientFactory;
43
+
44
+ function resolveLocalBaseDir(input: string | undefined) {
45
+ const envBaseDir = environment.env["T3CODE_HOME"];
46
+ const raw = input ?? envBaseDir;
47
+ if (raw === undefined || raw.length === 0) {
48
+ return path.join(environment.homeDir, ".t3");
49
+ }
50
+ if (raw === "~") {
51
+ return environment.homeDir;
52
+ }
53
+ if (raw.startsWith("~/") || raw.startsWith("~\\")) {
54
+ return path.join(environment.homeDir, raw.slice(2));
55
+ }
56
+ return path.resolve(environment.cwd, raw);
30
57
  }
31
58
 
32
- const args = [
33
- "auth",
34
- "session",
35
- "issue",
36
- "--base-dir",
37
- baseDir,
38
- "--json",
39
- "--role",
40
- input.role,
41
- "--label",
42
- input.label,
43
- "--subject",
44
- input.subject,
45
- ];
46
- const command = resolveLocalT3Command(input.t3Command, environment);
47
- const output = yield* runLocalT3Command(command, args);
48
- const parsed = yield* parseSessionIssueOutput(output.stdout);
49
- return { baseDir, session: parsed } as const;
50
- });
59
+ function resolveLocalOrigin(input: { readonly baseDir: string; readonly origin?: string }) {
60
+ return Effect.gen(function* () {
61
+ if (input.origin !== undefined) {
62
+ return yield* normalizeLocalOrigin(input.origin);
63
+ }
51
64
 
52
- export const resolveLocalOrigin = Effect.fn("resolveLocalOrigin")(function* (input: {
53
- readonly baseDir: string;
54
- readonly origin?: string;
55
- }) {
56
- if (input.origin !== undefined) {
57
- return yield* normalizeLocalOrigin(input.origin);
65
+ const runtimeStatePath = path.join(input.baseDir, "userdata", "server-runtime.json");
66
+ const raw = yield* fs.readFileString(runtimeStatePath).pipe(
67
+ Effect.mapError(
68
+ (error) =>
69
+ new AuthLocalError({
70
+ message: `local runtime state not found: ${runtimeStatePath}. Make sure T3 Code is running with Network access enabled, or pass --origin manually.`,
71
+ cause: error,
72
+ }),
73
+ ),
74
+ );
75
+ const state = yield* decodeAuthLocalRuntimeStateFromJson(raw).pipe(
76
+ Effect.mapError(
77
+ (error) =>
78
+ new AuthLocalError({ message: "local runtime state has invalid shape", cause: error }),
79
+ ),
80
+ );
81
+ return yield* normalizeLocalOrigin(state.origin);
82
+ });
58
83
  }
59
84
 
60
- const path = yield* Path.Path;
61
- const runtimeStatePath = path.join(input.baseDir, "userdata", "server-runtime.json");
62
- const fs = yield* FileSystem.FileSystem;
63
- const raw = yield* fs.readFileString(runtimeStatePath).pipe(
64
- Effect.mapError(
65
- (error) =>
66
- new AuthLocalError({
67
- message: `local runtime state not found: ${runtimeStatePath}. Make sure T3 Code is running with Network access enabled, or pass --origin manually.`,
68
- cause: error,
85
+ function readSigningSecret(secretPath: string) {
86
+ return fs.readFile(secretPath).pipe(
87
+ Effect.map((bytes) => Uint8Array.from(bytes)),
88
+ Effect.catchFilter(Filter.reason("PlatformError", "NotFound"), () =>
89
+ Effect.succeed(undefined),
90
+ ),
91
+ Effect.mapError(
92
+ (error) =>
93
+ new AuthLocalSecretError({
94
+ message: `failed to read signing secret: ${secretPath}`,
95
+ cause: error,
96
+ }),
97
+ ),
98
+ );
99
+ }
100
+
101
+ const readRequiredSigningSecret = Effect.fn("readRequiredSigningSecret")(function* (
102
+ secretsDir: string,
103
+ ) {
104
+ const secretPath = path.join(secretsDir, `${signingSecretName}.bin`);
105
+ const secret = yield* readSigningSecret(secretPath);
106
+ if (secret === undefined) {
107
+ return yield* Effect.fail(
108
+ new AuthLocalSecretError({
109
+ message: `local signing secret not found: ${secretPath}`,
69
110
  }),
70
- ),
71
- );
72
- const state = yield* decodeAuthLocalRuntimeStateFromJson(raw).pipe(
73
- Effect.mapError(
74
- (error) =>
75
- new AuthLocalError({ message: "local runtime state has invalid shape", cause: error }),
76
- ),
77
- );
78
- return yield* normalizeLocalOrigin(state.origin);
79
- });
111
+ );
112
+ }
113
+ return secret;
114
+ });
80
115
 
81
- function parseSessionIssueOutput(
82
- stdout: string,
83
- ): Effect.Effect<AuthLocalSessionIssueResult, AuthLocalError> {
84
- return decodeAuthLocalSessionIssueResultFromJson(stdout).pipe(
85
- Effect.mapError(
86
- (error) => new AuthLocalError({ message: "local auth returned invalid shape", cause: error }),
87
- ),
88
- );
89
- }
116
+ const hmacSha256 = (secret: Uint8Array, payload: Uint8Array) =>
117
+ Effect.gen(function* () {
118
+ const key =
119
+ secret.byteLength > sha256BlockSize ? yield* crypto.digest("SHA-256", secret) : secret;
120
+ const block = new Uint8Array(sha256BlockSize);
121
+ block.set(key);
122
+ const outerPad = block.map((byte) => byte ^ 0x5c);
123
+ const innerPad = block.map((byte) => byte ^ 0x36);
124
+ const innerHash = yield* crypto.digest("SHA-256", concatBytes(innerPad, payload));
125
+ return yield* crypto.digest("SHA-256", concatBytes(outerPad, innerHash));
126
+ });
90
127
 
91
- function normalizeLocalOrigin(origin: string) {
92
- return normalizeHttpBaseUrl(origin).pipe(
93
- Effect.mapError((error) => new AuthLocalError({ message: error.message, cause: error })),
94
- );
95
- }
128
+ const signPayload = (payload: string, secret: Uint8Array) =>
129
+ hmacSha256(secret, new TextEncoder().encode(payload)).pipe(
130
+ Effect.map(Encoding.encodeBase64Url),
131
+ Effect.mapError(
132
+ (error) =>
133
+ new AuthLocalSigningError({
134
+ operation: "sign",
135
+ message: "failed to sign local auth payload",
136
+ cause: error,
137
+ }),
138
+ ),
139
+ );
96
140
 
97
- type LocalT3Command = {
98
- readonly command: string;
99
- readonly argsPrefix: ReadonlyArray<string>;
100
- readonly env?: Readonly<Record<string, string>>;
101
- };
141
+ function openAuthDatabase(dbPath: string) {
142
+ return sqlClientFactory.sqliteClient({ filename: dbPath }).pipe(
143
+ Effect.catchTag("SqlError", (error) =>
144
+ Effect.fail(
145
+ new AuthLocalDatabaseError({
146
+ operation: "connect",
147
+ message: error.message,
148
+ }),
149
+ ),
150
+ ),
151
+ );
152
+ }
102
153
 
103
- const defaultLocalT3Command: LocalT3Command = {
104
- command: "t3",
105
- argsPrefix: [],
106
- };
154
+ const provideAuthDatabase =
155
+ (dbPath: string) =>
156
+ <A, E, R>(effect: Effect.Effect<A, E, R>) =>
157
+ Effect.gen(function* () {
158
+ const sql = yield* openAuthDatabase(dbPath);
159
+ return yield* effect.pipe(Effect.provideService(SqlClient.SqlClient, sql));
160
+ }).pipe(Effect.scoped);
107
161
 
108
- const macOsAppNames = ["T3 Code (Dev)", "T3 Code (Alpha)", "T3 Code (Nightly)", "T3 Code"] as const;
109
-
110
- function resolveLocalT3Command(
111
- input: string | undefined,
112
- environment: EnvironmentShape,
113
- ): LocalT3Command {
114
- const envCommand = environment.env["T3CLI_T3_COMMAND"];
115
- const exactCommand =
116
- input ?? (envCommand !== undefined && envCommand.length > 0 ? envCommand : undefined);
117
- if (exactCommand !== undefined && exactCommand.length > 0) {
118
- return { command: exactCommand, argsPrefix: [] };
119
- }
120
- return defaultLocalT3Command;
121
- }
162
+ const issueLocalDatabaseSession = Effect.fn("issueLocalDatabaseSession")(function* (
163
+ input: LocalDatabaseSessionInput,
164
+ ) {
165
+ const secret = yield* readRequiredSigningSecret(input.secretsDir);
166
+ const issuedAt = yield* DateTime.now;
167
+ const expiresAt = DateTime.add(issuedAt, { milliseconds: defaultSessionTtlMs });
168
+ const sessionId = yield* crypto.randomUUIDv4.pipe(
169
+ Effect.map((id) => AuthSessionId.make(id)),
170
+ Effect.mapError(
171
+ (error) =>
172
+ new AuthLocalSecretError({
173
+ message: "failed to generate auth session id",
174
+ cause: error,
175
+ }),
176
+ ),
177
+ );
178
+ const scopes = [...AuthAdministrativeScopes];
179
+ const claims: LocalSessionClaims = {
180
+ v: 1,
181
+ kind: "session",
182
+ sid: sessionId,
183
+ sub: input.subject,
184
+ scopes,
185
+ method: "bearer-access-token",
186
+ iat: DateTime.toEpochMillis(issuedAt),
187
+ exp: DateTime.toEpochMillis(expiresAt),
188
+ };
189
+ const encodedPayload = Encoding.encodeBase64Url(JSON.stringify(claims));
190
+ const token = `${encodedPayload}.${yield* signPayload(encodedPayload, secret)}`;
191
+ yield* insertAuthSession({
192
+ sessionId,
193
+ subject: input.subject,
194
+ scopes,
195
+ label: input.label,
196
+ issuedAt: DateTime.formatIso(issuedAt),
197
+ expiresAt: DateTime.formatIso(expiresAt),
198
+ }).pipe(
199
+ provideAuthDatabase(input.dbPath),
200
+ Effect.catchTag("SqlError", (error) =>
201
+ Effect.fail(
202
+ new AuthLocalDatabaseError({
203
+ operation: Predicate.isTagged(error.reason, "ConnectionError") ? "connect" : "query",
204
+ message: error.message,
205
+ }),
206
+ ),
207
+ ),
208
+ );
209
+ return {
210
+ token,
211
+ role: input.role,
212
+ expiresAt: DateTime.formatIso(expiresAt),
213
+ };
214
+ });
122
215
 
123
- const resolveMacOsAppT3Command = Effect.fn("resolveMacOsAppT3Command")(function* () {
124
- const fs = yield* FileSystem.FileSystem;
125
- for (const appName of macOsAppNames) {
126
- const appPath = `/Applications/${appName}.app`;
127
- const command = `${appPath}/Contents/MacOS/${appName}`;
128
- if (yield* fs.exists(command)) {
129
- return {
130
- command,
131
- argsPrefix: [`${appPath}/Contents/Resources/app.asar/apps/server/dist/bin.mjs`],
132
- env: { ELECTRON_RUN_AS_NODE: "1" },
133
- } satisfies LocalT3Command;
216
+ const local = Effect.fn("T3LocalAuthLive.local")(function* (input: LocalAuthInput) {
217
+ if (input.label.length === 0) {
218
+ return yield* Effect.fail(
219
+ new AuthLocalError({ message: "local auth label cannot be empty" }),
220
+ );
221
+ }
222
+ if (input.subject.length === 0) {
223
+ return yield* Effect.fail(
224
+ new AuthLocalError({ message: "local auth subject cannot be empty" }),
225
+ );
134
226
  }
135
- }
136
- return undefined;
137
- });
138
227
 
139
- const runLocalT3Command = Effect.fn("runLocalT3Command")(function* (
140
- command: LocalT3Command,
141
- args: ReadonlyArray<string>,
142
- ) {
143
- return yield* runLocalT3CommandOnce(command, args).pipe(
144
- Effect.catchTag("PlatformError", (error) =>
145
- command === defaultLocalT3Command && process.platform === "darwin"
146
- ? Effect.gen(function* () {
147
- const fallback = yield* resolveMacOsAppT3Command();
148
- if (fallback === undefined) {
149
- return yield* Effect.fail(error);
150
- }
151
- return yield* runLocalT3CommandOnce(fallback, args);
152
- })
153
- : Effect.fail(error),
154
- ),
155
- Effect.catchTag("PlatformError", (error) =>
156
- Effect.fail(
157
- new AuthLocalError({
158
- message: `local auth failed: ${error.message}`,
159
- cause: error,
160
- }),
228
+ const baseDir = resolveLocalBaseDir(input.baseDir);
229
+ const session = yield* issueLocalDatabaseSession({
230
+ dbPath: path.join(baseDir, "userdata", "state.sqlite"),
231
+ secretsDir: path.join(baseDir, "userdata", "secrets"),
232
+ role: input.role,
233
+ label: input.label,
234
+ subject: input.subject,
235
+ }).pipe(
236
+ Effect.mapError(
237
+ (error) =>
238
+ new AuthLocalError({ message: `local auth failed: ${error.message}`, cause: error }),
161
239
  ),
162
- ),
163
- );
240
+ );
241
+ const url = yield* resolveLocalOrigin({
242
+ baseDir,
243
+ ...(input.origin !== undefined ? { origin: input.origin } : {}),
244
+ });
245
+ return {
246
+ url,
247
+ token: session.token,
248
+ role: session.role,
249
+ expiresAt: session.expiresAt,
250
+ source: "local" as const,
251
+ baseDir,
252
+ };
253
+ });
254
+
255
+ return { local };
164
256
  });
165
257
 
166
- const runLocalT3CommandOnce = Effect.fn("runLocalT3CommandOnce")(function* (
167
- command: LocalT3Command,
168
- args: ReadonlyArray<string>,
169
- ) {
170
- const spawner = yield* ChildProcessSpawner;
171
- const processCommand = ChildProcess.make(command.command, [...command.argsPrefix, ...args]).pipe(
172
- command.env === undefined ? (self) => self : ChildProcess.setEnv(command.env),
173
- );
174
- const child = yield* spawner.spawn(processCommand);
175
- const [stdout, stderr, exitCode] = yield* Effect.all(
176
- [
177
- collectProcessOutput(child.stdout),
178
- collectProcessOutput(child.stderr),
179
- child.exitCode.pipe(Effect.map(Number)),
180
- ],
181
- { concurrency: "unbounded" },
182
- ).pipe(
183
- Effect.mapError(
184
- (error) =>
185
- new AuthLocalError({
186
- message: `local auth failed: ${error.message}`,
187
- cause: error,
258
+ export const T3LocalAuthLive = Layer.effect(T3LocalAuth, makeT3LocalAuth());
259
+
260
+ function insertAuthSession(input: InsertAuthSessionInput) {
261
+ return Effect.gen(function* () {
262
+ const sql = yield* SqlClient.SqlClient;
263
+ yield* sql`PRAGMA busy_timeout = 5000;`;
264
+ yield* sql`PRAGMA foreign_keys = ON;`;
265
+ const columns = yield* sql<{ readonly name: string }>`PRAGMA table_info(auth_sessions)`;
266
+ if (!columns.some((column) => column.name === "scopes")) {
267
+ return yield* Effect.fail(
268
+ new AuthLocalDatabaseError({
269
+ operation: "schema",
270
+ message: "local auth database is missing scoped auth_sessions schema",
188
271
  }),
189
- ),
272
+ );
273
+ }
274
+ yield* sql`
275
+ INSERT INTO auth_sessions (
276
+ session_id,
277
+ subject,
278
+ scopes,
279
+ method,
280
+ client_label,
281
+ client_ip_address,
282
+ client_user_agent,
283
+ client_device_type,
284
+ client_os,
285
+ client_browser,
286
+ issued_at,
287
+ expires_at,
288
+ revoked_at
289
+ )
290
+ VALUES (
291
+ ${input.sessionId},
292
+ ${input.subject},
293
+ ${JSON.stringify(input.scopes)},
294
+ ${"bearer-access-token"},
295
+ ${input.label},
296
+ NULL,
297
+ NULL,
298
+ ${"bot"},
299
+ NULL,
300
+ NULL,
301
+ ${input.issuedAt},
302
+ ${input.expiresAt},
303
+ NULL
304
+ )
305
+ `;
306
+ return undefined;
307
+ });
308
+ }
309
+
310
+ function normalizeLocalOrigin(origin: string) {
311
+ return normalizeHttpBaseUrl(origin).pipe(
312
+ Effect.mapError((error) => new AuthLocalError({ message: error.message, cause: error })),
190
313
  );
314
+ }
191
315
 
192
- if (exitCode !== 0) {
193
- return yield* Effect.fail(
194
- new AuthLocalError({
195
- message: `local auth failed: ${formatCommandFailure(stderr, stdout, exitCode)}`,
196
- }),
197
- );
198
- }
316
+ type LocalSessionClaims = {
317
+ readonly v: 1;
318
+ readonly kind: "session";
319
+ readonly sid: AuthSessionId;
320
+ readonly sub: string;
321
+ readonly scopes: ReadonlyArray<AuthEnvironmentScope>;
322
+ readonly method: "bearer-access-token";
323
+ readonly iat: number;
324
+ readonly exp: number;
325
+ };
199
326
 
200
- return { stdout, stderr };
201
- });
327
+ type LocalDatabaseSessionInput = {
328
+ readonly dbPath: string;
329
+ readonly secretsDir: string;
330
+ readonly role: LocalAuthInput["role"];
331
+ readonly label: string;
332
+ readonly subject: string;
333
+ };
202
334
 
203
- const collectProcessOutput = <E>(stream: Stream.Stream<Uint8Array, E>): Effect.Effect<string, E> =>
204
- stream.pipe(
205
- Stream.decodeText(),
206
- Stream.runFold(
207
- () => "",
208
- (acc, chunk) => acc + chunk,
209
- ),
210
- );
335
+ type InsertAuthSessionInput = {
336
+ readonly sessionId: AuthSessionId;
337
+ readonly subject: string;
338
+ readonly scopes: ReadonlyArray<AuthEnvironmentScope>;
339
+ readonly label: string;
340
+ readonly issuedAt: string;
341
+ readonly expiresAt: string;
342
+ };
211
343
 
212
- const resolveLocalBaseDir = Effect.fn("resolveLocalBaseDir")(function* (
213
- input: string | undefined,
214
- environment: EnvironmentShape,
215
- ) {
216
- const path = yield* Path.Path;
217
- const envBaseDir = environment.env["T3CODE_HOME"];
218
- const raw = input ?? envBaseDir;
219
- if (raw === undefined || raw.length === 0) {
220
- return path.join(environment.homeDir, ".t3");
221
- }
222
- if (raw === "~") {
223
- return environment.homeDir;
224
- }
225
- if (raw.startsWith("~/") || raw.startsWith("~\\")) {
226
- return path.join(environment.homeDir, raw.slice(2));
227
- }
228
- return path.resolve(environment.cwd, raw);
229
- });
344
+ const defaultSessionTtlMs = 30 * 24 * 60 * 60 * 1000;
345
+ const signingSecretName = "server-signing-key";
346
+ const sha256BlockSize = 64;
230
347
 
231
- function formatCommandFailure(stderr: string, stdout: string, exitCode: number) {
232
- const stderrDetail = stderr.trim();
233
- if (stderrDetail.length > 0) {
234
- return stderrDetail;
235
- }
236
- const stdoutDetail = stdout.trim();
237
- if (stdoutDetail.length > 0) {
238
- return stdoutDetail;
239
- }
240
- return `t3 exited with code ${exitCode}`;
348
+ function concatBytes(first: Uint8Array, second: Uint8Array) {
349
+ const bytes = new Uint8Array(first.byteLength + second.byteLength);
350
+ bytes.set(first);
351
+ bytes.set(second, first.byteLength);
352
+ return bytes;
241
353
  }
@@ -1,8 +1,39 @@
1
+ import * as Context from "effect/Context";
1
2
  import * as Effect from "effect/Effect";
3
+ import * as Layer from "effect/Layer";
2
4
  import { Url } from "effect/unstable/http";
3
5
 
4
- import { AuthPairingUrlError } from "./error.ts";
5
- import type { PairingUrl } from "./type.ts";
6
+ import { AuthPairingUrlError, AuthTransportError } from "./error.ts";
7
+ import { T3AuthTransport } from "./transport.ts";
8
+ import type { PairingUrl, PairResult } from "./type.ts";
9
+
10
+ export class T3AuthPairing extends Context.Service<
11
+ T3AuthPairing,
12
+ {
13
+ readonly pair: (
14
+ pairingUrl: string,
15
+ ) => Effect.Effect<PairResult, AuthPairingUrlError | AuthTransportError>;
16
+ }
17
+ >()("t3cli/T3AuthPairing") {}
18
+
19
+ export const makeT3AuthPairing = Effect.fn("makeT3AuthPairing")(function* () {
20
+ const transport = yield* T3AuthTransport;
21
+
22
+ const pair = Effect.fn("T3AuthPairingLive.pair")(function* (pairingUrl: string) {
23
+ const parsed = yield* parsePairingUrl(pairingUrl);
24
+ const result = yield* transport.bootstrapBearer(parsed);
25
+ return {
26
+ url: parsed.baseUrl,
27
+ token: result.sessionToken,
28
+ role: result.role,
29
+ expiresAt: result.expiresAt,
30
+ };
31
+ });
32
+
33
+ return { pair };
34
+ });
35
+
36
+ export const T3AuthPairingLive = Layer.effect(T3AuthPairing, makeT3AuthPairing());
6
37
 
7
38
  export function parsePairingUrl(value: string): Effect.Effect<PairingUrl, AuthPairingUrlError> {
8
39
  return Effect.gen(function* () {