@coswic/aipay 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/README.md +55 -0
- package/dist/agent-guide.d.ts +18 -0
- package/dist/agent-guide.js +60 -0
- package/dist/args.d.ts +15 -0
- package/dist/args.js +47 -0
- package/dist/auth-store.d.ts +23 -0
- package/dist/auth-store.js +113 -0
- package/dist/bin.d.ts +2 -0
- package/dist/bin.js +8 -0
- package/dist/browser.d.ts +1 -0
- package/dist/browser.js +18 -0
- package/dist/commands/apps.d.ts +15 -0
- package/dist/commands/apps.js +53 -0
- package/dist/commands/doctor.d.ts +17 -0
- package/dist/commands/doctor.js +153 -0
- package/dist/commands/init.d.ts +18 -0
- package/dist/commands/init.js +252 -0
- package/dist/commands/login.d.ts +25 -0
- package/dist/commands/login.js +149 -0
- package/dist/commands/session.d.ts +6 -0
- package/dist/commands/session.js +63 -0
- package/dist/commands/status.d.ts +3 -0
- package/dist/commands/status.js +24 -0
- package/dist/commands/test.d.ts +12 -0
- package/dist/commands/test.js +159 -0
- package/dist/commands/unlink.d.ts +5 -0
- package/dist/commands/unlink.js +70 -0
- package/dist/detect.d.ts +27 -0
- package/dist/detect.js +79 -0
- package/dist/developer-api.d.ts +14 -0
- package/dist/developer-api.js +74 -0
- package/dist/gate.d.ts +13 -0
- package/dist/gate.js +29 -0
- package/dist/index.d.ts +22 -0
- package/dist/index.js +22 -0
- package/dist/loopback.d.ts +37 -0
- package/dist/loopback.js +95 -0
- package/dist/main.d.ts +5 -0
- package/dist/main.js +90 -0
- package/dist/output.d.ts +36 -0
- package/dist/output.js +52 -0
- package/dist/project.d.ts +50 -0
- package/dist/project.js +107 -0
- package/dist/remote.d.ts +21 -0
- package/dist/remote.js +41 -0
- package/dist/setup-events.d.ts +3 -0
- package/dist/setup-events.js +28 -0
- package/dist/templates.d.ts +24 -0
- package/dist/templates.js +226 -0
- package/dist/version.d.ts +1 -0
- package/dist/version.js +10 -0
- package/package.json +46 -0
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
export function startPathFor(callbackPath) {
|
|
2
|
+
return callbackPath.replace(/\/[^/]*$/, '/start');
|
|
3
|
+
}
|
|
4
|
+
export function scaffoldFiles(input) {
|
|
5
|
+
const ext = input.typescript ? 'ts' : 'js';
|
|
6
|
+
const startPath = startPathFor(input.callbackPath);
|
|
7
|
+
const core = {
|
|
8
|
+
path: `${prefix(input.srcDir)}aipay.${ext}`,
|
|
9
|
+
content: coreModule(input.typescript),
|
|
10
|
+
};
|
|
11
|
+
switch (input.framework) {
|
|
12
|
+
case 'next':
|
|
13
|
+
return [
|
|
14
|
+
core,
|
|
15
|
+
{ path: `app${startPath}/route.${ext}`, content: nextStart(input) },
|
|
16
|
+
{ path: `app${input.callbackPath}/route.${ext}`, content: nextCallback(input) },
|
|
17
|
+
];
|
|
18
|
+
case 'express':
|
|
19
|
+
return [
|
|
20
|
+
core,
|
|
21
|
+
{
|
|
22
|
+
path: `${prefix(input.srcDir)}aipay-routes.${ext}`,
|
|
23
|
+
content: expressRoutes(input, startPath),
|
|
24
|
+
},
|
|
25
|
+
];
|
|
26
|
+
case 'fastify':
|
|
27
|
+
return [
|
|
28
|
+
core,
|
|
29
|
+
{
|
|
30
|
+
path: `${prefix(input.srcDir)}aipay-routes.${ext}`,
|
|
31
|
+
content: fastifyRoutes(input, startPath),
|
|
32
|
+
},
|
|
33
|
+
];
|
|
34
|
+
case 'hono':
|
|
35
|
+
return [
|
|
36
|
+
core,
|
|
37
|
+
{
|
|
38
|
+
path: `${prefix(input.srcDir)}aipay-routes.${ext}`,
|
|
39
|
+
content: honoRoutes(input, startPath),
|
|
40
|
+
},
|
|
41
|
+
];
|
|
42
|
+
default:
|
|
43
|
+
return [
|
|
44
|
+
core,
|
|
45
|
+
{
|
|
46
|
+
path: `${prefix(input.srcDir)}aipay-routes.${ext}`,
|
|
47
|
+
content: nodeRoutes(input, startPath),
|
|
48
|
+
},
|
|
49
|
+
];
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
function prefix(srcDir) {
|
|
53
|
+
return srcDir === '.' ? '' : `${srcDir.replace(/\/$/, '')}/`;
|
|
54
|
+
}
|
|
55
|
+
const ENV_HEADER = `// Reads the public values \`aipay init\` wrote to .env.local (load them with your usual
|
|
56
|
+
// dotenv/framework mechanism). No secret exists: the app is a public PKCE client.`;
|
|
57
|
+
function coreModule(ts) {
|
|
58
|
+
const t = (typed, plain) => (ts ? typed : plain);
|
|
59
|
+
return `${ENV_HEADER}
|
|
60
|
+
import {
|
|
61
|
+
AIPayClient,
|
|
62
|
+
createConnectUrl,
|
|
63
|
+
getUserInfo,
|
|
64
|
+
handleCallback,${t('\n type TokenSet,', '')}
|
|
65
|
+
} from '@coswic/aipay-sdk'
|
|
66
|
+
|
|
67
|
+
const cfg = {
|
|
68
|
+
hydraPublicUrl: process.env.AIPAY_AUTH_ORIGIN${t(' as string', '')},
|
|
69
|
+
clientId: process.env.AIPAY_CLIENT_ID${t(' as string', '')},
|
|
70
|
+
redirectUri: process.env.AIPAY_CALLBACK_URL${t(' as string', '')},
|
|
71
|
+
}
|
|
72
|
+
const apiBase = process.env.AIPAY_API_BASE${t(' as string', '')}
|
|
73
|
+
|
|
74
|
+
// DEV ONLY: in-memory stores. Replace both with your session store before shipping.
|
|
75
|
+
const pendingFlows = new Map${t('<string, { codeVerifier: string; expires: number }>', '')}()
|
|
76
|
+
const sessions = new Map${t('<string, TokenSet>', '')}()
|
|
77
|
+
|
|
78
|
+
/** Step 1 — build the AI-Pay sign-in URL; remember the PKCE verifier under the state. */
|
|
79
|
+
export async function beginSignIn()${t(': Promise<string>', '')} {
|
|
80
|
+
const { url, state, codeVerifier } = await createConnectUrl(cfg)
|
|
81
|
+
pendingFlows.set(state, { codeVerifier, expires: Date.now() + 10 * 60_000 })
|
|
82
|
+
return url
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Step 2 — on the callback, exchange the code (state + PKCE are verified by the SDK). */
|
|
86
|
+
export async function finishSignIn(callbackUrl${t(': string', '')})${t(': Promise<{ sessionId: string; sub: string; email: string | null }>', '')} {
|
|
87
|
+
const state = new URL(callbackUrl).searchParams.get('state') ?? ''
|
|
88
|
+
const pending = pendingFlows.get(state)
|
|
89
|
+
pendingFlows.delete(state)
|
|
90
|
+
const saved = pending && pending.expires > Date.now() ? { state, codeVerifier: pending.codeVerifier } : null
|
|
91
|
+
const tokens = await handleCallback(cfg, callbackUrl, saved)
|
|
92
|
+
const user = await getUserInfo(cfg, tokens.accessToken)
|
|
93
|
+
const sessionId = crypto.randomUUID()
|
|
94
|
+
sessions.set(sessionId, tokens)
|
|
95
|
+
// user.sub is the stable per-app id of this user — key your accounts on it, not on email.
|
|
96
|
+
return { sessionId, sub: user.sub, email: user.email ?? null }
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** Step 3 — call a model on the user's behalf; the response carries the billing receipt. */
|
|
100
|
+
export async function firstCompletion(sessionId${t(': string', '')}, model${t(': string', '')}) {
|
|
101
|
+
const tokens = sessions.get(sessionId)
|
|
102
|
+
if (!tokens) throw new Error('not signed in')
|
|
103
|
+
const client = new AIPayClient({ baseUrl: apiBase, accessToken: tokens.accessToken })
|
|
104
|
+
const res = await client.chat.completions.create({
|
|
105
|
+
model,
|
|
106
|
+
messages: [{ role: 'user', content: 'Say hello in five words.' }],
|
|
107
|
+
max_tokens: 32,
|
|
108
|
+
})
|
|
109
|
+
return { text: res.choices[0]?.message.content ?? '', receipt: res.aipay }
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function sessionCookie(sessionId${t(': string', '')})${t(': string', '')} {
|
|
113
|
+
return \`aipay_session=\${sessionId}; Path=/; HttpOnly; SameSite=Lax\`
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function sessionFromCookie(cookieHeader${t(': string | null | undefined', '')})${t(': string | null', '')} {
|
|
117
|
+
const m = /(?:^|;\\s*)aipay_session=([^;]+)/.exec(cookieHeader ?? '')
|
|
118
|
+
return m ? m[1]${t(' ?? null', '')} : null
|
|
119
|
+
}
|
|
120
|
+
`;
|
|
121
|
+
}
|
|
122
|
+
function nextStart(input) {
|
|
123
|
+
const imp = importPath(input, 'app/x/y/z'); // depth: app/<a>/<b>/<start>/route
|
|
124
|
+
return `import { beginSignIn } from '${imp}'
|
|
125
|
+
|
|
126
|
+
export async function GET() {
|
|
127
|
+
const url = await beginSignIn()
|
|
128
|
+
return Response.redirect(url, 302)
|
|
129
|
+
}
|
|
130
|
+
`;
|
|
131
|
+
}
|
|
132
|
+
function nextCallback(input) {
|
|
133
|
+
const imp = importPath(input, 'app/x/y/z');
|
|
134
|
+
return `import { finishSignIn, sessionCookie } from '${imp}'
|
|
135
|
+
|
|
136
|
+
export async function GET(request${input.typescript ? ': Request' : ''}) {
|
|
137
|
+
const { sessionId, email } = await finishSignIn(request.url)
|
|
138
|
+
return new Response(\`Signed in as \${email ?? 'user'}. You can now call firstCompletion(sessionId, model).\`, {
|
|
139
|
+
status: 200,
|
|
140
|
+
headers: { 'set-cookie': sessionCookie(sessionId), 'content-type': 'text/plain; charset=utf-8' },
|
|
141
|
+
})
|
|
142
|
+
}
|
|
143
|
+
`;
|
|
144
|
+
}
|
|
145
|
+
/** Relative import from a route file to the core module (depth-aware, extension-less). */
|
|
146
|
+
function importPath(input, routeDir) {
|
|
147
|
+
const depth = routeDir.split('/').length;
|
|
148
|
+
const up = '../'.repeat(depth);
|
|
149
|
+
const core = `${prefix(input.srcDir)}aipay`;
|
|
150
|
+
return `${up}${core}${input.typescript ? '' : '.js'}`;
|
|
151
|
+
}
|
|
152
|
+
function expressRoutes(input, startPath) {
|
|
153
|
+
const ts = input.typescript;
|
|
154
|
+
return `import { Router } from 'express'
|
|
155
|
+
import { beginSignIn, finishSignIn, sessionCookie } from './aipay${ts ? '' : '.js'}'
|
|
156
|
+
|
|
157
|
+
export const aipayRouter = Router()
|
|
158
|
+
|
|
159
|
+
aipayRouter.get('${startPath}', async (_req, res) => {
|
|
160
|
+
res.redirect(await beginSignIn())
|
|
161
|
+
})
|
|
162
|
+
|
|
163
|
+
aipayRouter.get('${input.callbackPath}', async (req, res) => {
|
|
164
|
+
const full = new URL(req.originalUrl, \`\${req.protocol}://\${req.get('host')}\`).toString()
|
|
165
|
+
const { sessionId, email } = await finishSignIn(full)
|
|
166
|
+
res.setHeader('set-cookie', sessionCookie(sessionId))
|
|
167
|
+
res.type('text/plain').send(\`Signed in as \${email ?? 'user'}.\`)
|
|
168
|
+
})
|
|
169
|
+
// app.use(aipayRouter)
|
|
170
|
+
`;
|
|
171
|
+
}
|
|
172
|
+
function fastifyRoutes(input, startPath) {
|
|
173
|
+
const ts = input.typescript;
|
|
174
|
+
return `${ts ? "import type { FastifyInstance } from 'fastify'\n" : ''}import { beginSignIn, finishSignIn, sessionCookie } from './aipay${ts ? '' : '.js'}'
|
|
175
|
+
|
|
176
|
+
export async function aipayRoutes(app${ts ? ': FastifyInstance' : ''}) {
|
|
177
|
+
app.get('${startPath}', async (_req, reply) => reply.redirect(await beginSignIn()))
|
|
178
|
+
app.get('${input.callbackPath}', async (req, reply) => {
|
|
179
|
+
const full = new URL(req.url, \`\${req.protocol}://\${req.headers.host}\`).toString()
|
|
180
|
+
const { sessionId, email } = await finishSignIn(full)
|
|
181
|
+
reply.header('set-cookie', sessionCookie(sessionId))
|
|
182
|
+
return \`Signed in as \${email ?? 'user'}.\`
|
|
183
|
+
})
|
|
184
|
+
}
|
|
185
|
+
// await app.register(aipayRoutes)
|
|
186
|
+
`;
|
|
187
|
+
}
|
|
188
|
+
function honoRoutes(input, startPath) {
|
|
189
|
+
const ts = input.typescript;
|
|
190
|
+
return `import { Hono } from 'hono'
|
|
191
|
+
import { beginSignIn, finishSignIn, sessionCookie } from './aipay${ts ? '' : '.js'}'
|
|
192
|
+
|
|
193
|
+
export const aipay = new Hono()
|
|
194
|
+
|
|
195
|
+
aipay.get('${startPath}', async (c) => c.redirect(await beginSignIn()))
|
|
196
|
+
|
|
197
|
+
aipay.get('${input.callbackPath}', async (c) => {
|
|
198
|
+
const { sessionId, email } = await finishSignIn(c.req.url)
|
|
199
|
+
c.header('set-cookie', sessionCookie(sessionId))
|
|
200
|
+
return c.text(\`Signed in as \${email ?? 'user'}.\`)
|
|
201
|
+
})
|
|
202
|
+
// app.route('/', aipay)
|
|
203
|
+
`;
|
|
204
|
+
}
|
|
205
|
+
function nodeRoutes(input, startPath) {
|
|
206
|
+
const ts = input.typescript;
|
|
207
|
+
return `${ts ? "import type { IncomingMessage, ServerResponse } from 'node:http'\n" : ''}import { beginSignIn, finishSignIn, sessionCookie } from './aipay${ts ? '' : '.js'}'
|
|
208
|
+
|
|
209
|
+
/** Plug into your http server: \`if (await handleAIPay(req, res)) return\` */
|
|
210
|
+
export async function handleAIPay(req${ts ? ': IncomingMessage' : ''}, res${ts ? ': ServerResponse' : ''})${ts ? ': Promise<boolean>' : ''} {
|
|
211
|
+
const url = new URL(req.url ?? '/', \`http://\${req.headers.host}\`)
|
|
212
|
+
if (url.pathname === '${startPath}') {
|
|
213
|
+
res.writeHead(302, { location: await beginSignIn() })
|
|
214
|
+
res.end()
|
|
215
|
+
return true
|
|
216
|
+
}
|
|
217
|
+
if (url.pathname === '${input.callbackPath}') {
|
|
218
|
+
const { sessionId, email } = await finishSignIn(url.toString())
|
|
219
|
+
res.writeHead(200, { 'set-cookie': sessionCookie(sessionId), 'content-type': 'text/plain; charset=utf-8' })
|
|
220
|
+
res.end(\`Signed in as \${email ?? 'user'}.\`)
|
|
221
|
+
return true
|
|
222
|
+
}
|
|
223
|
+
return false
|
|
224
|
+
}
|
|
225
|
+
`;
|
|
226
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function cliVersion(): string;
|
package/dist/version.js
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@coswic/aipay",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "AI-Pay CLI — init (link an app + scaffold the integration), doctor, test (real sign-in + first receipt), status, unlink. For developers and their coding agents.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"bin": {
|
|
8
|
+
"aipay": "./dist/bin.js"
|
|
9
|
+
},
|
|
10
|
+
"exports": {
|
|
11
|
+
".": {
|
|
12
|
+
"types": "./dist/index.d.ts",
|
|
13
|
+
"import": "./dist/index.js"
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
"files": [
|
|
17
|
+
"dist",
|
|
18
|
+
"README.md"
|
|
19
|
+
],
|
|
20
|
+
"publishConfig": {
|
|
21
|
+
"access": "public"
|
|
22
|
+
},
|
|
23
|
+
"engines": {
|
|
24
|
+
"node": ">=20"
|
|
25
|
+
},
|
|
26
|
+
"dependencies": {
|
|
27
|
+
"@coswic/aipay-sdk": "^0.3.1"
|
|
28
|
+
},
|
|
29
|
+
"devDependencies": {
|
|
30
|
+
"@types/node": "^26.1.1",
|
|
31
|
+
"typescript": "^7.0.2",
|
|
32
|
+
"vitest": "^4.1.10"
|
|
33
|
+
},
|
|
34
|
+
"keywords": [
|
|
35
|
+
"aipay",
|
|
36
|
+
"cli",
|
|
37
|
+
"oauth",
|
|
38
|
+
"pkce",
|
|
39
|
+
"coding-agent"
|
|
40
|
+
],
|
|
41
|
+
"scripts": {
|
|
42
|
+
"build": "tsc -p tsconfig.build.json",
|
|
43
|
+
"typecheck": "tsc --noEmit",
|
|
44
|
+
"test": "vitest run"
|
|
45
|
+
}
|
|
46
|
+
}
|