bravenlab 0.0.0-stage → 0.1.3
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 +247 -2
- package/dist/auth.js +651 -0
- package/dist/auth.js.map +7 -0
- package/dist/bin.js +14492 -0
- package/dist/bin.js.map +7 -0
- package/dist/cli.js +8978 -0
- package/dist/cli.js.map +7 -0
- package/package.json +54 -4
package/dist/auth.js
ADDED
|
@@ -0,0 +1,651 @@
|
|
|
1
|
+
// src/auth.ts
|
|
2
|
+
import {
|
|
3
|
+
Cause,
|
|
4
|
+
Console as Console2,
|
|
5
|
+
Duration as Duration2,
|
|
6
|
+
Effect as Effect4,
|
|
7
|
+
Match as Match2,
|
|
8
|
+
Option as Option4,
|
|
9
|
+
Ref,
|
|
10
|
+
Schedule
|
|
11
|
+
} from "effect";
|
|
12
|
+
import { HttpClient as HttpClient2, HttpClientRequest as HttpClientRequest2, HttpClientResponse as HttpClientResponse2 } from "effect/http";
|
|
13
|
+
import * as Schema4 from "effect/Schema";
|
|
14
|
+
import * as Tuple from "effect/Tuple";
|
|
15
|
+
|
|
16
|
+
// ../identity-store/src/environment.ts
|
|
17
|
+
import { Context, Layer, Match, Option } from "effect";
|
|
18
|
+
import * as Str from "effect/String";
|
|
19
|
+
var Environment = class extends Context.Service()("braven/Environment") {
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
// ../identity-store/src/errors.ts
|
|
23
|
+
import { Effect } from "effect";
|
|
24
|
+
import * as Schema from "effect/Schema";
|
|
25
|
+
import * as Str2 from "effect/String";
|
|
26
|
+
var EXIT_FAILURE = 1;
|
|
27
|
+
var CliFailure = class extends Schema.TaggedError()("CliFailure", {
|
|
28
|
+
message: Schema.String,
|
|
29
|
+
exitCode: Schema.Int
|
|
30
|
+
}) {
|
|
31
|
+
};
|
|
32
|
+
var fail = (message, exitCode = EXIT_FAILURE) => Effect.fail(new CliFailure({ message, exitCode }));
|
|
33
|
+
var oneLine = (value) => Str2.trim(Str2.replace(/\s+/g, " ")(String(value))).slice(0, 300);
|
|
34
|
+
|
|
35
|
+
// ../identity-store/src/identity.ts
|
|
36
|
+
import { Context as Context2, Effect as Effect2, Layer as Layer2, Option as Option2, pipe } from "effect";
|
|
37
|
+
import * as Arr from "effect/Array";
|
|
38
|
+
import * as R from "effect/Record";
|
|
39
|
+
import * as FileSystem from "effect/FileSystem";
|
|
40
|
+
import * as Path from "effect/Path";
|
|
41
|
+
import * as Predicate from "effect/Predicate";
|
|
42
|
+
import * as Schema2 from "effect/Schema";
|
|
43
|
+
import * as Str3 from "effect/String";
|
|
44
|
+
var CONFIG_VERSION = 1;
|
|
45
|
+
var CredentialKind = Schema2.Literals(["session", "apiKey"]);
|
|
46
|
+
var Account = class extends Schema2.Class("Account")({
|
|
47
|
+
origin: Schema2.String,
|
|
48
|
+
email: Schema2.String,
|
|
49
|
+
kind: CredentialKind,
|
|
50
|
+
label: Schema2.OptionFromOptionalKey(Schema2.String)
|
|
51
|
+
}) {
|
|
52
|
+
};
|
|
53
|
+
var ConfigFile = class extends Schema2.Class("ConfigFile")({
|
|
54
|
+
version: Schema2.Int.pipe(
|
|
55
|
+
Schema2.withConstructorDefault(Effect2.succeed(CONFIG_VERSION)),
|
|
56
|
+
Schema2.withDecodingDefaultKey(Effect2.succeed(CONFIG_VERSION))
|
|
57
|
+
),
|
|
58
|
+
current: Schema2.OptionFromNullishOr(Schema2.String, {
|
|
59
|
+
onNoneEncoding: null
|
|
60
|
+
}).pipe(
|
|
61
|
+
Schema2.withConstructorDefault(Effect2.succeed(Option2.none())),
|
|
62
|
+
Schema2.withDecodingDefaultKey(Effect2.succeed(null))
|
|
63
|
+
),
|
|
64
|
+
accounts: Schema2.Array(Account).pipe(
|
|
65
|
+
Schema2.withConstructorDefault(Effect2.succeed([])),
|
|
66
|
+
Schema2.withDecodingDefaultKey(Effect2.succeed([]))
|
|
67
|
+
),
|
|
68
|
+
defaults: Schema2.Record(Schema2.String, Schema2.Unknown).pipe(
|
|
69
|
+
Schema2.withConstructorDefault(Effect2.succeed({})),
|
|
70
|
+
Schema2.withDecodingDefaultKey(Effect2.succeed({}))
|
|
71
|
+
),
|
|
72
|
+
watcher: Schema2.Record(Schema2.String, Schema2.Unknown).pipe(
|
|
73
|
+
Schema2.withConstructorDefault(Effect2.succeed({})),
|
|
74
|
+
Schema2.withDecodingDefaultKey(Effect2.succeed({}))
|
|
75
|
+
)
|
|
76
|
+
}) {
|
|
77
|
+
};
|
|
78
|
+
var ConfigJson = Schema2.fromJsonString(ConfigFile, { space: 2 });
|
|
79
|
+
var StoreConfig = class extends Context2.Service()("braven/StoreConfig") {
|
|
80
|
+
};
|
|
81
|
+
var CREDENTIALS_FILE = "credentials.json";
|
|
82
|
+
var Secrets = class extends Context2.Service()(
|
|
83
|
+
"braven/Secrets"
|
|
84
|
+
) {
|
|
85
|
+
};
|
|
86
|
+
var SecretsJson = Schema2.fromJsonString(
|
|
87
|
+
Schema2.Record(Schema2.String, Schema2.String),
|
|
88
|
+
{ space: 2 }
|
|
89
|
+
);
|
|
90
|
+
var credentialsPath = (configPath) => Str3.replace(/config\.json$/, CREDENTIALS_FILE)(configPath);
|
|
91
|
+
var secretsLayer = Layer2.effect(Secrets)(
|
|
92
|
+
Effect2.gen(function* () {
|
|
93
|
+
const { path } = yield* StoreConfig;
|
|
94
|
+
const fs = yield* FileSystem.FileSystem;
|
|
95
|
+
const pathService = yield* Path.Path;
|
|
96
|
+
const file = credentialsPath(path);
|
|
97
|
+
const read = () => Effect2.gen(function* () {
|
|
98
|
+
if (!(yield* fs.exists(file).pipe(Effect2.mapError(storageFailure("read", file))))) {
|
|
99
|
+
return {};
|
|
100
|
+
}
|
|
101
|
+
const text = yield* fs.readFileString(file).pipe(Effect2.mapError(storageFailure("read", file)));
|
|
102
|
+
return yield* Schema2.decodeUnknownEffect(SecretsJson)(text).pipe(
|
|
103
|
+
Effect2.mapError(
|
|
104
|
+
(cause) => new CliFailure({
|
|
105
|
+
message: `${file} is not a credentials file: ${oneLine(cause.message)}`,
|
|
106
|
+
exitCode: EXIT_FAILURE
|
|
107
|
+
})
|
|
108
|
+
)
|
|
109
|
+
);
|
|
110
|
+
});
|
|
111
|
+
const write = (secrets) => Effect2.gen(function* () {
|
|
112
|
+
const text = yield* Schema2.encodeUnknownEffect(SecretsJson)(
|
|
113
|
+
secrets
|
|
114
|
+
).pipe(Effect2.mapError(storageFailure("encode", file)));
|
|
115
|
+
yield* fs.makeDirectory(pathService.dirname(file), { recursive: true }).pipe(Effect2.mapError(storageFailure("create", file)));
|
|
116
|
+
const temporary = `${file}.tmp`;
|
|
117
|
+
yield* fs.writeFileString(temporary, text, { mode: 384 }).pipe(Effect2.mapError(storageFailure("write", file)));
|
|
118
|
+
yield* fs.chmod(temporary, 384).pipe(Effect2.mapError(storageFailure("write", file)));
|
|
119
|
+
yield* fs.rename(temporary, file).pipe(Effect2.mapError(storageFailure("write", file)));
|
|
120
|
+
});
|
|
121
|
+
const update = (change) => Effect2.gen(function* () {
|
|
122
|
+
yield* write(change(yield* read()));
|
|
123
|
+
});
|
|
124
|
+
return Secrets.of({
|
|
125
|
+
get: (account) => read().pipe(Effect2.map((all) => Option2.fromNullishOr(all[account]))),
|
|
126
|
+
set: (account, secret) => update((all) => R.set(all, account, secret)),
|
|
127
|
+
remove: (account) => update((all) => R.remove(all, account))
|
|
128
|
+
});
|
|
129
|
+
})
|
|
130
|
+
);
|
|
131
|
+
var accountKey = (origin, email) => `${normalizeOrigin(origin)}|${email}`;
|
|
132
|
+
var normalizeOrigin = (origin) => Str3.replace(/\/+$/, "")(origin);
|
|
133
|
+
var storageFailure = (action, path) => (cause) => new CliFailure({
|
|
134
|
+
message: `${action} ${path}: ${oneLine(cause.message)}`,
|
|
135
|
+
exitCode: EXIT_FAILURE
|
|
136
|
+
});
|
|
137
|
+
var loadConfig = () => Effect2.gen(function* () {
|
|
138
|
+
const { path } = yield* StoreConfig;
|
|
139
|
+
const fs = yield* FileSystem.FileSystem;
|
|
140
|
+
if (!(yield* fs.exists(path).pipe(Effect2.mapError(storageFailure("read", path))))) {
|
|
141
|
+
return new ConfigFile({});
|
|
142
|
+
}
|
|
143
|
+
const text = yield* fs.readFileString(path).pipe(Effect2.mapError(storageFailure("read", path)));
|
|
144
|
+
const config = yield* Schema2.decodeUnknownEffect(ConfigJson)(text).pipe(
|
|
145
|
+
Effect2.mapError(
|
|
146
|
+
(cause) => new CliFailure({
|
|
147
|
+
message: `${path} is not a version-${CONFIG_VERSION} config: ${oneLine(cause.message)}`,
|
|
148
|
+
exitCode: EXIT_FAILURE
|
|
149
|
+
})
|
|
150
|
+
)
|
|
151
|
+
);
|
|
152
|
+
if (config.version !== CONFIG_VERSION) {
|
|
153
|
+
return yield* fail(
|
|
154
|
+
`${path} is version ${config.version}, expected ${CONFIG_VERSION}`
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
return config;
|
|
158
|
+
});
|
|
159
|
+
var saveConfig = (config) => Effect2.gen(function* () {
|
|
160
|
+
const { path } = yield* StoreConfig;
|
|
161
|
+
const fs = yield* FileSystem.FileSystem;
|
|
162
|
+
const pathService = yield* Path.Path;
|
|
163
|
+
const text = yield* Schema2.encodeUnknownEffect(ConfigJson)(config).pipe(
|
|
164
|
+
Effect2.mapError(storageFailure("encode", path))
|
|
165
|
+
);
|
|
166
|
+
yield* fs.makeDirectory(pathService.dirname(path), { recursive: true }).pipe(Effect2.mapError(storageFailure("create", path)));
|
|
167
|
+
const temporary = `${path}.tmp`;
|
|
168
|
+
yield* fs.writeFileString(temporary, text).pipe(Effect2.mapError(storageFailure("write", path)));
|
|
169
|
+
yield* fs.rename(temporary, path).pipe(Effect2.mapError(storageFailure("write", path)));
|
|
170
|
+
});
|
|
171
|
+
var storeCredential = (credential, label = Option2.none()) => Effect2.gen(function* () {
|
|
172
|
+
if (credential.secret.length === 0) {
|
|
173
|
+
return yield* fail("refusing to store an empty secret");
|
|
174
|
+
}
|
|
175
|
+
if (credential.email.length === 0) {
|
|
176
|
+
return yield* fail("storing a credential needs an email");
|
|
177
|
+
}
|
|
178
|
+
const origin = normalizeOrigin(credential.origin);
|
|
179
|
+
const key = accountKey(origin, credential.email);
|
|
180
|
+
const secrets = yield* Secrets;
|
|
181
|
+
yield* secrets.set(key, credential.secret);
|
|
182
|
+
const config = yield* loadConfig();
|
|
183
|
+
const account = new Account({
|
|
184
|
+
origin,
|
|
185
|
+
email: credential.email,
|
|
186
|
+
kind: credential.kind,
|
|
187
|
+
label
|
|
188
|
+
});
|
|
189
|
+
yield* saveConfig(
|
|
190
|
+
new ConfigFile({
|
|
191
|
+
...config,
|
|
192
|
+
accounts: [
|
|
193
|
+
...config.accounts.filter(
|
|
194
|
+
(stored) => accountKey(stored.origin, stored.email) !== key
|
|
195
|
+
),
|
|
196
|
+
account
|
|
197
|
+
],
|
|
198
|
+
current: Option2.some(key)
|
|
199
|
+
})
|
|
200
|
+
);
|
|
201
|
+
return account;
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
// ../identity-store/src/login.ts
|
|
205
|
+
import open from "open";
|
|
206
|
+
import { createServer } from "node:http";
|
|
207
|
+
import { NodeHttpServer } from "@effect/platform-node";
|
|
208
|
+
import {
|
|
209
|
+
Console,
|
|
210
|
+
Context as Context3,
|
|
211
|
+
Deferred,
|
|
212
|
+
Duration,
|
|
213
|
+
Effect as Effect3,
|
|
214
|
+
Layer as Layer3,
|
|
215
|
+
Option as Option3,
|
|
216
|
+
Predicate as Predicate2,
|
|
217
|
+
Schema as Schema3
|
|
218
|
+
} from "effect";
|
|
219
|
+
import {
|
|
220
|
+
HttpClient,
|
|
221
|
+
HttpClientRequest,
|
|
222
|
+
HttpClientResponse,
|
|
223
|
+
HttpRouter,
|
|
224
|
+
HttpServer,
|
|
225
|
+
HttpServerRequest,
|
|
226
|
+
HttpServerResponse
|
|
227
|
+
} from "effect/http";
|
|
228
|
+
import "effect/FileSystem";
|
|
229
|
+
import * as NetAddress from "effect/net/NetAddress";
|
|
230
|
+
import "effect/Path";
|
|
231
|
+
import * as Str4 from "effect/String";
|
|
232
|
+
var LOGIN_TIMEOUT = Duration.minutes(5);
|
|
233
|
+
var SignInReason = Schema3.Literals([
|
|
234
|
+
"timeout",
|
|
235
|
+
"listener",
|
|
236
|
+
"browser",
|
|
237
|
+
"rejected",
|
|
238
|
+
"storage"
|
|
239
|
+
]);
|
|
240
|
+
var SignInFailure = class extends Schema3.TaggedError()(
|
|
241
|
+
"SignInFailure",
|
|
242
|
+
{ reason: SignInReason, message: Schema3.String }
|
|
243
|
+
) {
|
|
244
|
+
};
|
|
245
|
+
var Browser = class extends Context3.Service()("braven/Browser") {
|
|
246
|
+
};
|
|
247
|
+
var browserLayer = Layer3.succeed(Browser)({
|
|
248
|
+
open: (url) => Effect3.tryPromise({
|
|
249
|
+
try: () => open(url),
|
|
250
|
+
catch: (cause) => new CliFailure({
|
|
251
|
+
message: `cannot open a browser for ${url}: ${oneLine(cause)}`,
|
|
252
|
+
exitCode: EXIT_FAILURE
|
|
253
|
+
})
|
|
254
|
+
}).pipe(Effect3.asVoid)
|
|
255
|
+
});
|
|
256
|
+
var Sessions = class extends Context3.Service()("braven/Sessions") {
|
|
257
|
+
};
|
|
258
|
+
var SessionUser = class SessionUser2 extends Schema3.Class(
|
|
259
|
+
"SessionUser"
|
|
260
|
+
)({ email: Schema3.NonEmptyString }) {
|
|
261
|
+
};
|
|
262
|
+
var SessionBody = class SessionBody2 extends Schema3.Class(
|
|
263
|
+
"SessionBody"
|
|
264
|
+
)({ user: SessionUser }) {
|
|
265
|
+
};
|
|
266
|
+
var lookupFailure = (origin, cause) => new CliFailure({
|
|
267
|
+
message: `cannot read a session from ${origin}: ${oneLine(cause.message)}`,
|
|
268
|
+
exitCode: EXIT_FAILURE
|
|
269
|
+
});
|
|
270
|
+
var lookupEmail = (origin, token) => Effect3.gen(function* () {
|
|
271
|
+
const client = yield* HttpClient.HttpClient;
|
|
272
|
+
const response = yield* client.get(`${origin}/api/auth/get-session`, {
|
|
273
|
+
headers: { authorization: `Bearer ${token}` }
|
|
274
|
+
}).pipe(Effect3.mapError((cause) => lookupFailure(origin, cause)));
|
|
275
|
+
if (response.status !== 200) {
|
|
276
|
+
const detail = yield* response.text.pipe(Effect3.orElseSucceed(() => ""));
|
|
277
|
+
return yield* fail(
|
|
278
|
+
`the API rejected that sign-in (${response.status})${detail ? `: ${oneLine(detail)}` : ""} \u2014 run \`braven login\` again`
|
|
279
|
+
);
|
|
280
|
+
}
|
|
281
|
+
const body = yield* HttpClientResponse.schemaBodyJson(SessionBody)(
|
|
282
|
+
response
|
|
283
|
+
).pipe(Effect3.mapError((cause) => lookupFailure(origin, cause)));
|
|
284
|
+
return body.user.email;
|
|
285
|
+
});
|
|
286
|
+
var revokeSession = (origin, token) => Effect3.gen(function* () {
|
|
287
|
+
const client = yield* HttpClient.HttpClient;
|
|
288
|
+
const response = yield* client.execute(
|
|
289
|
+
HttpClientRequest.post(`${origin}/api/auth/sign-out`).pipe(
|
|
290
|
+
HttpClientRequest.bearerToken(token),
|
|
291
|
+
HttpClientRequest.bodyJsonUnsafe({})
|
|
292
|
+
)
|
|
293
|
+
).pipe(
|
|
294
|
+
Effect3.mapError(
|
|
295
|
+
(cause) => new CliFailure({
|
|
296
|
+
message: `cannot reach ${origin} to sign out: ${oneLine(cause.message)}`,
|
|
297
|
+
exitCode: EXIT_FAILURE
|
|
298
|
+
})
|
|
299
|
+
)
|
|
300
|
+
);
|
|
301
|
+
if (response.status === 200 || response.status === 401) return;
|
|
302
|
+
const detail = yield* response.text.pipe(Effect3.orElseSucceed(() => ""));
|
|
303
|
+
return yield* fail(
|
|
304
|
+
`the API refused the sign-out (${response.status})${detail ? `: ${oneLine(detail)}` : ""}`
|
|
305
|
+
);
|
|
306
|
+
});
|
|
307
|
+
var sessionsLayer = Layer3.effect(Sessions)(
|
|
308
|
+
Effect3.map(HttpClient.HttpClient, (client) => ({
|
|
309
|
+
emailFor: (origin, token) => lookupEmail(origin, token).pipe(
|
|
310
|
+
Effect3.provideService(HttpClient.HttpClient, client)
|
|
311
|
+
),
|
|
312
|
+
revoke: (origin, token) => revokeSession(origin, token).pipe(
|
|
313
|
+
Effect3.provideService(HttpClient.HttpClient, client)
|
|
314
|
+
)
|
|
315
|
+
}))
|
|
316
|
+
);
|
|
317
|
+
var tokenOf = (params) => Option3.fromNullishOr(params["token"]).pipe(
|
|
318
|
+
Option3.flatMap(
|
|
319
|
+
(value) => Predicate2.isString(value) ? Option3.some(value) : Option3.none()
|
|
320
|
+
),
|
|
321
|
+
Option3.filter(Str4.isNonEmpty)
|
|
322
|
+
);
|
|
323
|
+
var tokenRoute = (tokens) => HttpRouter.add(
|
|
324
|
+
"GET",
|
|
325
|
+
"/",
|
|
326
|
+
Effect3.gen(function* () {
|
|
327
|
+
const params = yield* HttpServerRequest.ParsedSearchParams;
|
|
328
|
+
return yield* Option3.match(tokenOf(params), {
|
|
329
|
+
onNone: () => Effect3.succeed(
|
|
330
|
+
HttpServerResponse.text("Missing token.", { status: 400 })
|
|
331
|
+
),
|
|
332
|
+
onSome: (token) => Effect3.gen(function* () {
|
|
333
|
+
yield* Deferred.succeed(tokens, token);
|
|
334
|
+
return HttpServerResponse.html(
|
|
335
|
+
"<h1>Authenticated. You can close this window.</h1>"
|
|
336
|
+
);
|
|
337
|
+
})
|
|
338
|
+
});
|
|
339
|
+
})
|
|
340
|
+
);
|
|
341
|
+
var listenerFailure = (message) => new SignInFailure({ reason: "listener", message });
|
|
342
|
+
var portOf = (address) => NetAddress.isInetAddress(address) ? Effect3.succeed(address.port) : Effect3.fail(
|
|
343
|
+
listenerFailure("the login listener did not bind a TCP port")
|
|
344
|
+
);
|
|
345
|
+
var openingLines = (url) => `Opening ${url}
|
|
346
|
+
If your browser did not open, visit that URL to finish signing in.`;
|
|
347
|
+
var awaitToken = (options) => Effect3.gen(function* () {
|
|
348
|
+
const tokens = yield* Deferred.make();
|
|
349
|
+
const app = HttpRouter.serve(
|
|
350
|
+
Layer3.mergeAll(
|
|
351
|
+
tokenRoute(tokens),
|
|
352
|
+
HttpRouter.add(
|
|
353
|
+
"*",
|
|
354
|
+
"*",
|
|
355
|
+
HttpServerResponse.text("Missing token.", { status: 400 })
|
|
356
|
+
)
|
|
357
|
+
),
|
|
358
|
+
{
|
|
359
|
+
disableLogger: true,
|
|
360
|
+
disableListenLog: true
|
|
361
|
+
}
|
|
362
|
+
).pipe(
|
|
363
|
+
Layer3.provideMerge(
|
|
364
|
+
NodeHttpServer.layer(createServer, { host: "127.0.0.1", port: 0 })
|
|
365
|
+
)
|
|
366
|
+
);
|
|
367
|
+
return yield* Effect3.gen(function* () {
|
|
368
|
+
const server = yield* HttpServer.HttpServer;
|
|
369
|
+
const port = yield* portOf(server.address);
|
|
370
|
+
const browser = yield* Browser;
|
|
371
|
+
const loginUrl = `${options.webOrigin}/cli-login?port=${port}`;
|
|
372
|
+
yield* Option3.match(Option3.fromNullishOr(options.onLoginUrl), {
|
|
373
|
+
onNone: () => Console.log(openingLines(loginUrl)),
|
|
374
|
+
onSome: (report) => report(loginUrl)
|
|
375
|
+
});
|
|
376
|
+
yield* browser.open(loginUrl).pipe(
|
|
377
|
+
Effect3.mapError(
|
|
378
|
+
(failure) => new SignInFailure({
|
|
379
|
+
reason: "browser",
|
|
380
|
+
message: failure.message
|
|
381
|
+
})
|
|
382
|
+
)
|
|
383
|
+
);
|
|
384
|
+
return { token: yield* Deferred.await(tokens), port };
|
|
385
|
+
}).pipe(
|
|
386
|
+
Effect3.timeoutOrElse({
|
|
387
|
+
duration: options.timeout ?? LOGIN_TIMEOUT,
|
|
388
|
+
orElse: () => Effect3.fail(
|
|
389
|
+
new SignInFailure({
|
|
390
|
+
reason: "timeout",
|
|
391
|
+
message: "timed out waiting for the browser sign-in (5 minutes) \u2014 run `braven login` again"
|
|
392
|
+
})
|
|
393
|
+
)
|
|
394
|
+
}),
|
|
395
|
+
Effect3.provide(app),
|
|
396
|
+
Effect3.catchTag(
|
|
397
|
+
"ServeError",
|
|
398
|
+
(cause) => Effect3.fail(
|
|
399
|
+
listenerFailure(
|
|
400
|
+
`cannot start the login listener: ${oneLine(cause.message)}`
|
|
401
|
+
)
|
|
402
|
+
)
|
|
403
|
+
)
|
|
404
|
+
);
|
|
405
|
+
});
|
|
406
|
+
var asSignInFailure = (reason) => (failure) => new SignInFailure({ reason, message: failure.message });
|
|
407
|
+
var rememberSession = (origin, token) => Effect3.gen(function* () {
|
|
408
|
+
const sessions = yield* Sessions;
|
|
409
|
+
const email = yield* sessions.emailFor(origin, token).pipe(Effect3.mapError(asSignInFailure("rejected")));
|
|
410
|
+
return yield* storeCredential({
|
|
411
|
+
origin,
|
|
412
|
+
email,
|
|
413
|
+
kind: "session",
|
|
414
|
+
secret: token
|
|
415
|
+
}).pipe(Effect3.mapError(asSignInFailure("storage")));
|
|
416
|
+
});
|
|
417
|
+
var signIn = (options) => Effect3.gen(function* () {
|
|
418
|
+
const { token } = yield* awaitToken(options);
|
|
419
|
+
return yield* rememberSession(options.origin, token);
|
|
420
|
+
});
|
|
421
|
+
|
|
422
|
+
// src/auth.ts
|
|
423
|
+
var DEVICE_GRANT_TYPE = "urn:ietf:params:oauth:grant-type:device_code";
|
|
424
|
+
var DEVICE_SCOPE = "openid profile email";
|
|
425
|
+
var DEVICE_CODE_LIFETIME = Duration2.minutes(30);
|
|
426
|
+
var DEVICE_POLL_INTERVAL = Duration2.seconds(5);
|
|
427
|
+
var DEVICE_SLOW_DOWN = Duration2.seconds(5);
|
|
428
|
+
var deviceUrl = (origin, endpoint) => `${origin}/api/auth/device/${endpoint}`;
|
|
429
|
+
var DeviceCode = class extends Schema4.Class("DeviceCode")({
|
|
430
|
+
device_code: Schema4.NonEmptyString,
|
|
431
|
+
user_code: Schema4.NonEmptyString,
|
|
432
|
+
verification_uri: Schema4.NonEmptyString,
|
|
433
|
+
verification_uri_complete: Schema4.OptionFromOptionalKey(Schema4.String),
|
|
434
|
+
expires_in: Schema4.Int.pipe(
|
|
435
|
+
Schema4.withConstructorDefault(
|
|
436
|
+
Effect4.succeed(Duration2.toSeconds(DEVICE_CODE_LIFETIME))
|
|
437
|
+
),
|
|
438
|
+
Schema4.withDecodingDefaultKey(
|
|
439
|
+
Effect4.succeed(Duration2.toSeconds(DEVICE_CODE_LIFETIME))
|
|
440
|
+
)
|
|
441
|
+
),
|
|
442
|
+
interval: Schema4.Int.pipe(
|
|
443
|
+
Schema4.withConstructorDefault(
|
|
444
|
+
Effect4.succeed(Duration2.toSeconds(DEVICE_POLL_INTERVAL))
|
|
445
|
+
),
|
|
446
|
+
Schema4.withDecodingDefaultKey(
|
|
447
|
+
Effect4.succeed(Duration2.toSeconds(DEVICE_POLL_INTERVAL))
|
|
448
|
+
)
|
|
449
|
+
)
|
|
450
|
+
}) {
|
|
451
|
+
};
|
|
452
|
+
var DeviceToken = class extends Schema4.Class("DeviceToken")({
|
|
453
|
+
access_token: Schema4.OptionFromOptionalKey(Schema4.String),
|
|
454
|
+
error: Schema4.OptionFromOptionalKey(Schema4.String),
|
|
455
|
+
error_description: Schema4.OptionFromOptionalKey(Schema4.String)
|
|
456
|
+
}) {
|
|
457
|
+
};
|
|
458
|
+
var retryLine = "run `braven login --device` again";
|
|
459
|
+
var deviceExpiredFailure = () => new CliFailure({
|
|
460
|
+
message: `the device code expired before it was approved \u2014 ${retryLine}`,
|
|
461
|
+
exitCode: EXIT_FAILURE
|
|
462
|
+
});
|
|
463
|
+
var transportFailure = (origin, cause) => new CliFailure({
|
|
464
|
+
message: `cannot reach the sign-in API at ${origin}: ${oneLine(cause.message)}`,
|
|
465
|
+
exitCode: EXIT_FAILURE
|
|
466
|
+
});
|
|
467
|
+
var detailOf = (description) => Option4.match(description, {
|
|
468
|
+
onNone: () => "",
|
|
469
|
+
onSome: (value) => `: ${oneLine(value)}`
|
|
470
|
+
});
|
|
471
|
+
var refusalFor = (error, description) => Match2.value(error).pipe(
|
|
472
|
+
Match2.when("expired_token", deviceExpiredFailure),
|
|
473
|
+
Match2.when(
|
|
474
|
+
"access_denied",
|
|
475
|
+
() => new CliFailure({
|
|
476
|
+
message: `the sign-in was denied in the browser \u2014 ${retryLine}`,
|
|
477
|
+
exitCode: EXIT_FAILURE
|
|
478
|
+
})
|
|
479
|
+
),
|
|
480
|
+
Match2.when(
|
|
481
|
+
"invalid_grant",
|
|
482
|
+
() => new CliFailure({
|
|
483
|
+
message: `the device code is no longer valid \u2014 ${retryLine}`,
|
|
484
|
+
exitCode: EXIT_FAILURE
|
|
485
|
+
})
|
|
486
|
+
),
|
|
487
|
+
Match2.orElse(
|
|
488
|
+
(other) => new CliFailure({
|
|
489
|
+
message: `the device sign-in failed (${other})${detailOf(description)}`,
|
|
490
|
+
exitCode: EXIT_FAILURE
|
|
491
|
+
})
|
|
492
|
+
)
|
|
493
|
+
);
|
|
494
|
+
var refusal = (cause) => ({
|
|
495
|
+
_tag: "Refused",
|
|
496
|
+
failure: cause
|
|
497
|
+
});
|
|
498
|
+
var verdictOf = (body) => Option4.match(body.access_token, {
|
|
499
|
+
onSome: (token) => Effect4.succeed({ _tag: "Granted", token }),
|
|
500
|
+
onNone: () => Match2.value(Option4.getOrElse(body.error, () => "unknown_error")).pipe(
|
|
501
|
+
Match2.when(
|
|
502
|
+
"authorization_pending",
|
|
503
|
+
() => Effect4.succeed({ _tag: "Pending" })
|
|
504
|
+
),
|
|
505
|
+
Match2.when(
|
|
506
|
+
"slow_down",
|
|
507
|
+
() => Effect4.succeed({ _tag: "SlowDown" })
|
|
508
|
+
),
|
|
509
|
+
Match2.orElse(
|
|
510
|
+
(error) => Effect4.fail(refusalFor(error, body.error_description))
|
|
511
|
+
)
|
|
512
|
+
)
|
|
513
|
+
});
|
|
514
|
+
var requestDeviceCode = (origin, clientId) => Effect4.gen(function* () {
|
|
515
|
+
const client = yield* HttpClient2.HttpClient;
|
|
516
|
+
const request = yield* HttpClientRequest2.post(
|
|
517
|
+
deviceUrl(origin, "code")
|
|
518
|
+
).pipe(
|
|
519
|
+
HttpClientRequest2.bodyJson({
|
|
520
|
+
client_id: clientId,
|
|
521
|
+
scope: DEVICE_SCOPE
|
|
522
|
+
}),
|
|
523
|
+
Effect4.mapError((cause) => transportFailure(origin, cause))
|
|
524
|
+
);
|
|
525
|
+
const response = yield* client.execute(request).pipe(Effect4.mapError((cause) => transportFailure(origin, cause)));
|
|
526
|
+
if (response.status !== 200) {
|
|
527
|
+
const detail = yield* response.text.pipe(Effect4.orElseSucceed(() => ""));
|
|
528
|
+
return yield* fail(
|
|
529
|
+
`the sign-in API refused a device code (${response.status})${detail ? `: ${oneLine(detail)}` : ""} \u2014 ${retryLine}`
|
|
530
|
+
);
|
|
531
|
+
}
|
|
532
|
+
return yield* HttpClientResponse2.schemaBodyJson(DeviceCode)(response).pipe(
|
|
533
|
+
Effect4.mapError(
|
|
534
|
+
(cause) => new CliFailure({
|
|
535
|
+
message: `cannot read a device code from ${origin}: ${oneLine(cause.message)}`,
|
|
536
|
+
exitCode: EXIT_FAILURE
|
|
537
|
+
})
|
|
538
|
+
)
|
|
539
|
+
);
|
|
540
|
+
});
|
|
541
|
+
var pollDeviceToken = (origin, clientId, deviceCode) => Effect4.gen(function* () {
|
|
542
|
+
const client = yield* HttpClient2.HttpClient;
|
|
543
|
+
const response = yield* client.execute(
|
|
544
|
+
HttpClientRequest2.post(deviceUrl(origin, "token")).pipe(
|
|
545
|
+
// better-auth's `/device/token` reads JSON only; a form body is 415.
|
|
546
|
+
HttpClientRequest2.bodyJsonUnsafe({
|
|
547
|
+
grant_type: DEVICE_GRANT_TYPE,
|
|
548
|
+
device_code: deviceCode,
|
|
549
|
+
client_id: clientId
|
|
550
|
+
})
|
|
551
|
+
)
|
|
552
|
+
).pipe(Effect4.mapError((cause) => transportFailure(origin, cause)));
|
|
553
|
+
const body = yield* HttpClientResponse2.schemaBodyJson(DeviceToken)(
|
|
554
|
+
response
|
|
555
|
+
).pipe(
|
|
556
|
+
Effect4.mapError(
|
|
557
|
+
() => new CliFailure({
|
|
558
|
+
message: `the sign-in API at ${origin} returned an unreadable device-token answer`,
|
|
559
|
+
exitCode: EXIT_FAILURE
|
|
560
|
+
})
|
|
561
|
+
)
|
|
562
|
+
);
|
|
563
|
+
return yield* verdictOf(body);
|
|
564
|
+
}).pipe(
|
|
565
|
+
// The poll's contract is a verdict, never a failure: a transport or decode
|
|
566
|
+
// error stops the schedule as `Refused` and surfaces one line later.
|
|
567
|
+
Effect4.catch((cause) => Effect4.succeed(refusal(cause)))
|
|
568
|
+
);
|
|
569
|
+
var devicePollSchedule = (start) => Schedule.fromStepWithMetadata(
|
|
570
|
+
Effect4.map(
|
|
571
|
+
Ref.make(start),
|
|
572
|
+
(interval) => (metadata) => Match2.value(metadata.input).pipe(
|
|
573
|
+
Match2.tag("Granted", () => Cause.done(metadata.input)),
|
|
574
|
+
Match2.tag("Refused", () => Cause.done(metadata.input)),
|
|
575
|
+
Match2.tag(
|
|
576
|
+
"Pending",
|
|
577
|
+
() => Ref.get(interval).pipe(
|
|
578
|
+
Effect4.map((delay) => Tuple.make(metadata.input, delay))
|
|
579
|
+
)
|
|
580
|
+
),
|
|
581
|
+
Match2.tag(
|
|
582
|
+
"SlowDown",
|
|
583
|
+
() => Ref.updateAndGet(
|
|
584
|
+
interval,
|
|
585
|
+
(delay) => Duration2.millis(
|
|
586
|
+
Duration2.toMillis(delay) + Duration2.toMillis(DEVICE_SLOW_DOWN)
|
|
587
|
+
)
|
|
588
|
+
).pipe(Effect4.map((delay) => Tuple.make(metadata.input, delay)))
|
|
589
|
+
),
|
|
590
|
+
Match2.exhaustive
|
|
591
|
+
)
|
|
592
|
+
)
|
|
593
|
+
);
|
|
594
|
+
var deviceLogin = (options) => Effect4.gen(function* () {
|
|
595
|
+
const code = yield* requestDeviceCode(options.origin, options.clientId);
|
|
596
|
+
const verification = Option4.getOrElse(
|
|
597
|
+
code.verification_uri_complete,
|
|
598
|
+
() => code.verification_uri
|
|
599
|
+
);
|
|
600
|
+
yield* Console2.log(
|
|
601
|
+
`Sign in at ${verification}
|
|
602
|
+
and confirm the code ${code.user_code} in the browser.`
|
|
603
|
+
);
|
|
604
|
+
const browser = yield* Browser;
|
|
605
|
+
yield* browser.open(verification);
|
|
606
|
+
const interval = Duration2.seconds(code.interval);
|
|
607
|
+
const poll = pollDeviceToken(
|
|
608
|
+
options.origin,
|
|
609
|
+
options.clientId,
|
|
610
|
+
code.device_code
|
|
611
|
+
);
|
|
612
|
+
const untilFirstPoll = yield* Ref.make(true);
|
|
613
|
+
const outcome = yield* Effect4.suspend(
|
|
614
|
+
() => Effect4.flatMap(
|
|
615
|
+
Ref.getAndSet(untilFirstPoll, false),
|
|
616
|
+
(isFirst) => isFirst ? Effect4.delay(poll, interval) : poll
|
|
617
|
+
)
|
|
618
|
+
).pipe(
|
|
619
|
+
Effect4.repeat(devicePollSchedule(interval)),
|
|
620
|
+
Effect4.timeoutOrElse({
|
|
621
|
+
duration: options.timeout ?? Duration2.seconds(code.expires_in),
|
|
622
|
+
orElse: () => Effect4.fail(deviceExpiredFailure())
|
|
623
|
+
})
|
|
624
|
+
);
|
|
625
|
+
return yield* Match2.value(outcome).pipe(
|
|
626
|
+
Match2.tag("Granted", (granted) => Effect4.succeed(granted.token)),
|
|
627
|
+
Match2.tag("Refused", (refused) => Effect4.fail(refused.failure)),
|
|
628
|
+
Match2.orElse(() => Effect4.fail(deviceExpiredFailure()))
|
|
629
|
+
);
|
|
630
|
+
});
|
|
631
|
+
export {
|
|
632
|
+
Browser,
|
|
633
|
+
DEVICE_GRANT_TYPE,
|
|
634
|
+
DEVICE_SCOPE,
|
|
635
|
+
DeviceCode,
|
|
636
|
+
DeviceToken,
|
|
637
|
+
LOGIN_TIMEOUT,
|
|
638
|
+
Sessions,
|
|
639
|
+
SignInFailure,
|
|
640
|
+
SignInReason,
|
|
641
|
+
awaitToken,
|
|
642
|
+
browserLayer,
|
|
643
|
+
deviceLogin,
|
|
644
|
+
devicePollSchedule,
|
|
645
|
+
lookupEmail,
|
|
646
|
+
rememberSession,
|
|
647
|
+
requestDeviceCode,
|
|
648
|
+
sessionsLayer,
|
|
649
|
+
signIn
|
|
650
|
+
};
|
|
651
|
+
//# sourceMappingURL=auth.js.map
|