@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.
Files changed (127) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/LICENSE +18 -0
  3. package/README.md +64 -0
  4. package/dist/callback-page.d.ts +11 -0
  5. package/dist/callback-page.js +230 -0
  6. package/dist/callback-page.js.map +1 -0
  7. package/dist/clerk-auth.d.ts +18 -0
  8. package/dist/clerk-auth.js +296 -0
  9. package/dist/clerk-auth.js.map +1 -0
  10. package/dist/cli-command.d.ts +10 -0
  11. package/dist/cli-command.js +17 -0
  12. package/dist/cli-command.js.map +1 -0
  13. package/dist/cli.d.ts +2 -0
  14. package/dist/cli.js +84 -0
  15. package/dist/cli.js.map +1 -0
  16. package/dist/i18n.d.ts +52 -0
  17. package/dist/i18n.js +163 -0
  18. package/dist/i18n.js.map +1 -0
  19. package/dist/product-manifest.d.ts +43 -0
  20. package/dist/product-manifest.js +140 -0
  21. package/dist/product-manifest.js.map +1 -0
  22. package/dist/products/second-brain/apply.d.ts +86 -0
  23. package/dist/products/second-brain/apply.js +277 -0
  24. package/dist/products/second-brain/apply.js.map +1 -0
  25. package/dist/products/second-brain/bearer.d.ts +29 -0
  26. package/dist/products/second-brain/bearer.js +78 -0
  27. package/dist/products/second-brain/bearer.js.map +1 -0
  28. package/dist/products/second-brain/bootstrap.d.ts +33 -0
  29. package/dist/products/second-brain/bootstrap.js +39 -0
  30. package/dist/products/second-brain/bootstrap.js.map +1 -0
  31. package/dist/products/second-brain/envfile.d.ts +35 -0
  32. package/dist/products/second-brain/envfile.js +136 -0
  33. package/dist/products/second-brain/envfile.js.map +1 -0
  34. package/dist/products/second-brain/github.d.ts +77 -0
  35. package/dist/products/second-brain/github.js +195 -0
  36. package/dist/products/second-brain/github.js.map +1 -0
  37. package/dist/products/second-brain/hostinger.d.ts +140 -0
  38. package/dist/products/second-brain/hostinger.js +607 -0
  39. package/dist/products/second-brain/hostinger.js.map +1 -0
  40. package/dist/products/second-brain/i18n.d.ts +887 -0
  41. package/dist/products/second-brain/i18n.js +587 -0
  42. package/dist/products/second-brain/i18n.js.map +1 -0
  43. package/dist/products/second-brain/keys.d.ts +5 -0
  44. package/dist/products/second-brain/keys.js +16 -0
  45. package/dist/products/second-brain/keys.js.map +1 -0
  46. package/dist/products/second-brain/main.d.ts +3 -0
  47. package/dist/products/second-brain/main.js +78 -0
  48. package/dist/products/second-brain/main.js.map +1 -0
  49. package/dist/products/second-brain/ports.d.ts +34 -0
  50. package/dist/products/second-brain/ports.js +77 -0
  51. package/dist/products/second-brain/ports.js.map +1 -0
  52. package/dist/products/second-brain/recovery.d.ts +50 -0
  53. package/dist/products/second-brain/recovery.js +96 -0
  54. package/dist/products/second-brain/recovery.js.map +1 -0
  55. package/dist/products/second-brain/sealed-box.d.ts +1 -0
  56. package/dist/products/second-brain/sealed-box.js +11 -0
  57. package/dist/products/second-brain/sealed-box.js.map +1 -0
  58. package/dist/products/second-brain/server.d.ts +143 -0
  59. package/dist/products/second-brain/server.js +280 -0
  60. package/dist/products/second-brain/server.js.map +1 -0
  61. package/dist/products/second-brain/service.d.ts +26 -0
  62. package/dist/products/second-brain/service.js +806 -0
  63. package/dist/products/second-brain/service.js.map +1 -0
  64. package/dist/products/second-brain/ssh.d.ts +35 -0
  65. package/dist/products/second-brain/ssh.js +140 -0
  66. package/dist/products/second-brain/ssh.js.map +1 -0
  67. package/dist/products/second-brain/state.d.ts +27 -0
  68. package/dist/products/second-brain/state.js +60 -0
  69. package/dist/products/second-brain/state.js.map +1 -0
  70. package/dist/products/second-brain/ui/assets/index-BoXpakUt.css +2 -0
  71. package/dist/products/second-brain/ui/assets/index-MgfOpW_R.js +66 -0
  72. package/dist/products/second-brain/ui/index.html +15 -0
  73. package/dist/products/second-brain/verify.d.ts +23 -0
  74. package/dist/products/second-brain/verify.js +59 -0
  75. package/dist/products/second-brain/verify.js.map +1 -0
  76. package/dist/products/severino/apply.d.ts +87 -0
  77. package/dist/products/severino/apply.js +250 -0
  78. package/dist/products/severino/apply.js.map +1 -0
  79. package/dist/products/severino/domain.d.ts +6 -0
  80. package/dist/products/severino/domain.js +20 -0
  81. package/dist/products/severino/domain.js.map +1 -0
  82. package/dist/products/severino/envfile.d.ts +20 -0
  83. package/dist/products/severino/envfile.js +89 -0
  84. package/dist/products/severino/envfile.js.map +1 -0
  85. package/dist/products/severino/github.d.ts +77 -0
  86. package/dist/products/severino/github.js +195 -0
  87. package/dist/products/severino/github.js.map +1 -0
  88. package/dist/products/severino/hostinger.d.ts +140 -0
  89. package/dist/products/severino/hostinger.js +607 -0
  90. package/dist/products/severino/hostinger.js.map +1 -0
  91. package/dist/products/severino/i18n.d.ts +887 -0
  92. package/dist/products/severino/i18n.js +587 -0
  93. package/dist/products/severino/i18n.js.map +1 -0
  94. package/dist/products/severino/keys.d.ts +5 -0
  95. package/dist/products/severino/keys.js +16 -0
  96. package/dist/products/severino/keys.js.map +1 -0
  97. package/dist/products/severino/main.d.ts +3 -0
  98. package/dist/products/severino/main.js +78 -0
  99. package/dist/products/severino/main.js.map +1 -0
  100. package/dist/products/severino/recovery.d.ts +50 -0
  101. package/dist/products/severino/recovery.js +96 -0
  102. package/dist/products/severino/recovery.js.map +1 -0
  103. package/dist/products/severino/sealed-box.d.ts +1 -0
  104. package/dist/products/severino/sealed-box.js +11 -0
  105. package/dist/products/severino/sealed-box.js.map +1 -0
  106. package/dist/products/severino/server.d.ts +123 -0
  107. package/dist/products/severino/server.js +274 -0
  108. package/dist/products/severino/server.js.map +1 -0
  109. package/dist/products/severino/service.d.ts +28 -0
  110. package/dist/products/severino/service.js +752 -0
  111. package/dist/products/severino/service.js.map +1 -0
  112. package/dist/products/severino/slack-manifest.d.ts +3 -0
  113. package/dist/products/severino/slack-manifest.js +45 -0
  114. package/dist/products/severino/slack-manifest.js.map +1 -0
  115. package/dist/products/severino/ssh.d.ts +35 -0
  116. package/dist/products/severino/ssh.js +140 -0
  117. package/dist/products/severino/ssh.js.map +1 -0
  118. package/dist/products/severino/state.d.ts +26 -0
  119. package/dist/products/severino/state.js +61 -0
  120. package/dist/products/severino/state.js.map +1 -0
  121. package/dist/products/severino/ui/assets/index-7S8Hl4jq.js +96 -0
  122. package/dist/products/severino/ui/assets/index-D5kql1Bh.css +2 -0
  123. package/dist/products/severino/ui/index.html +15 -0
  124. package/dist/products/severino/verify.d.ts +22 -0
  125. package/dist/products/severino/verify.js +149 -0
  126. package/dist/products/severino/verify.js.map +1 -0
  127. 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("&", "&amp;")
18
+ .replaceAll("<", "&lt;")
19
+ .replaceAll(">", "&gt;")
20
+ .replaceAll('"', "&quot;")
21
+ .replaceAll("'", "&#39;");
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