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.
- package/README.md +1 -1
- package/dist/bin.js +414 -87
- package/dist/index.js +2 -2
- package/dist/{runtime-CMPZpQaG.js → runtime-15tR27tv.js} +4764 -2045
- package/dist/src/application/layer.d.ts +3 -3
- package/dist/src/application/models.d.ts +1 -1
- package/dist/src/application/projects.d.ts +1 -1
- package/dist/src/application/threads.d.ts +1 -1
- package/dist/src/auth/error.d.ts +20 -1
- package/dist/src/auth/layer.d.ts +11 -22
- package/dist/src/auth/local.d.ts +22 -14
- package/dist/src/auth/pairing.d.ts +20 -2
- package/dist/src/auth/schema.d.ts +25 -34
- package/dist/src/auth/service.d.ts +4 -3
- package/dist/src/auth/transport.d.ts +13 -20
- package/dist/src/auth/type.d.ts +6 -1
- package/dist/src/domain/model-config.d.ts +3 -3
- package/dist/src/index.d.ts +19 -1
- package/dist/src/orchestration/layer.d.ts +3 -3
- package/dist/src/rpc/layer.d.ts +8 -8
- package/dist/src/rpc/ws-group.d.ts +3 -3
- package/dist/src/runtime.d.ts +6 -2
- 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 +14 -12
- package/dist/upstream-t3code/packages/contracts/src/environmentHttp.d.ts +167 -7
- package/dist/upstream-t3code/packages/contracts/src/index.d.ts +1 -0
- package/dist/upstream-t3code/packages/contracts/src/ipc.d.ts +24 -0
- package/dist/upstream-t3code/packages/contracts/src/providerRuntime.d.ts +90 -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/rpc.d.ts +78 -9
- package/dist/upstream-t3code/packages/contracts/src/server.d.ts +3 -3
- package/package.json +3 -3
- package/src/auth/error.ts +33 -1
- package/src/auth/layer.ts +23 -71
- package/src/auth/local.ts +321 -209
- package/src/auth/pairing.ts +33 -2
- package/src/auth/schema.ts +21 -28
- package/src/auth/service.ts +4 -3
- package/src/auth/transport.ts +59 -22
- package/src/auth/type.ts +7 -1
- package/src/cli/auth.ts +3 -3
- package/src/index.ts +50 -1
- package/src/rpc/layer.ts +2 -2
- package/src/runtime.ts +13 -2
- 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,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
|
|
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
|
|
|
8
18
|
import { normalizeHttpBaseUrl } from "../config/url.ts";
|
|
9
|
-
import { Environment
|
|
10
|
-
import {
|
|
19
|
+
import { Environment } from "../environment/service.ts";
|
|
20
|
+
import { SqlClientFactory } from "../sql/service.ts";
|
|
11
21
|
import {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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
|
-
|
|
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
|
-
});
|
|
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
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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
|
-
|
|
73
|
-
|
|
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
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
(
|
|
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
|
-
|
|
92
|
-
|
|
93
|
-
|
|
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
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
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
|
|
104
|
-
|
|
105
|
-
|
|
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
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
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
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
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
|
|
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
|
-
}),
|
|
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
|
|
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,
|
|
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
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
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
|
-
|
|
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
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
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
|
|
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
|
-
});
|
|
344
|
+
const defaultSessionTtlMs = 30 * 24 * 60 * 60 * 1000;
|
|
345
|
+
const signingSecretName = "server-signing-key";
|
|
346
|
+
const sha256BlockSize = 64;
|
|
230
347
|
|
|
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}`;
|
|
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
|
}
|
package/src/auth/pairing.ts
CHANGED
|
@@ -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
|
|
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* () {
|