openplexer 0.1.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/LICENSE +21 -0
- package/dist/acp-client.d.ts +13 -0
- package/dist/acp-client.d.ts.map +1 -0
- package/dist/acp-client.js +88 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +308 -0
- package/dist/config.d.ts +34 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +26 -0
- package/dist/env.d.ts +6 -0
- package/dist/env.d.ts.map +1 -0
- package/dist/env.js +4 -0
- package/dist/git.d.ts +14 -0
- package/dist/git.d.ts.map +1 -0
- package/dist/git.js +60 -0
- package/dist/lock.d.ts +9 -0
- package/dist/lock.d.ts.map +1 -0
- package/dist/lock.js +55 -0
- package/dist/notion.d.ts +59 -0
- package/dist/notion.d.ts.map +1 -0
- package/dist/notion.js +154 -0
- package/dist/startup-service.d.ts +9 -0
- package/dist/startup-service.d.ts.map +1 -0
- package/dist/startup-service.js +150 -0
- package/dist/sync.d.ts +7 -0
- package/dist/sync.d.ts.map +1 -0
- package/dist/sync.js +139 -0
- package/dist/worker.d.ts +6 -0
- package/dist/worker.d.ts.map +1 -0
- package/dist/worker.js +151 -0
- package/package.json +66 -0
- package/src/acp-client.ts +124 -0
- package/src/cli.ts +387 -0
- package/src/config.ts +63 -0
- package/src/env.ts +9 -0
- package/src/git.ts +84 -0
- package/src/lock.ts +65 -0
- package/src/notion.ts +233 -0
- package/src/startup-service.ts +175 -0
- package/src/sync.ts +193 -0
- package/src/worker.ts +187 -0
package/src/worker.ts
ADDED
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
// Cloudflare Worker entrypoint for openplexer.
|
|
2
|
+
// Handles Notion OAuth flow: redirects user to Notion, receives callback
|
|
3
|
+
// with auth code, exchanges it for tokens, and stores result in KV for
|
|
4
|
+
// the CLI to poll. Same pattern as kimaki's gateway onboarding.
|
|
5
|
+
|
|
6
|
+
import { Spiceflow } from 'spiceflow'
|
|
7
|
+
import type { Env } from './env.ts'
|
|
8
|
+
|
|
9
|
+
const REDIRECT_PATH = '/auth/callback'
|
|
10
|
+
|
|
11
|
+
const app = new Spiceflow()
|
|
12
|
+
.state('env', {} as Env)
|
|
13
|
+
|
|
14
|
+
.onError(({ error }) => {
|
|
15
|
+
console.error(error)
|
|
16
|
+
const message = error instanceof Error ? error.message : String(error)
|
|
17
|
+
return new Response(message, { status: 500 })
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
.route({
|
|
21
|
+
method: 'GET',
|
|
22
|
+
path: '/',
|
|
23
|
+
handler() {
|
|
24
|
+
return new Response(null, {
|
|
25
|
+
status: 302,
|
|
26
|
+
headers: { Location: 'https://github.com/remorses/kimaki/tree/main/openplexer' },
|
|
27
|
+
})
|
|
28
|
+
},
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
.route({
|
|
32
|
+
method: 'GET',
|
|
33
|
+
path: '/health',
|
|
34
|
+
handler() {
|
|
35
|
+
return { status: 'ok' }
|
|
36
|
+
},
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
// Step 1: CLI opens browser to this URL. We redirect to Notion OAuth.
|
|
40
|
+
// The CLI passes a random `state` param to correlate the callback.
|
|
41
|
+
.route({
|
|
42
|
+
method: 'GET',
|
|
43
|
+
path: '/auth/notion',
|
|
44
|
+
handler({ request, state }) {
|
|
45
|
+
const url = new URL(request.url)
|
|
46
|
+
const stateParam = url.searchParams.get('state')
|
|
47
|
+
if (!stateParam) {
|
|
48
|
+
return new Response('Missing state parameter', { status: 400 })
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const env = state.env
|
|
52
|
+
const notionAuthUrl = new URL('https://api.notion.com/v1/oauth/authorize')
|
|
53
|
+
notionAuthUrl.searchParams.set('client_id', env.NOTION_CLIENT_ID)
|
|
54
|
+
notionAuthUrl.searchParams.set('response_type', 'code')
|
|
55
|
+
notionAuthUrl.searchParams.set('owner', 'user')
|
|
56
|
+
notionAuthUrl.searchParams.set(
|
|
57
|
+
'redirect_uri',
|
|
58
|
+
new URL(REDIRECT_PATH, url.origin).toString(),
|
|
59
|
+
)
|
|
60
|
+
notionAuthUrl.searchParams.set('state', stateParam)
|
|
61
|
+
|
|
62
|
+
return new Response(null, {
|
|
63
|
+
status: 302,
|
|
64
|
+
headers: { Location: notionAuthUrl.toString() },
|
|
65
|
+
})
|
|
66
|
+
},
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
// Step 2: Notion redirects here after user authorizes.
|
|
70
|
+
// We exchange the code for tokens and store in KV.
|
|
71
|
+
.route({
|
|
72
|
+
method: 'GET',
|
|
73
|
+
path: REDIRECT_PATH,
|
|
74
|
+
async handler({ request, state }) {
|
|
75
|
+
const url = new URL(request.url)
|
|
76
|
+
const code = url.searchParams.get('code')
|
|
77
|
+
const stateParam = url.searchParams.get('state')
|
|
78
|
+
|
|
79
|
+
if (!code || !stateParam) {
|
|
80
|
+
return new Response('Missing code or state parameter', { status: 400 })
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const env = state.env
|
|
84
|
+
const redirectUri = new URL(REDIRECT_PATH, url.origin).toString()
|
|
85
|
+
|
|
86
|
+
// Exchange code for tokens
|
|
87
|
+
const encoded = btoa(`${env.NOTION_CLIENT_ID}:${env.NOTION_CLIENT_SECRET}`)
|
|
88
|
+
const tokenResponse = await fetch('https://api.notion.com/v1/oauth/token', {
|
|
89
|
+
method: 'POST',
|
|
90
|
+
headers: {
|
|
91
|
+
Accept: 'application/json',
|
|
92
|
+
'Content-Type': 'application/json',
|
|
93
|
+
Authorization: `Basic ${encoded}`,
|
|
94
|
+
},
|
|
95
|
+
body: JSON.stringify({
|
|
96
|
+
grant_type: 'authorization_code',
|
|
97
|
+
code,
|
|
98
|
+
redirect_uri: redirectUri,
|
|
99
|
+
}),
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
if (!tokenResponse.ok) {
|
|
103
|
+
const errorBody = await tokenResponse.text()
|
|
104
|
+
console.error('Notion token exchange failed:', errorBody)
|
|
105
|
+
return new Response(`Notion authorization failed: ${errorBody}`, { status: 500 })
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const tokenData = (await tokenResponse.json()) as {
|
|
109
|
+
access_token: string
|
|
110
|
+
token_type: string
|
|
111
|
+
bot_id: string
|
|
112
|
+
workspace_id: string
|
|
113
|
+
workspace_name: string
|
|
114
|
+
owner: { type: string; user?: { id: string; name: string } }
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Store tokens in KV with 5 minute TTL
|
|
118
|
+
const kvPayload = {
|
|
119
|
+
accessToken: tokenData.access_token,
|
|
120
|
+
botId: tokenData.bot_id,
|
|
121
|
+
workspaceId: tokenData.workspace_id,
|
|
122
|
+
workspaceName: tokenData.workspace_name,
|
|
123
|
+
notionUserId: tokenData.owner?.user?.id,
|
|
124
|
+
notionUserName: tokenData.owner?.user?.name,
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
await env.OPENPLEXER_KV.put(`auth:${stateParam}`, JSON.stringify(kvPayload), {
|
|
128
|
+
expirationTtl: 300,
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
// Show success page
|
|
132
|
+
return new Response(
|
|
133
|
+
`<!DOCTYPE html>
|
|
134
|
+
<html>
|
|
135
|
+
<head><title>openplexer - Connected</title>
|
|
136
|
+
<style>
|
|
137
|
+
body { font-family: system-ui, sans-serif; display: flex; justify-content: center;
|
|
138
|
+
align-items: center; height: 100vh; margin: 0; background: #1a1a2e; color: #eee; }
|
|
139
|
+
.card { text-align: center; padding: 48px; border-radius: 12px;
|
|
140
|
+
background: #16213e; max-width: 400px; }
|
|
141
|
+
h1 { margin: 0 0 16px; font-size: 24px; }
|
|
142
|
+
p { color: #999; margin: 0; }
|
|
143
|
+
</style>
|
|
144
|
+
</head>
|
|
145
|
+
<body>
|
|
146
|
+
<div class="card">
|
|
147
|
+
<h1>Connected to Notion</h1>
|
|
148
|
+
<p>You can close this tab and return to the CLI.</p>
|
|
149
|
+
</div>
|
|
150
|
+
</body>
|
|
151
|
+
</html>`,
|
|
152
|
+
{ status: 200, headers: { 'Content-Type': 'text/html' } },
|
|
153
|
+
)
|
|
154
|
+
},
|
|
155
|
+
})
|
|
156
|
+
|
|
157
|
+
// Step 3: CLI polls this endpoint to get the tokens.
|
|
158
|
+
.route({
|
|
159
|
+
method: 'GET',
|
|
160
|
+
path: '/auth/status',
|
|
161
|
+
async handler({ request, state }) {
|
|
162
|
+
const url = new URL(request.url)
|
|
163
|
+
const stateParam = url.searchParams.get('state')
|
|
164
|
+
if (!stateParam) {
|
|
165
|
+
return new Response('Missing state parameter', { status: 400 })
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const result = await state.env.OPENPLEXER_KV.get(`auth:${stateParam}`)
|
|
169
|
+
if (!result) {
|
|
170
|
+
return new Response(JSON.stringify({ status: 'pending' }), {
|
|
171
|
+
status: 404,
|
|
172
|
+
headers: { 'Content-Type': 'application/json' },
|
|
173
|
+
})
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return new Response(result, {
|
|
177
|
+
status: 200,
|
|
178
|
+
headers: { 'Content-Type': 'application/json' },
|
|
179
|
+
})
|
|
180
|
+
},
|
|
181
|
+
})
|
|
182
|
+
|
|
183
|
+
export default {
|
|
184
|
+
fetch(request: Request, env: Env) {
|
|
185
|
+
return app.handle(request, { state: { env } })
|
|
186
|
+
},
|
|
187
|
+
}
|