@ai-coders/installer 0.3.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/CHANGELOG.md +25 -0
- package/LICENSE +18 -0
- package/README.md +64 -0
- package/dist/callback-page.d.ts +11 -0
- package/dist/callback-page.js +230 -0
- package/dist/callback-page.js.map +1 -0
- package/dist/clerk-auth.d.ts +18 -0
- package/dist/clerk-auth.js +296 -0
- package/dist/clerk-auth.js.map +1 -0
- package/dist/cli-command.d.ts +10 -0
- package/dist/cli-command.js +17 -0
- package/dist/cli-command.js.map +1 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +84 -0
- package/dist/cli.js.map +1 -0
- package/dist/i18n.d.ts +52 -0
- package/dist/i18n.js +163 -0
- package/dist/i18n.js.map +1 -0
- package/dist/product-manifest.d.ts +43 -0
- package/dist/product-manifest.js +140 -0
- package/dist/product-manifest.js.map +1 -0
- package/dist/products/second-brain/apply.d.ts +86 -0
- package/dist/products/second-brain/apply.js +277 -0
- package/dist/products/second-brain/apply.js.map +1 -0
- package/dist/products/second-brain/bearer.d.ts +29 -0
- package/dist/products/second-brain/bearer.js +78 -0
- package/dist/products/second-brain/bearer.js.map +1 -0
- package/dist/products/second-brain/bootstrap.d.ts +33 -0
- package/dist/products/second-brain/bootstrap.js +39 -0
- package/dist/products/second-brain/bootstrap.js.map +1 -0
- package/dist/products/second-brain/envfile.d.ts +35 -0
- package/dist/products/second-brain/envfile.js +136 -0
- package/dist/products/second-brain/envfile.js.map +1 -0
- package/dist/products/second-brain/github.d.ts +77 -0
- package/dist/products/second-brain/github.js +195 -0
- package/dist/products/second-brain/github.js.map +1 -0
- package/dist/products/second-brain/hostinger.d.ts +140 -0
- package/dist/products/second-brain/hostinger.js +607 -0
- package/dist/products/second-brain/hostinger.js.map +1 -0
- package/dist/products/second-brain/i18n.d.ts +887 -0
- package/dist/products/second-brain/i18n.js +587 -0
- package/dist/products/second-brain/i18n.js.map +1 -0
- package/dist/products/second-brain/keys.d.ts +5 -0
- package/dist/products/second-brain/keys.js +16 -0
- package/dist/products/second-brain/keys.js.map +1 -0
- package/dist/products/second-brain/main.d.ts +3 -0
- package/dist/products/second-brain/main.js +78 -0
- package/dist/products/second-brain/main.js.map +1 -0
- package/dist/products/second-brain/ports.d.ts +34 -0
- package/dist/products/second-brain/ports.js +77 -0
- package/dist/products/second-brain/ports.js.map +1 -0
- package/dist/products/second-brain/recovery.d.ts +50 -0
- package/dist/products/second-brain/recovery.js +96 -0
- package/dist/products/second-brain/recovery.js.map +1 -0
- package/dist/products/second-brain/sealed-box.d.ts +1 -0
- package/dist/products/second-brain/sealed-box.js +11 -0
- package/dist/products/second-brain/sealed-box.js.map +1 -0
- package/dist/products/second-brain/server.d.ts +143 -0
- package/dist/products/second-brain/server.js +280 -0
- package/dist/products/second-brain/server.js.map +1 -0
- package/dist/products/second-brain/service.d.ts +26 -0
- package/dist/products/second-brain/service.js +806 -0
- package/dist/products/second-brain/service.js.map +1 -0
- package/dist/products/second-brain/ssh.d.ts +35 -0
- package/dist/products/second-brain/ssh.js +140 -0
- package/dist/products/second-brain/ssh.js.map +1 -0
- package/dist/products/second-brain/state.d.ts +27 -0
- package/dist/products/second-brain/state.js +60 -0
- package/dist/products/second-brain/state.js.map +1 -0
- package/dist/products/second-brain/ui/assets/index-BoXpakUt.css +2 -0
- package/dist/products/second-brain/ui/assets/index-MgfOpW_R.js +66 -0
- package/dist/products/second-brain/ui/index.html +15 -0
- package/dist/products/second-brain/verify.d.ts +23 -0
- package/dist/products/second-brain/verify.js +59 -0
- package/dist/products/second-brain/verify.js.map +1 -0
- package/dist/products/severino/apply.d.ts +87 -0
- package/dist/products/severino/apply.js +250 -0
- package/dist/products/severino/apply.js.map +1 -0
- package/dist/products/severino/domain.d.ts +6 -0
- package/dist/products/severino/domain.js +20 -0
- package/dist/products/severino/domain.js.map +1 -0
- package/dist/products/severino/envfile.d.ts +20 -0
- package/dist/products/severino/envfile.js +89 -0
- package/dist/products/severino/envfile.js.map +1 -0
- package/dist/products/severino/github.d.ts +77 -0
- package/dist/products/severino/github.js +195 -0
- package/dist/products/severino/github.js.map +1 -0
- package/dist/products/severino/hostinger.d.ts +140 -0
- package/dist/products/severino/hostinger.js +607 -0
- package/dist/products/severino/hostinger.js.map +1 -0
- package/dist/products/severino/i18n.d.ts +887 -0
- package/dist/products/severino/i18n.js +587 -0
- package/dist/products/severino/i18n.js.map +1 -0
- package/dist/products/severino/keys.d.ts +5 -0
- package/dist/products/severino/keys.js +16 -0
- package/dist/products/severino/keys.js.map +1 -0
- package/dist/products/severino/main.d.ts +3 -0
- package/dist/products/severino/main.js +78 -0
- package/dist/products/severino/main.js.map +1 -0
- package/dist/products/severino/recovery.d.ts +50 -0
- package/dist/products/severino/recovery.js +96 -0
- package/dist/products/severino/recovery.js.map +1 -0
- package/dist/products/severino/sealed-box.d.ts +1 -0
- package/dist/products/severino/sealed-box.js +11 -0
- package/dist/products/severino/sealed-box.js.map +1 -0
- package/dist/products/severino/server.d.ts +123 -0
- package/dist/products/severino/server.js +274 -0
- package/dist/products/severino/server.js.map +1 -0
- package/dist/products/severino/service.d.ts +28 -0
- package/dist/products/severino/service.js +752 -0
- package/dist/products/severino/service.js.map +1 -0
- package/dist/products/severino/slack-manifest.d.ts +3 -0
- package/dist/products/severino/slack-manifest.js +45 -0
- package/dist/products/severino/slack-manifest.js.map +1 -0
- package/dist/products/severino/ssh.d.ts +35 -0
- package/dist/products/severino/ssh.js +140 -0
- package/dist/products/severino/ssh.js.map +1 -0
- package/dist/products/severino/state.d.ts +26 -0
- package/dist/products/severino/state.js +61 -0
- package/dist/products/severino/state.js.map +1 -0
- package/dist/products/severino/ui/assets/index-7S8Hl4jq.js +96 -0
- package/dist/products/severino/ui/assets/index-D5kql1Bh.css +2 -0
- package/dist/products/severino/ui/index.html +15 -0
- package/dist/products/severino/verify.d.ts +22 -0
- package/dist/products/severino/verify.js +149 -0
- package/dist/products/severino/verify.js.map +1 -0
- package/package.json +53 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# @ai-coders/installer
|
|
2
|
+
|
|
3
|
+
## [0.3.0](https://github.com/vinilana/ai-coders-installers/compare/v0.2.0...v0.3.0) (2026-08-19)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Features
|
|
7
|
+
|
|
8
|
+
* add Severino product support and refactor installer CLI ([339ee94](https://github.com/vinilana/ai-coders-installers/commit/339ee9460894ed835cde160aaa02bc13f94c4ccb))
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Bug Fixes
|
|
12
|
+
|
|
13
|
+
* run package audit through active npm CLI ([e5c6932](https://github.com/vinilana/ai-coders-installers/commit/e5c69327ce48cd7981602de40ae78eb04329ad48))
|
|
14
|
+
|
|
15
|
+
## [0.2.0](https://github.com/vinilana/ai-coders-installers/compare/v0.1.0...v0.2.0) (2026-08-16)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
### Features
|
|
19
|
+
|
|
20
|
+
* add initial release workflow, CI configuration, and update package metadata ([0fc19d4](https://github.com/vinilana/ai-coders-installers/commit/0fc19d46cafdec7a7b8366376b44412d77224127))
|
|
21
|
+
* add skills for managing AI Coders installer products, including adding, developing, and releasing installers ([7b33f97](https://github.com/vinilana/ai-coders-installers/commit/7b33f97c4e93aaa8c1c901aec7de14e64c060742))
|
|
22
|
+
|
|
23
|
+
## 0.1.0
|
|
24
|
+
|
|
25
|
+
- Initial public CLI package for authenticated AI Coders product installers.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
Proprietary Software Notice
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 AI Coders. All rights reserved.
|
|
4
|
+
|
|
5
|
+
This software is proprietary to AI Coders. Its availability through the public
|
|
6
|
+
npm registry is solely intended to let authorized users execute the installer
|
|
7
|
+
with npx.
|
|
8
|
+
|
|
9
|
+
Except as expressly authorized by a separate written agreement or applicable
|
|
10
|
+
AI Coders terms of service, this notice does not grant any license to use,
|
|
11
|
+
copy, modify, distribute, sublicense, sell, reverse engineer, or create
|
|
12
|
+
derivative works from this software.
|
|
13
|
+
|
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
15
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
16
|
+
FITNESS FOR A PARTICULAR PURPOSE, AND NONINFRINGEMENT. TO THE MAXIMUM EXTENT
|
|
17
|
+
PERMITTED BY LAW, AI CODERS SHALL NOT BE LIABLE FOR ANY CLAIM, DAMAGES, OR
|
|
18
|
+
OTHER LIABILITY ARISING FROM OR RELATED TO THE SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# AI Coders Installers
|
|
2
|
+
|
|
3
|
+
This repository publishes the product-aware `@ai-coders/installer` CLI while
|
|
4
|
+
keeping installation code separate from the project templates shared with AI
|
|
5
|
+
Coders members.
|
|
6
|
+
|
|
7
|
+
This is proprietary software. The package is published to the public npm
|
|
8
|
+
registry only so authorized AI Coders users can execute it with `npx`.
|
|
9
|
+
Registry availability does not make the project open source or grant reuse,
|
|
10
|
+
modification, or redistribution rights. See [LICENSE](LICENSE).
|
|
11
|
+
|
|
12
|
+
## Use
|
|
13
|
+
|
|
14
|
+
Run the installer from the root of a supported product repository:
|
|
15
|
+
|
|
16
|
+
```sh
|
|
17
|
+
npx @ai-coders/installer second-brain
|
|
18
|
+
npx @ai-coders/installer severino
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
The command always reads the product's contract from fixed files in the
|
|
22
|
+
current directory — `./invokta.mcp.json` for Second Brain, `./package.json`
|
|
23
|
+
for Severino — and does not accept a repository path or an alternate manifest
|
|
24
|
+
file. The requested product must match the contract before any product
|
|
25
|
+
installer starts. The installer then opens Clerk in the browser and starts the
|
|
26
|
+
product only after a successful AI Coders sign-in.
|
|
27
|
+
|
|
28
|
+
The public OAuth client configuration is bundled with the package, so members
|
|
29
|
+
do not need to provide Clerk settings. Tokens and user credentials remain in
|
|
30
|
+
memory for the current command and are never persisted. See
|
|
31
|
+
[Installer authentication](docs/authentication.md) for the one-time Clerk
|
|
32
|
+
application setup and security contract.
|
|
33
|
+
|
|
34
|
+
## Develop
|
|
35
|
+
|
|
36
|
+
```sh
|
|
37
|
+
npm install
|
|
38
|
+
npm run check
|
|
39
|
+
npm start -- second-brain
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
The monorepo currently contains:
|
|
43
|
+
|
|
44
|
+
- `src/`: public CLI, manifest validation, and product dispatch.
|
|
45
|
+
- `packages/second-brain/`: the private Second Brain installer — node-side
|
|
46
|
+
pipeline under `src/`, plus a React + shadcn/Tailwind web UI under `ui/`
|
|
47
|
+
that Vite builds into `dist/products/second-brain/ui/`.
|
|
48
|
+
- `packages/severino/`: the private Severino installer — node-side pipeline
|
|
49
|
+
under `src/`, plus a React + shadcn/Tailwind web UI under `ui/` that Vite
|
|
50
|
+
builds into `dist/products/severino/ui/`.
|
|
51
|
+
- `packages/installer-ui/`: the private, source-only React + shadcn/Tailwind
|
|
52
|
+
kit shared by both product UIs through a Vite alias. It is not published.
|
|
53
|
+
- `docs/adding-product.md`: the contract for adding another product installer.
|
|
54
|
+
|
|
55
|
+
The public tarball contains the root CLI and compiled product implementations;
|
|
56
|
+
the private workspace packages are not published independently.
|
|
57
|
+
|
|
58
|
+
## Release
|
|
59
|
+
|
|
60
|
+
Conventional Commit titles drive an automated Release PR with the version and
|
|
61
|
+
changelog update. Merging that PR publishes `@ai-coders/installer` through npm
|
|
62
|
+
trusted publishing without a long-lived npm token. See
|
|
63
|
+
[Releases and npm publishing](docs/releases.md) for the maintainer workflow and
|
|
64
|
+
one-time registry bootstrap.
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export type AuthenticationCallbackStatus = "success" | "failure";
|
|
2
|
+
export interface AuthenticationCallbackPageContent {
|
|
3
|
+
readonly language: string;
|
|
4
|
+
readonly title: string;
|
|
5
|
+
readonly status: AuthenticationCallbackStatus;
|
|
6
|
+
readonly statusLabel: string;
|
|
7
|
+
readonly heading: string;
|
|
8
|
+
readonly description: string;
|
|
9
|
+
readonly footer: string;
|
|
10
|
+
}
|
|
11
|
+
export declare function renderAuthenticationCallbackPage(content: AuthenticationCallbackPageContent): string;
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
const aiCodersLogo = `<svg class="brand-logo" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 215 44" width="215" height="44" role="img" aria-label="AI Coders Academy">
|
|
2
|
+
<g transform="translate(0, 6)" fill="none" stroke="#FFFFFF">
|
|
3
|
+
<rect x="8" y="8" width="24" height="24" stroke-width="1"></rect>
|
|
4
|
+
<rect x="4" y="4" width="24" height="24" stroke-width="1.5"></rect>
|
|
5
|
+
<rect x="0" y="0" width="24" height="24" stroke-width="2"></rect>
|
|
6
|
+
</g>
|
|
7
|
+
<g font-family="Geist, Inter, ui-sans-serif, system-ui, sans-serif" letter-spacing="-0.6">
|
|
8
|
+
<text x="44" y="28" font-size="18" font-weight="600" fill="#FFFFFF">AI Coders <tspan font-weight="300" fill="#A1A1AA">Academy</tspan></text>
|
|
9
|
+
</g>
|
|
10
|
+
</svg>`;
|
|
11
|
+
const statusIcons = {
|
|
12
|
+
success: `<svg viewBox="0 0 24 24" aria-hidden="true"><path d="m6.75 12.5 3.25 3.25 7.25-7.5"/></svg>`,
|
|
13
|
+
failure: `<svg viewBox="0 0 24 24" aria-hidden="true"><path d="m8 8 8 8m0-8-8 8"/></svg>`,
|
|
14
|
+
};
|
|
15
|
+
function escapeHtml(value) {
|
|
16
|
+
return value
|
|
17
|
+
.replaceAll("&", "&")
|
|
18
|
+
.replaceAll("<", "<")
|
|
19
|
+
.replaceAll(">", ">")
|
|
20
|
+
.replaceAll('"', """)
|
|
21
|
+
.replaceAll("'", "'");
|
|
22
|
+
}
|
|
23
|
+
export function renderAuthenticationCallbackPage(content) {
|
|
24
|
+
const language = escapeHtml(content.language);
|
|
25
|
+
const title = escapeHtml(content.title);
|
|
26
|
+
const statusLabel = escapeHtml(content.statusLabel);
|
|
27
|
+
const heading = escapeHtml(content.heading);
|
|
28
|
+
const description = escapeHtml(content.description);
|
|
29
|
+
const footer = escapeHtml(content.footer);
|
|
30
|
+
return `<!doctype html>
|
|
31
|
+
<html lang="${language}" data-auth-status="${content.status}">
|
|
32
|
+
<head>
|
|
33
|
+
<meta charset="utf-8">
|
|
34
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
35
|
+
<meta name="color-scheme" content="dark">
|
|
36
|
+
<meta name="theme-color" content="#070707">
|
|
37
|
+
<title>${title}</title>
|
|
38
|
+
<style>
|
|
39
|
+
:root {
|
|
40
|
+
color: #fafafa;
|
|
41
|
+
background: #070707;
|
|
42
|
+
font-family: Geist, Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
|
43
|
+
font-synthesis: none;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
* { box-sizing: border-box; }
|
|
47
|
+
|
|
48
|
+
body {
|
|
49
|
+
min-width: 320px;
|
|
50
|
+
min-height: 100vh;
|
|
51
|
+
min-height: 100svh;
|
|
52
|
+
margin: 0;
|
|
53
|
+
display: grid;
|
|
54
|
+
place-items: center;
|
|
55
|
+
overflow: hidden;
|
|
56
|
+
background:
|
|
57
|
+
radial-gradient(circle at 50% -12%, rgba(255, 255, 255, 0.14), transparent 36rem),
|
|
58
|
+
#070707;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
body::before {
|
|
62
|
+
position: fixed;
|
|
63
|
+
inset: 0;
|
|
64
|
+
content: "";
|
|
65
|
+
pointer-events: none;
|
|
66
|
+
opacity: 0.16;
|
|
67
|
+
background-image:
|
|
68
|
+
linear-gradient(rgba(255, 255, 255, 0.08) 1px, transparent 1px),
|
|
69
|
+
linear-gradient(90deg, rgba(255, 255, 255, 0.08) 1px, transparent 1px);
|
|
70
|
+
background-size: 44px 44px;
|
|
71
|
+
mask-image: linear-gradient(to bottom, black, transparent 72%);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
main {
|
|
75
|
+
position: relative;
|
|
76
|
+
width: min(100% - 32px, 680px);
|
|
77
|
+
margin: 32px auto;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
.brand {
|
|
81
|
+
display: flex;
|
|
82
|
+
align-items: center;
|
|
83
|
+
margin: 0 0 28px 4px;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
.brand-logo {
|
|
87
|
+
display: block;
|
|
88
|
+
width: min(215px, 72vw);
|
|
89
|
+
height: auto;
|
|
90
|
+
overflow: visible;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
.panel {
|
|
94
|
+
position: relative;
|
|
95
|
+
overflow: hidden;
|
|
96
|
+
padding: clamp(32px, 7vw, 56px);
|
|
97
|
+
border: 1px solid #292929;
|
|
98
|
+
border-radius: 2px;
|
|
99
|
+
background: rgba(12, 12, 12, 0.94);
|
|
100
|
+
box-shadow: 0 32px 80px rgba(0, 0, 0, 0.48);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
.panel::before,
|
|
104
|
+
.panel::after {
|
|
105
|
+
position: absolute;
|
|
106
|
+
width: 48px;
|
|
107
|
+
height: 48px;
|
|
108
|
+
content: "";
|
|
109
|
+
pointer-events: none;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
.panel::before {
|
|
113
|
+
top: -24px;
|
|
114
|
+
right: -24px;
|
|
115
|
+
border: 1px solid #52525b;
|
|
116
|
+
box-shadow: -8px 8px 0 -1px #0c0c0c, -8px 8px 0 0 #3f3f46;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
.panel::after {
|
|
120
|
+
right: 0;
|
|
121
|
+
bottom: 0;
|
|
122
|
+
border-right: 2px solid var(--status-color);
|
|
123
|
+
border-bottom: 2px solid var(--status-color);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
html[data-auth-status="success"] { --status-color: #6ee7b7; }
|
|
127
|
+
html[data-auth-status="failure"] { --status-color: #fb7185; }
|
|
128
|
+
|
|
129
|
+
.status {
|
|
130
|
+
display: inline-flex;
|
|
131
|
+
align-items: center;
|
|
132
|
+
gap: 10px;
|
|
133
|
+
color: var(--status-color);
|
|
134
|
+
font-size: 12px;
|
|
135
|
+
font-weight: 650;
|
|
136
|
+
letter-spacing: 0.12em;
|
|
137
|
+
line-height: 1.4;
|
|
138
|
+
text-transform: uppercase;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
.status-icon {
|
|
142
|
+
display: grid;
|
|
143
|
+
width: 28px;
|
|
144
|
+
height: 28px;
|
|
145
|
+
place-items: center;
|
|
146
|
+
border: 1px solid currentColor;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
.status-icon svg {
|
|
150
|
+
width: 17px;
|
|
151
|
+
height: 17px;
|
|
152
|
+
fill: none;
|
|
153
|
+
stroke: currentColor;
|
|
154
|
+
stroke-linecap: square;
|
|
155
|
+
stroke-linejoin: miter;
|
|
156
|
+
stroke-width: 1.75;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
h1 {
|
|
160
|
+
max-width: 12ch;
|
|
161
|
+
margin: 30px 0 16px;
|
|
162
|
+
font-size: clamp(36px, 8vw, 64px);
|
|
163
|
+
font-weight: 520;
|
|
164
|
+
letter-spacing: -0.055em;
|
|
165
|
+
line-height: 0.98;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
.description {
|
|
169
|
+
max-width: 47ch;
|
|
170
|
+
margin: 0;
|
|
171
|
+
color: #a1a1aa;
|
|
172
|
+
font-size: clamp(16px, 2.5vw, 18px);
|
|
173
|
+
line-height: 1.65;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
.footer {
|
|
177
|
+
display: flex;
|
|
178
|
+
align-items: center;
|
|
179
|
+
gap: 12px;
|
|
180
|
+
margin-top: 40px;
|
|
181
|
+
padding-top: 20px;
|
|
182
|
+
border-top: 1px solid #27272a;
|
|
183
|
+
color: #71717a;
|
|
184
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
|
|
185
|
+
font-size: 11px;
|
|
186
|
+
letter-spacing: 0.08em;
|
|
187
|
+
text-transform: uppercase;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
.footer-mark {
|
|
191
|
+
width: 7px;
|
|
192
|
+
height: 7px;
|
|
193
|
+
border: 1px solid #a1a1aa;
|
|
194
|
+
box-shadow: 3px 3px 0 -1px #0c0c0c, 3px 3px 0 0 #71717a;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
@media (max-width: 480px) {
|
|
198
|
+
main { width: min(100% - 24px, 680px); }
|
|
199
|
+
.brand { margin-bottom: 20px; }
|
|
200
|
+
.panel { padding: 28px 24px 30px; }
|
|
201
|
+
h1 { margin-top: 26px; }
|
|
202
|
+
.footer { margin-top: 32px; }
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
@media (prefers-reduced-motion: no-preference) {
|
|
206
|
+
.panel { animation: enter 420ms cubic-bezier(0.22, 1, 0.36, 1) both; }
|
|
207
|
+
@keyframes enter {
|
|
208
|
+
from { opacity: 0; transform: translateY(8px); }
|
|
209
|
+
to { opacity: 1; transform: translateY(0); }
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
</style>
|
|
213
|
+
</head>
|
|
214
|
+
<body>
|
|
215
|
+
<main aria-labelledby="callback-heading">
|
|
216
|
+
<div class="brand">${aiCodersLogo}</div>
|
|
217
|
+
<section class="panel">
|
|
218
|
+
<div class="status">
|
|
219
|
+
<span class="status-icon">${statusIcons[content.status]}</span>
|
|
220
|
+
<span>${statusLabel}</span>
|
|
221
|
+
</div>
|
|
222
|
+
<h1 id="callback-heading">${heading}</h1>
|
|
223
|
+
<p class="description">${description}</p>
|
|
224
|
+
<div class="footer"><span class="footer-mark" aria-hidden="true"></span>${footer}</div>
|
|
225
|
+
</section>
|
|
226
|
+
</main>
|
|
227
|
+
</body>
|
|
228
|
+
</html>`;
|
|
229
|
+
}
|
|
230
|
+
//# sourceMappingURL=callback-page.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"callback-page.js","sourceRoot":"","sources":["../src/callback-page.ts"],"names":[],"mappings":"AAYA,MAAM,YAAY,GAAG;;;;;;;;;OASd,CAAC;AAER,MAAM,WAAW,GAAiD;IAChE,OAAO,EAAE,6FAA6F;IACtG,OAAO,EAAE,gFAAgF;CAC1F,CAAC;AAEF,SAAS,UAAU,CAAC,KAAa;IAC/B,OAAO,KAAK;SACT,UAAU,CAAC,GAAG,EAAE,OAAO,CAAC;SACxB,UAAU,CAAC,GAAG,EAAE,MAAM,CAAC;SACvB,UAAU,CAAC,GAAG,EAAE,MAAM,CAAC;SACvB,UAAU,CAAC,GAAG,EAAE,QAAQ,CAAC;SACzB,UAAU,CAAC,GAAG,EAAE,OAAO,CAAC,CAAC;AAC9B,CAAC;AAED,MAAM,UAAU,gCAAgC,CAC9C,OAA0C;IAE1C,MAAM,QAAQ,GAAG,UAAU,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC;IAC9C,MAAM,KAAK,GAAG,UAAU,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;IACxC,MAAM,WAAW,GAAG,UAAU,CAAC,OAAO,CAAC,WAAW,CAAC,CAAC;IACpD,MAAM,OAAO,GAAG,UAAU,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC;IAC5C,MAAM,WAAW,GAAG,UAAU,CAAC,OAAO,CAAC,WAAW,CAAC,CAAC;IACpD,MAAM,MAAM,GAAG,UAAU,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC;IAE1C,OAAO;cACK,QAAQ,uBAAuB,OAAO,CAAC,MAAM;;;;;;WAMhD,KAAK;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;yBAmLS,YAAY;;;oCAGD,WAAW,CAAC,OAAO,CAAC,MAAM,CAAC;gBAC/C,WAAW;;kCAEO,OAAO;+BACV,WAAW;gFACsC,MAAM;;;;QAI9E,CAAC;AACT,CAAC"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export declare const clerkOAuthIssuer = "https://clerk.aicoders.academy";
|
|
2
|
+
export declare const clerkOAuthClientId = "g43bqqz4g6o6uEV7";
|
|
3
|
+
type FetchImplementation = (input: string | URL, init?: RequestInit) => Promise<Response>;
|
|
4
|
+
export interface AuthenticatedClerkUser {
|
|
5
|
+
readonly id: string;
|
|
6
|
+
readonly email?: string;
|
|
7
|
+
readonly name?: string;
|
|
8
|
+
}
|
|
9
|
+
export interface AuthenticateWithClerkOptions {
|
|
10
|
+
readonly issuer: string;
|
|
11
|
+
readonly clientId: string;
|
|
12
|
+
readonly writeOutput: (value: string) => void;
|
|
13
|
+
readonly fetch?: FetchImplementation;
|
|
14
|
+
readonly openBrowser?: (url: string) => Promise<void>;
|
|
15
|
+
readonly callbackTimeoutMs?: number;
|
|
16
|
+
}
|
|
17
|
+
export declare function authenticateWithClerk(options: AuthenticateWithClerkOptions): Promise<AuthenticatedClerkUser>;
|
|
18
|
+
export {};
|
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { createHash, randomBytes, timingSafeEqual } from "node:crypto";
|
|
3
|
+
import { createServer } from "node:http";
|
|
4
|
+
import { cliMessages } from "./i18n.js";
|
|
5
|
+
export const clerkOAuthIssuer = "https://clerk.aicoders.academy";
|
|
6
|
+
export const clerkOAuthClientId = "g43bqqz4g6o6uEV7";
|
|
7
|
+
const clerkOAuthScopes = ["openid", "profile", "email"];
|
|
8
|
+
const defaultCallbackTimeoutMs = 120_000;
|
|
9
|
+
const clerkRequestTimeoutMs = 15_000;
|
|
10
|
+
const successPage = cliMessages.authenticationCompletePage;
|
|
11
|
+
const failurePage = cliMessages.authenticationFailedPage;
|
|
12
|
+
function normalizeIssuer(value) {
|
|
13
|
+
const normalized = value.trim().replace(/\/+$/, "");
|
|
14
|
+
let url;
|
|
15
|
+
try {
|
|
16
|
+
url = new URL(normalized);
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
throw new Error(cliMessages.oauthIssuerInvalidUrl);
|
|
20
|
+
}
|
|
21
|
+
if (url.protocol !== "https:" &&
|
|
22
|
+
!(url.protocol === "http:" && url.hostname === "127.0.0.1")) {
|
|
23
|
+
throw new Error(cliMessages.oauthIssuerRequiresHttps);
|
|
24
|
+
}
|
|
25
|
+
if (url.username !== "" ||
|
|
26
|
+
url.password !== "" ||
|
|
27
|
+
url.pathname !== "/" ||
|
|
28
|
+
url.search !== "" ||
|
|
29
|
+
url.hash !== "") {
|
|
30
|
+
throw new Error(cliMessages.oauthIssuerUrlInvalid);
|
|
31
|
+
}
|
|
32
|
+
return normalized;
|
|
33
|
+
}
|
|
34
|
+
function secureEqual(received, expected) {
|
|
35
|
+
const receivedBuffer = Buffer.from(received);
|
|
36
|
+
const expectedBuffer = Buffer.from(expected);
|
|
37
|
+
return (receivedBuffer.length === expectedBuffer.length &&
|
|
38
|
+
timingSafeEqual(receivedBuffer, expectedBuffer));
|
|
39
|
+
}
|
|
40
|
+
function callbackHeaders(contentType) {
|
|
41
|
+
return {
|
|
42
|
+
"Cache-Control": "no-store",
|
|
43
|
+
Connection: "close",
|
|
44
|
+
"Content-Security-Policy": "default-src 'none'; style-src 'unsafe-inline'",
|
|
45
|
+
"Content-Type": contentType,
|
|
46
|
+
"X-Content-Type-Options": "nosniff",
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
function endHtml(response, status, html, afterResponse) {
|
|
50
|
+
response.writeHead(status, callbackHeaders("text/html; charset=utf-8"));
|
|
51
|
+
response.end(html, afterResponse);
|
|
52
|
+
}
|
|
53
|
+
async function startOAuthCallbackServer(options) {
|
|
54
|
+
let settled = false;
|
|
55
|
+
let timeout;
|
|
56
|
+
let resolveCode;
|
|
57
|
+
let rejectCode;
|
|
58
|
+
const codePromise = new Promise((resolve, reject) => {
|
|
59
|
+
resolveCode = resolve;
|
|
60
|
+
rejectCode = reject;
|
|
61
|
+
});
|
|
62
|
+
const server = createServer((request, response) => {
|
|
63
|
+
if (request.method !== "GET" || request.url === undefined) {
|
|
64
|
+
response.writeHead(404, callbackHeaders("text/plain; charset=utf-8"));
|
|
65
|
+
response.end(cliMessages.callbackNotFound);
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
const url = new URL(request.url, "http://127.0.0.1");
|
|
69
|
+
if (url.pathname !== "/callback") {
|
|
70
|
+
response.writeHead(404, callbackHeaders("text/plain; charset=utf-8"));
|
|
71
|
+
response.end(cliMessages.callbackNotFound);
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
if (settled) {
|
|
75
|
+
response.writeHead(410, callbackHeaders("text/plain; charset=utf-8"));
|
|
76
|
+
response.end(cliMessages.callbackAlreadyHandled);
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
const errorCode = url.searchParams.get("error");
|
|
80
|
+
const state = url.searchParams.get("state");
|
|
81
|
+
const code = url.searchParams.get("code");
|
|
82
|
+
const finish = (status, page, settlePromise) => {
|
|
83
|
+
settled = true;
|
|
84
|
+
if (timeout !== undefined)
|
|
85
|
+
clearTimeout(timeout);
|
|
86
|
+
endHtml(response, status, page, () => {
|
|
87
|
+
settlePromise();
|
|
88
|
+
server.close();
|
|
89
|
+
});
|
|
90
|
+
};
|
|
91
|
+
if (errorCode !== null) {
|
|
92
|
+
finish(400, failurePage, () => {
|
|
93
|
+
const safeErrorCode = errorCode
|
|
94
|
+
.replace(/[^a-z0-9_-]/gi, "_")
|
|
95
|
+
.slice(0, 64);
|
|
96
|
+
rejectCode(new Error(cliMessages.authenticationNotApproved(safeErrorCode)));
|
|
97
|
+
});
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
if (state === null || !secureEqual(state, options.expectedState)) {
|
|
101
|
+
finish(400, failurePage, () => {
|
|
102
|
+
rejectCode(new Error(cliMessages.callbackStateMismatch));
|
|
103
|
+
});
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
if (code === null || code === "") {
|
|
107
|
+
finish(400, failurePage, () => {
|
|
108
|
+
rejectCode(new Error(cliMessages.callbackMissingCode));
|
|
109
|
+
});
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
finish(200, successPage, () => {
|
|
113
|
+
resolveCode(code);
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
try {
|
|
117
|
+
await new Promise((resolve, reject) => {
|
|
118
|
+
server.once("error", reject);
|
|
119
|
+
server.listen(0, "127.0.0.1", resolve);
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
catch {
|
|
123
|
+
throw new Error(cliMessages.callbackServerStartFailed);
|
|
124
|
+
}
|
|
125
|
+
const address = server.address();
|
|
126
|
+
timeout = setTimeout(() => {
|
|
127
|
+
if (settled)
|
|
128
|
+
return;
|
|
129
|
+
settled = true;
|
|
130
|
+
rejectCode(new Error(cliMessages.authenticationTimedOut));
|
|
131
|
+
server.close();
|
|
132
|
+
}, options.timeoutMs);
|
|
133
|
+
return {
|
|
134
|
+
redirectUri: `http://127.0.0.1:${address.port}/callback`,
|
|
135
|
+
waitForCode: () => codePromise,
|
|
136
|
+
close() {
|
|
137
|
+
if (timeout !== undefined)
|
|
138
|
+
clearTimeout(timeout);
|
|
139
|
+
if (!settled)
|
|
140
|
+
settled = true;
|
|
141
|
+
if (server.listening)
|
|
142
|
+
server.close();
|
|
143
|
+
},
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
function buildAuthorizationUrl(options) {
|
|
147
|
+
const url = new URL(`${options.issuer}/oauth/authorize`);
|
|
148
|
+
url.searchParams.set("response_type", "code");
|
|
149
|
+
url.searchParams.set("client_id", options.clientId);
|
|
150
|
+
url.searchParams.set("redirect_uri", options.redirectUri);
|
|
151
|
+
url.searchParams.set("scope", clerkOAuthScopes.join(" "));
|
|
152
|
+
url.searchParams.set("state", options.state);
|
|
153
|
+
url.searchParams.set("code_challenge", options.codeChallenge);
|
|
154
|
+
url.searchParams.set("code_challenge_method", "S256");
|
|
155
|
+
return url;
|
|
156
|
+
}
|
|
157
|
+
async function openBrowserBestEffort(url) {
|
|
158
|
+
const command = process.platform === "darwin"
|
|
159
|
+
? { file: "open", args: [url] }
|
|
160
|
+
: process.platform === "win32"
|
|
161
|
+
? { file: "explorer.exe", args: [url] }
|
|
162
|
+
: { file: "xdg-open", args: [url] };
|
|
163
|
+
try {
|
|
164
|
+
const child = spawn(command.file, command.args, {
|
|
165
|
+
detached: true,
|
|
166
|
+
stdio: "ignore",
|
|
167
|
+
});
|
|
168
|
+
child.on("error", () => { });
|
|
169
|
+
child.unref();
|
|
170
|
+
}
|
|
171
|
+
catch {
|
|
172
|
+
// Printing the URL is the reliable fallback on every platform.
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
async function exchangeAuthorizationCode(options) {
|
|
176
|
+
const body = new URLSearchParams({
|
|
177
|
+
grant_type: "authorization_code",
|
|
178
|
+
client_id: options.clientId,
|
|
179
|
+
code: options.code,
|
|
180
|
+
code_verifier: options.codeVerifier,
|
|
181
|
+
redirect_uri: options.redirectUri,
|
|
182
|
+
});
|
|
183
|
+
let response;
|
|
184
|
+
try {
|
|
185
|
+
response = await options.fetch(`${options.issuer}/oauth/token`, {
|
|
186
|
+
method: "POST",
|
|
187
|
+
headers: {
|
|
188
|
+
Accept: "application/json",
|
|
189
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
190
|
+
},
|
|
191
|
+
body: body.toString(),
|
|
192
|
+
signal: AbortSignal.timeout(clerkRequestTimeoutMs),
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
catch {
|
|
196
|
+
throw new Error(cliMessages.tokenRequestFailed);
|
|
197
|
+
}
|
|
198
|
+
if (!response.ok) {
|
|
199
|
+
throw new Error(cliMessages.authorizationCodeRejected(response.status));
|
|
200
|
+
}
|
|
201
|
+
let value;
|
|
202
|
+
try {
|
|
203
|
+
value = (await response.json());
|
|
204
|
+
}
|
|
205
|
+
catch {
|
|
206
|
+
throw new Error(cliMessages.tokenResponseInvalidJson);
|
|
207
|
+
}
|
|
208
|
+
if (typeof value.access_token !== "string" || value.access_token === "") {
|
|
209
|
+
throw new Error(cliMessages.tokenResponseMissingAccessToken);
|
|
210
|
+
}
|
|
211
|
+
return value.access_token;
|
|
212
|
+
}
|
|
213
|
+
async function fetchClerkUser(options) {
|
|
214
|
+
let response;
|
|
215
|
+
try {
|
|
216
|
+
response = await options.fetch(`${options.issuer}/oauth/userinfo`, {
|
|
217
|
+
headers: { Authorization: `Bearer ${options.accessToken}` },
|
|
218
|
+
signal: AbortSignal.timeout(clerkRequestTimeoutMs),
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
catch {
|
|
222
|
+
throw new Error(cliMessages.userInfoRequestFailed);
|
|
223
|
+
}
|
|
224
|
+
if (!response.ok) {
|
|
225
|
+
throw new Error(cliMessages.authenticatedSessionRejected(response.status));
|
|
226
|
+
}
|
|
227
|
+
let value;
|
|
228
|
+
try {
|
|
229
|
+
value = (await response.json());
|
|
230
|
+
}
|
|
231
|
+
catch {
|
|
232
|
+
throw new Error(cliMessages.userInfoResponseInvalidJson);
|
|
233
|
+
}
|
|
234
|
+
if (typeof value.sub !== "string" || value.sub === "") {
|
|
235
|
+
throw new Error(cliMessages.userInfoResponseMissingUserId);
|
|
236
|
+
}
|
|
237
|
+
return {
|
|
238
|
+
id: value.sub,
|
|
239
|
+
...(typeof value.email === "string" && value.email !== ""
|
|
240
|
+
? { email: value.email }
|
|
241
|
+
: {}),
|
|
242
|
+
...(typeof value.name === "string" && value.name !== ""
|
|
243
|
+
? { name: value.name }
|
|
244
|
+
: {}),
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
export async function authenticateWithClerk(options) {
|
|
248
|
+
const issuer = normalizeIssuer(options.issuer);
|
|
249
|
+
const clientId = options.clientId.trim();
|
|
250
|
+
if (clientId === "") {
|
|
251
|
+
throw new Error(cliMessages.oauthClientIdRequired);
|
|
252
|
+
}
|
|
253
|
+
const codeVerifier = randomBytes(32).toString("base64url");
|
|
254
|
+
const codeChallenge = createHash("sha256")
|
|
255
|
+
.update(codeVerifier)
|
|
256
|
+
.digest("base64url");
|
|
257
|
+
const state = randomBytes(32).toString("base64url");
|
|
258
|
+
const callbackServer = await startOAuthCallbackServer({
|
|
259
|
+
expectedState: state,
|
|
260
|
+
timeoutMs: options.callbackTimeoutMs ?? defaultCallbackTimeoutMs,
|
|
261
|
+
});
|
|
262
|
+
try {
|
|
263
|
+
const authorizeUrl = buildAuthorizationUrl({
|
|
264
|
+
issuer,
|
|
265
|
+
clientId,
|
|
266
|
+
redirectUri: callbackServer.redirectUri,
|
|
267
|
+
state,
|
|
268
|
+
codeChallenge,
|
|
269
|
+
}).toString();
|
|
270
|
+
options.writeOutput(cliMessages.signInPrompt(authorizeUrl));
|
|
271
|
+
const [code] = await Promise.all([
|
|
272
|
+
callbackServer.waitForCode(),
|
|
273
|
+
(options.openBrowser ?? openBrowserBestEffort)(authorizeUrl),
|
|
274
|
+
]);
|
|
275
|
+
const fetchImplementation = options.fetch ?? globalThis.fetch;
|
|
276
|
+
const accessToken = await exchangeAuthorizationCode({
|
|
277
|
+
fetch: fetchImplementation,
|
|
278
|
+
issuer,
|
|
279
|
+
clientId,
|
|
280
|
+
code,
|
|
281
|
+
codeVerifier,
|
|
282
|
+
redirectUri: callbackServer.redirectUri,
|
|
283
|
+
});
|
|
284
|
+
const user = await fetchClerkUser({
|
|
285
|
+
fetch: fetchImplementation,
|
|
286
|
+
issuer,
|
|
287
|
+
accessToken,
|
|
288
|
+
});
|
|
289
|
+
options.writeOutput(cliMessages.authenticatedAs(user.email ?? user.name ?? user.id));
|
|
290
|
+
return user;
|
|
291
|
+
}
|
|
292
|
+
finally {
|
|
293
|
+
callbackServer.close();
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
//# sourceMappingURL=clerk-auth.js.map
|