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