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.
- package/README.md +1 -1
- package/dist/bin.js +412 -87
- package/dist/index.js +1 -1
- package/dist/{runtime-CMPZpQaG.js → runtime-Cq64iuZr.js} +4768 -2040
- package/dist/src/application/error.d.ts +3 -0
- package/dist/src/application/layer.d.ts +776 -0
- package/dist/src/application/model-selection.d.ts +19 -0
- package/dist/src/application/models.d.ts +93 -0
- package/dist/src/application/project-commands.d.ts +15 -0
- package/dist/src/application/projects.d.ts +136 -0
- package/dist/src/application/service.d.ts +72 -0
- package/dist/src/application/shell-sequence.d.ts +91 -0
- package/dist/src/application/thread-commands.d.ts +84 -0
- package/dist/src/application/thread-wait.d.ts +104 -0
- package/dist/src/application/threads.d.ts +563 -0
- package/dist/src/auth/error.d.ts +50 -0
- package/dist/src/auth/layer.d.ts +23 -0
- package/dist/src/auth/local.d.ts +27 -0
- package/dist/src/auth/pairing.d.ts +22 -0
- package/dist/src/auth/schema.d.ts +58 -0
- package/dist/src/auth/service.d.ts +14 -0
- package/dist/src/auth/transport.d.ts +19 -0
- package/dist/src/auth/type.d.ts +25 -0
- package/dist/src/config/error.d.ts +18 -0
- package/dist/src/config/layer.d.ts +20 -0
- package/dist/src/config/service.d.ts +20 -0
- package/dist/src/config/url.d.ts +6 -0
- package/dist/src/domain/error.d.ts +31 -0
- package/dist/src/domain/helpers.d.ts +38 -0
- package/dist/src/domain/model-config.d.ts +293 -0
- package/dist/src/domain/thread-lifecycle.d.ts +114 -0
- package/dist/src/environment/layer.d.ts +3 -0
- package/dist/src/environment/service.d.ts +12 -0
- package/dist/src/index.d.ts +6 -0
- package/dist/src/orchestration/layer.d.ts +2128 -0
- package/dist/src/orchestration/service.d.ts +27 -0
- package/dist/src/rpc/error.d.ts +15 -0
- package/dist/src/rpc/layer.d.ts +4678 -0
- package/dist/src/rpc/service.d.ts +15 -0
- package/dist/src/rpc/ws-group.d.ts +2238 -0
- package/dist/src/runtime.d.ts +3 -0
- package/dist/src/sql/node-sqlite-client.d.ts +10 -0
- package/dist/src/sql/service.d.ts +17 -0
- package/dist/upstream-t3code/packages/contracts/src/auth.d.ts +441 -0
- package/dist/upstream-t3code/packages/contracts/src/baseSchemas.d.ts +38 -0
- package/dist/upstream-t3code/packages/contracts/src/desktopBootstrap.d.ts +14 -0
- package/dist/upstream-t3code/packages/contracts/src/editor.d.ts +124 -0
- package/dist/upstream-t3code/packages/contracts/src/environment.d.ts +64 -0
- package/dist/upstream-t3code/packages/contracts/src/environmentHttp.d.ts +762 -0
- package/dist/upstream-t3code/packages/contracts/src/filesystem.d.ts +26 -0
- package/dist/upstream-t3code/packages/contracts/src/git.d.ts +500 -0
- package/dist/upstream-t3code/packages/contracts/src/index.d.ts +25 -0
- package/dist/upstream-t3code/packages/contracts/src/ipc.d.ts +490 -0
- package/dist/upstream-t3code/packages/contracts/src/keybindings.d.ts +133 -0
- package/dist/upstream-t3code/packages/contracts/src/model.d.ts +112 -0
- package/dist/upstream-t3code/packages/contracts/src/orchestration.d.ts +6682 -0
- package/dist/upstream-t3code/packages/contracts/src/project.d.ts +45 -0
- package/dist/upstream-t3code/packages/contracts/src/provider.d.ts +116 -0
- package/dist/upstream-t3code/packages/contracts/src/providerInstance.d.ts +99 -0
- package/dist/upstream-t3code/packages/contracts/src/providerRuntime.d.ts +4276 -0
- package/dist/upstream-t3code/packages/contracts/src/relay.d.ts +1262 -0
- package/dist/upstream-t3code/packages/contracts/src/relayClient.d.ts +48 -0
- package/dist/upstream-t3code/packages/contracts/src/remoteAccess.d.ts +45 -0
- package/dist/upstream-t3code/packages/contracts/src/review.d.ts +37 -0
- package/dist/upstream-t3code/packages/contracts/src/rpc.d.ts +8218 -0
- package/dist/upstream-t3code/packages/contracts/src/server.d.ts +2291 -0
- package/dist/upstream-t3code/packages/contracts/src/settings.d.ts +271 -0
- package/dist/upstream-t3code/packages/contracts/src/sourceControl.d.ts +177 -0
- package/dist/upstream-t3code/packages/contracts/src/terminal.d.ts +330 -0
- package/dist/upstream-t3code/packages/contracts/src/vcs.d.ts +148 -0
- package/package.json +15 -7
- package/src/application/model-selection.ts +2 -2
- package/src/application/project-commands.ts +1 -1
- package/src/application/service.ts +1 -1
- package/src/application/shell-sequence.ts +1 -1
- package/src/application/thread-commands.ts +1 -1
- package/src/application/thread-wait.ts +1 -1
- package/src/auth/error.ts +33 -1
- package/src/auth/layer.ts +11 -76
- package/src/auth/local.ts +342 -208
- package/src/auth/pairing.ts +44 -2
- package/src/auth/schema.ts +21 -28
- package/src/auth/service.ts +2 -2
- package/src/auth/transport.ts +59 -22
- package/src/auth/type.ts +0 -1
- package/src/cli/auth.ts +1 -3
- package/src/cli/model-format.ts +1 -1
- package/src/cli/project-format.ts +1 -1
- package/src/cli/thread-format.ts +1 -1
- package/src/domain/helpers.ts +1 -1
- package/src/domain/model-config.ts +1 -1
- package/src/domain/thread-lifecycle.ts +1 -1
- package/src/index.ts +3 -3
- package/src/orchestration/layer.ts +1 -1
- package/src/orchestration/service.ts +1 -1
- package/src/rpc/error.ts +1 -1
- package/src/rpc/layer.ts +2 -2
- package/src/rpc/ws-group.ts +1 -1
- package/src/runtime.ts +14 -1
- package/src/sql/node-sqlite-client.ts +141 -0
- 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
|
|
5
|
-
import * as
|
|
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
|
|
10
|
-
import {
|
|
20
|
+
import { Environment } from "../environment/service.ts";
|
|
21
|
+
import { SqlClientFactory } from "../sql/service.ts";
|
|
11
22
|
import {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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
|
-
|
|
73
|
-
|
|
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
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
(
|
|
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
|
-
|
|
92
|
-
|
|
93
|
-
|
|
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
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
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
|
|
104
|
-
|
|
105
|
-
|
|
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
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
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
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
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
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
)
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
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
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
) {
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
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
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
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
|
-
|
|
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
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
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
|
|
213
|
-
|
|
214
|
-
|
|
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
|
|
232
|
-
const
|
|
233
|
-
|
|
234
|
-
|
|
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
|
}
|
package/src/auth/pairing.ts
CHANGED
|
@@ -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 {
|
|
5
|
-
import
|
|
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* () {
|