@pikku/core 0.12.63 → 0.12.66
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 +368 -0
- package/dist/dev/hot-reload.js +13 -55
- package/dist/dev/module-runner.d.ts +14 -0
- package/dist/dev/module-runner.js +59 -0
- package/dist/dev/reload-meta.d.ts +13 -0
- package/dist/dev/reload-meta.js +22 -0
- package/dist/errors/errors.d.ts +12 -0
- package/dist/errors/errors.js +19 -0
- package/dist/function/function-runner.d.ts +5 -5
- package/dist/function/function-runner.js +39 -111
- package/dist/function/functions.types.d.ts +17 -7
- package/dist/index.d.ts +5 -1
- package/dist/index.js +3 -1
- package/dist/middleware/auth-apikey.d.ts +1 -0
- package/dist/middleware/auth-bearer.d.ts +1 -0
- package/dist/middleware/auth-cookie.d.ts +1 -0
- package/dist/middleware/cors.d.ts +1 -0
- package/dist/middleware/index.d.ts +1 -1
- package/dist/middleware/index.js +1 -1
- package/dist/middleware/remote-auth.d.ts +1 -0
- package/dist/middleware/telemetry.d.ts +2 -0
- package/dist/middleware/timeout.d.ts +1 -0
- package/dist/permissions.d.ts +42 -27
- package/dist/permissions.js +101 -203
- package/dist/pikku-state.js +0 -2
- package/dist/scopes.d.ts +17 -0
- package/dist/scopes.js +59 -0
- package/dist/services/ai-embedding-service.d.ts +31 -0
- package/dist/services/ai-embedding-service.js +1 -0
- package/dist/services/credential-wire-service.d.ts +3 -1
- package/dist/services/credential-wire-service.js +10 -4
- package/dist/services/index.d.ts +4 -0
- package/dist/services/index.js +2 -0
- package/dist/services/meta-service.d.ts +15 -2
- package/dist/services/meta-service.js +51 -1
- package/dist/services/queue-webhook-service.d.ts +55 -0
- package/dist/services/queue-webhook-service.js +136 -0
- package/dist/services/scope-service.d.ts +67 -0
- package/dist/services/scope-service.js +1 -0
- package/dist/services/typed-secret-service.d.ts +8 -0
- package/dist/services/typed-secret-service.js +39 -4
- package/dist/services/webhook-service.d.ts +140 -0
- package/dist/services/webhook-service.js +44 -0
- package/dist/services/workflow-service.d.ts +7 -0
- package/dist/testing/service-tests.js +37 -0
- package/dist/types/core.types.d.ts +34 -6
- package/dist/types/state.types.d.ts +20 -2
- package/dist/utils/hmac.d.ts +16 -0
- package/dist/utils/hmac.js +26 -0
- package/dist/utils/safe-fetch.d.ts +51 -0
- package/dist/utils/safe-fetch.js +192 -0
- package/dist/wirings/ai-agent/ai-agent-agui.d.ts +84 -0
- package/dist/wirings/ai-agent/ai-agent-agui.js +305 -0
- package/dist/wirings/ai-agent/ai-agent-prepare.d.ts +105 -1
- package/dist/wirings/ai-agent/ai-agent-prepare.js +249 -11
- package/dist/wirings/ai-agent/ai-agent-runner.js +10 -1
- package/dist/wirings/ai-agent/ai-agent-stream.js +88 -16
- package/dist/wirings/ai-agent/ai-agent.types.d.ts +56 -2
- package/dist/wirings/ai-agent/index.d.ts +2 -1
- package/dist/wirings/ai-agent/index.js +2 -1
- package/dist/wirings/ai-agent/voice-input.d.ts +1 -0
- package/dist/wirings/ai-agent/voice-input.js +5 -41
- package/dist/wirings/ai-agent/voice-output.d.ts +1 -0
- package/dist/wirings/channel/channel-common.js +0 -1
- package/dist/wirings/channel/channel-handler.js +1 -4
- package/dist/wirings/channel/channel-middleware-runner.d.ts +12 -0
- package/dist/wirings/channel/channel-middleware-runner.js +34 -19
- package/dist/wirings/channel/channel.types.d.ts +2 -6
- package/dist/wirings/cli/cli-runner.js +3 -2
- package/dist/wirings/cli/cli.types.d.ts +1 -2
- package/dist/wirings/cli/command-parser.d.ts +2 -0
- package/dist/wirings/cli/command-parser.js +59 -2
- package/dist/wirings/credential/credential.types.d.ts +14 -0
- package/dist/wirings/credential/validate-credential-definitions.js +1 -0
- package/dist/wirings/gateway/gateway-runner.js +100 -50
- package/dist/wirings/gateway/gateway.types.d.ts +8 -5
- package/dist/wirings/http/http-routes.js +0 -3
- package/dist/wirings/http/http-runner.d.ts +9 -29
- package/dist/wirings/http/http-runner.js +9 -34
- package/dist/wirings/http/http.types.d.ts +4 -12
- package/dist/wirings/mcp/mcp-runner.js +0 -2
- package/dist/wirings/mcp/mcp.types.d.ts +5 -11
- package/dist/wirings/oauth2/index.d.ts +0 -3
- package/dist/wirings/oauth2/index.js +1 -2
- package/dist/wirings/rpc/addon-runner.d.ts +28 -0
- package/dist/wirings/rpc/addon-runner.js +173 -0
- package/dist/wirings/rpc/index.d.ts +5 -1
- package/dist/wirings/rpc/index.js +3 -1
- package/dist/wirings/rpc/remote-addon-auth.d.ts +26 -0
- package/dist/wirings/rpc/remote-addon-auth.js +43 -0
- package/dist/wirings/rpc/rpc-runner.d.ts +19 -0
- package/dist/wirings/rpc/rpc-runner.js +109 -3
- package/dist/wirings/rpc/rpc-types.d.ts +15 -1
- package/dist/wirings/rpc/wire-addon.js +9 -0
- package/dist/wirings/rpc/wire-remote-addon.d.ts +47 -0
- package/dist/wirings/rpc/wire-remote-addon.js +19 -0
- package/dist/wirings/scope/index.d.ts +3 -0
- package/dist/wirings/scope/index.js +2 -0
- package/dist/wirings/scope/scope.types.d.ts +39 -0
- package/dist/wirings/scope/scope.types.js +1 -0
- package/dist/wirings/scope/validate-scope-definitions.d.ts +16 -0
- package/dist/wirings/scope/validate-scope-definitions.js +76 -0
- package/dist/wirings/scope/wire-scope.d.ts +33 -0
- package/dist/wirings/scope/wire-scope.js +32 -0
- package/dist/wirings/secret/secret.types.d.ts +14 -0
- package/dist/wirings/secret/validate-secret-definitions.js +2 -0
- package/dist/wirings/variable/validate-variable-definitions.js +2 -0
- package/dist/wirings/variable/variable.types.d.ts +14 -0
- package/dist/wirings/workflow/dsl/index.d.ts +1 -1
- package/dist/wirings/workflow/dsl/workflow-dsl.types.d.ts +89 -7
- package/dist/wirings/workflow/graph/graph-node.d.ts +2 -0
- package/dist/wirings/workflow/graph/graph-runner.js +3 -0
- package/dist/wirings/workflow/graph/wire-workflow-graph.d.ts +6 -0
- package/dist/wirings/workflow/graph/wire-workflow-graph.js +1 -0
- package/dist/wirings/workflow/graph/workflow-graph.types.d.ts +5 -0
- package/dist/wirings/workflow/index.d.ts +2 -2
- package/dist/wirings/workflow/index.js +1 -1
- package/dist/wirings/workflow/pikku-workflow-service.d.ts +59 -1
- package/dist/wirings/workflow/pikku-workflow-service.js +187 -0
- package/dist/wirings/workflow/workflow.types.d.ts +1 -3
- package/package.json +6 -1
- package/run-tests.sh +1 -0
- package/src/dev/hot-reload.ts +13 -68
- package/src/dev/module-runner.test.ts +169 -0
- package/src/dev/module-runner.ts +103 -0
- package/src/dev/reload-meta.test.ts +31 -2
- package/src/dev/reload-meta.ts +26 -0
- package/src/errors/errors.ts +24 -0
- package/src/function/function-runner.test.ts +276 -142
- package/src/function/function-runner.ts +52 -145
- package/src/function/functions.types.ts +28 -38
- package/src/index.ts +8 -6
- package/src/middleware/index.ts +1 -5
- package/src/permissions.test.ts +160 -373
- package/src/permissions.ts +137 -279
- package/src/pikku-state.ts +0 -2
- package/src/scopes.test.ts +167 -0
- package/src/scopes.ts +69 -0
- package/src/services/ai-embedding-service.ts +31 -0
- package/src/services/credential-wire-service.ts +10 -4
- package/src/services/index.ts +20 -0
- package/src/services/meta-service.ts +68 -3
- package/src/services/queue-webhook-service.test.ts +408 -0
- package/src/services/queue-webhook-service.ts +182 -0
- package/src/services/scope-service.ts +75 -0
- package/src/services/typed-secret-service.test.ts +35 -0
- package/src/services/typed-secret-service.ts +39 -4
- package/src/services/webhook-service.ts +180 -0
- package/src/services/workflow-service.ts +7 -0
- package/src/testing/service-tests.ts +49 -0
- package/src/types/core.types.ts +42 -14
- package/src/types/state.types.ts +23 -3
- package/src/utils/hmac.ts +27 -0
- package/src/utils/safe-fetch.test.ts +373 -0
- package/src/utils/safe-fetch.ts +213 -0
- package/src/wirings/ai-agent/ai-agent-agui.test.ts +1127 -0
- package/src/wirings/ai-agent/ai-agent-agui.ts +386 -0
- package/src/wirings/ai-agent/ai-agent-authorization.test.ts +204 -0
- package/src/wirings/ai-agent/ai-agent-prepare.test.ts +483 -0
- package/src/wirings/ai-agent/ai-agent-prepare.ts +324 -13
- package/src/wirings/ai-agent/ai-agent-resume-authorization.test.ts +207 -0
- package/src/wirings/ai-agent/ai-agent-runner.test.ts +48 -1
- package/src/wirings/ai-agent/ai-agent-runner.ts +28 -0
- package/src/wirings/ai-agent/ai-agent-stream.test.ts +301 -10
- package/src/wirings/ai-agent/ai-agent-stream.ts +122 -16
- package/src/wirings/ai-agent/ai-agent.types.ts +57 -0
- package/src/wirings/ai-agent/index.ts +5 -0
- package/src/wirings/ai-agent/voice-input.test.ts +90 -0
- package/src/wirings/ai-agent/voice-input.ts +9 -40
- package/src/wirings/channel/channel-common.ts +0 -1
- package/src/wirings/channel/channel-handler.ts +1 -9
- package/src/wirings/channel/channel-middleware-runner.test.ts +28 -2
- package/src/wirings/channel/channel-middleware-runner.ts +39 -27
- package/src/wirings/channel/channel.types.ts +0 -6
- package/src/wirings/cli/cli-runner.ts +4 -2
- package/src/wirings/cli/cli.types.ts +0 -2
- package/src/wirings/cli/command-parser.test.ts +130 -0
- package/src/wirings/cli/command-parser.ts +80 -2
- package/src/wirings/credential/credential.types.ts +14 -0
- package/src/wirings/credential/validate-credential-definitions.ts +1 -0
- package/src/wirings/gateway/gateway-authorization.test.ts +444 -0
- package/src/wirings/gateway/gateway-runner.ts +114 -68
- package/src/wirings/gateway/gateway.types.ts +7 -9
- package/src/wirings/http/http-routes.ts +0 -3
- package/src/wirings/http/http-runner.test.ts +1 -11
- package/src/wirings/http/http-runner.ts +16 -39
- package/src/wirings/http/http.types.ts +7 -14
- package/src/wirings/mcp/mcp-runner.ts +0 -2
- package/src/wirings/mcp/mcp.types.ts +3 -11
- package/src/wirings/oauth2/index.ts +0 -3
- package/src/wirings/rpc/addon-runner.ts +233 -0
- package/src/wirings/rpc/index.ts +17 -1
- package/src/wirings/rpc/remote-addon-auth.ts +69 -0
- package/src/wirings/rpc/rpc-runner.test.ts +319 -16
- package/src/wirings/rpc/rpc-runner.ts +155 -3
- package/src/wirings/rpc/rpc-types.ts +16 -5
- package/src/wirings/rpc/wire-addon.test.ts +6 -3
- package/src/wirings/rpc/wire-addon.ts +9 -0
- package/src/wirings/rpc/wire-remote-addon.ts +57 -0
- package/src/wirings/scope/index.ts +14 -0
- package/src/wirings/scope/scope.test.ts +135 -0
- package/src/wirings/scope/scope.types.ts +44 -0
- package/src/wirings/scope/validate-scope-definitions.ts +110 -0
- package/src/wirings/scope/wire-scope.ts +34 -0
- package/src/wirings/secret/secret.types.ts +14 -0
- package/src/wirings/secret/validate-secret-definitions.ts +2 -0
- package/src/wirings/variable/validate-variable-definitions.ts +2 -0
- package/src/wirings/variable/variable.types.ts +14 -0
- package/src/wirings/workflow/dsl/index.ts +4 -0
- package/src/wirings/workflow/dsl/workflow-dsl.types.ts +97 -6
- package/src/wirings/workflow/graph/graph-node.ts +2 -0
- package/src/wirings/workflow/graph/graph-runner.test.ts +58 -0
- package/src/wirings/workflow/graph/graph-runner.ts +3 -0
- package/src/wirings/workflow/graph/wire-workflow-graph.ts +7 -0
- package/src/wirings/workflow/graph/workflow-graph.types.ts +5 -0
- package/src/wirings/workflow/index.ts +5 -0
- package/src/wirings/workflow/pikku-workflow-service.test.ts +341 -0
- package/src/wirings/workflow/pikku-workflow-service.ts +272 -0
- package/src/wirings/workflow/workflow-on-error.test.ts +154 -0
- package/src/wirings/workflow/workflow-step-session.test.ts +21 -5
- package/src/wirings/workflow/workflow.types.ts +4 -2
- package/tsconfig.tsbuildinfo +1 -1
- package/src/wirings/oauth2/oauth2-client.test.ts +0 -930
- package/src/wirings/oauth2/oauth2-client.ts +0 -331
- package/src/wirings/oauth2/oauth2-routes.ts +0 -234
|
@@ -1,331 +0,0 @@
|
|
|
1
|
-
import type { OAuth2Token, OAuth2AppCredential } from './oauth2.types.js'
|
|
2
|
-
import type { OAuth2CredentialConfig } from '../secret/secret.types.js'
|
|
3
|
-
import type { SecretService } from '../../services/secret-service.js'
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* OAuth2 client that acts as a service.
|
|
7
|
-
* Created once in createSingletonServices, handles token caching and refresh internally.
|
|
8
|
-
* Functions use it directly via oauth.request().
|
|
9
|
-
*
|
|
10
|
-
* @example
|
|
11
|
-
* ```typescript
|
|
12
|
-
* // In services.ts
|
|
13
|
-
* export const createSingletonServices = async (config, { secrets }) => {
|
|
14
|
-
* return {
|
|
15
|
-
* slackOAuth: new OAuth2Client(
|
|
16
|
-
* { tokenSecretId: 'SLACK_TOKENS', authorizationUrl: '...', tokenUrl: '...', scopes: [] },
|
|
17
|
-
* 'SLACK_APP_CREDS',
|
|
18
|
-
* secrets
|
|
19
|
-
* ),
|
|
20
|
-
* }
|
|
21
|
-
* }
|
|
22
|
-
*
|
|
23
|
-
* // In function
|
|
24
|
-
* export const postMessage = pikkuFunc(async ({ slackOAuth }, { channel, text }) => {
|
|
25
|
-
* const response = await slackOAuth.request('https://slack.com/api/chat.postMessage', {
|
|
26
|
-
* method: 'POST',
|
|
27
|
-
* body: JSON.stringify({ channel, text }),
|
|
28
|
-
* })
|
|
29
|
-
* return response.json()
|
|
30
|
-
* })
|
|
31
|
-
* ```
|
|
32
|
-
*/
|
|
33
|
-
const DEFAULT_TIMEOUT_MS = 30_000
|
|
34
|
-
const TOKEN_EXPIRY_BUFFER_MS = 60_000
|
|
35
|
-
|
|
36
|
-
/**
|
|
37
|
-
* Helper to fetch with a timeout using AbortController.
|
|
38
|
-
*/
|
|
39
|
-
async function fetchWithTimeout(
|
|
40
|
-
url: string,
|
|
41
|
-
options: RequestInit,
|
|
42
|
-
timeoutMs: number = DEFAULT_TIMEOUT_MS
|
|
43
|
-
): Promise<Response> {
|
|
44
|
-
const controller = new AbortController()
|
|
45
|
-
const timeoutId = setTimeout(() => controller.abort(), timeoutMs)
|
|
46
|
-
|
|
47
|
-
try {
|
|
48
|
-
return await fetch(url, {
|
|
49
|
-
...options,
|
|
50
|
-
signal: controller.signal,
|
|
51
|
-
})
|
|
52
|
-
} finally {
|
|
53
|
-
clearTimeout(timeoutId)
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
export class OAuth2Client {
|
|
58
|
-
private cachedToken: OAuth2Token | null = null
|
|
59
|
-
private cachedAppCredential: OAuth2AppCredential | null = null
|
|
60
|
-
private refreshPromise: Promise<OAuth2Token> | null = null
|
|
61
|
-
|
|
62
|
-
constructor(
|
|
63
|
-
private oauth2Config: OAuth2CredentialConfig,
|
|
64
|
-
private appCredentialSecretId: string,
|
|
65
|
-
private secrets: SecretService
|
|
66
|
-
) {}
|
|
67
|
-
|
|
68
|
-
/**
|
|
69
|
-
* Make an authenticated request. Handles token caching and 401 refresh.
|
|
70
|
-
*/
|
|
71
|
-
async request(
|
|
72
|
-
url: string,
|
|
73
|
-
options?: RequestInit,
|
|
74
|
-
timeoutMs: number = DEFAULT_TIMEOUT_MS
|
|
75
|
-
): Promise<Response> {
|
|
76
|
-
const token = await this.getAccessToken()
|
|
77
|
-
|
|
78
|
-
const response = await fetchWithTimeout(
|
|
79
|
-
url,
|
|
80
|
-
{
|
|
81
|
-
...options,
|
|
82
|
-
headers: {
|
|
83
|
-
...options?.headers,
|
|
84
|
-
Authorization: `Bearer ${token}`,
|
|
85
|
-
},
|
|
86
|
-
},
|
|
87
|
-
timeoutMs
|
|
88
|
-
)
|
|
89
|
-
|
|
90
|
-
// Auto-retry on 401 after refresh
|
|
91
|
-
if (response.status === 401) {
|
|
92
|
-
const newToken = await this.refreshAndGetToken()
|
|
93
|
-
return fetchWithTimeout(
|
|
94
|
-
url,
|
|
95
|
-
{
|
|
96
|
-
...options,
|
|
97
|
-
headers: {
|
|
98
|
-
...options?.headers,
|
|
99
|
-
Authorization: `Bearer ${newToken}`,
|
|
100
|
-
},
|
|
101
|
-
},
|
|
102
|
-
timeoutMs
|
|
103
|
-
)
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
return response
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
/**
|
|
110
|
-
* Get cached token or load from secrets.
|
|
111
|
-
*/
|
|
112
|
-
async getAccessToken(): Promise<string> {
|
|
113
|
-
// Return cached token if valid
|
|
114
|
-
if (this.cachedToken) {
|
|
115
|
-
if (this.isTokenValid(this.cachedToken)) {
|
|
116
|
-
return this.cachedToken.accessToken
|
|
117
|
-
}
|
|
118
|
-
// Token expired, try to refresh
|
|
119
|
-
if (this.cachedToken.refreshToken) {
|
|
120
|
-
return this.refreshAndGetToken()
|
|
121
|
-
}
|
|
122
|
-
// Token expired and no refresh token available
|
|
123
|
-
throw new Error('OAuth2 token expired and no refresh token available')
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
// Load from secrets
|
|
127
|
-
const token = await this.secrets.getSecret<OAuth2Token>(
|
|
128
|
-
this.oauth2Config.tokenSecretId
|
|
129
|
-
)
|
|
130
|
-
this.cachedToken = token
|
|
131
|
-
|
|
132
|
-
// Check if loaded token is expired and needs refresh
|
|
133
|
-
if (!this.isTokenValid(token)) {
|
|
134
|
-
if (token.refreshToken) {
|
|
135
|
-
return this.refreshAndGetToken()
|
|
136
|
-
}
|
|
137
|
-
// Token expired and no refresh token available
|
|
138
|
-
throw new Error('OAuth2 token expired and no refresh token available')
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
return token.accessToken
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
/**
|
|
145
|
-
* Refresh token and update cache.
|
|
146
|
-
* Uses a promise lock to prevent concurrent refresh attempts.
|
|
147
|
-
*/
|
|
148
|
-
private async refreshAndGetToken(): Promise<string> {
|
|
149
|
-
if (this.refreshPromise) {
|
|
150
|
-
const token = await this.refreshPromise
|
|
151
|
-
return token.accessToken
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
this.refreshPromise = this.doRefreshToken()
|
|
155
|
-
|
|
156
|
-
try {
|
|
157
|
-
const token = await this.refreshPromise
|
|
158
|
-
return token.accessToken
|
|
159
|
-
} finally {
|
|
160
|
-
this.refreshPromise = null
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
/**
|
|
165
|
-
* Perform the actual token refresh.
|
|
166
|
-
*/
|
|
167
|
-
private async doRefreshToken(): Promise<OAuth2Token> {
|
|
168
|
-
if (!this.cachedToken?.refreshToken) {
|
|
169
|
-
throw new Error('No refresh token available')
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
const appCredential = await this.getAppCredential()
|
|
173
|
-
|
|
174
|
-
const params = new URLSearchParams({
|
|
175
|
-
grant_type: 'refresh_token',
|
|
176
|
-
refresh_token: this.cachedToken.refreshToken,
|
|
177
|
-
client_id: appCredential.clientId,
|
|
178
|
-
})
|
|
179
|
-
|
|
180
|
-
if (appCredential.clientSecret) {
|
|
181
|
-
params.set('client_secret', appCredential.clientSecret)
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
const response = await fetchWithTimeout(this.oauth2Config.tokenUrl, {
|
|
185
|
-
method: 'POST',
|
|
186
|
-
headers: {
|
|
187
|
-
'Content-Type': 'application/x-www-form-urlencoded',
|
|
188
|
-
},
|
|
189
|
-
body: params.toString(),
|
|
190
|
-
})
|
|
191
|
-
|
|
192
|
-
if (!response.ok) {
|
|
193
|
-
throw new Error(`Token refresh failed: ${response.status}`)
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
const data = await response.json()
|
|
197
|
-
|
|
198
|
-
if (!data.access_token || typeof data.access_token !== 'string') {
|
|
199
|
-
throw new Error('Invalid token response: missing access_token')
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
const token: OAuth2Token = {
|
|
203
|
-
accessToken: data.access_token,
|
|
204
|
-
refreshToken: data.refresh_token || this.cachedToken.refreshToken,
|
|
205
|
-
expiresAt: data.expires_in
|
|
206
|
-
? Date.now() + data.expires_in * 1000
|
|
207
|
-
: undefined,
|
|
208
|
-
tokenType: data.token_type || 'Bearer',
|
|
209
|
-
scope: data.scope,
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
await this.secrets.setSecret(this.oauth2Config.tokenSecretId, token)
|
|
213
|
-
this.cachedToken = token
|
|
214
|
-
|
|
215
|
-
return token
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
/**
|
|
219
|
-
* Check if token is valid (not expired).
|
|
220
|
-
*/
|
|
221
|
-
private isTokenValid(token: OAuth2Token): boolean {
|
|
222
|
-
if (!token.expiresAt) {
|
|
223
|
-
return true // No expiry info, assume valid
|
|
224
|
-
}
|
|
225
|
-
return token.expiresAt > Date.now() + TOKEN_EXPIRY_BUFFER_MS
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
/**
|
|
229
|
-
* Get app credentials (cached).
|
|
230
|
-
*/
|
|
231
|
-
private async getAppCredential(): Promise<OAuth2AppCredential> {
|
|
232
|
-
if (this.cachedAppCredential) {
|
|
233
|
-
return this.cachedAppCredential
|
|
234
|
-
}
|
|
235
|
-
this.cachedAppCredential =
|
|
236
|
-
await this.secrets.getSecret<OAuth2AppCredential>(
|
|
237
|
-
this.appCredentialSecretId
|
|
238
|
-
)
|
|
239
|
-
return this.cachedAppCredential
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
// ========================================
|
|
243
|
-
// CLI-only methods (for pikku oauth connect)
|
|
244
|
-
// ========================================
|
|
245
|
-
|
|
246
|
-
/**
|
|
247
|
-
* Generate OAuth2 authorization URL.
|
|
248
|
-
* Used by CLI during `pikku oauth connect`.
|
|
249
|
-
*/
|
|
250
|
-
async getAuthorizationUrl(
|
|
251
|
-
state: string,
|
|
252
|
-
redirectUri: string
|
|
253
|
-
): Promise<string> {
|
|
254
|
-
const appCredential = await this.getAppCredential()
|
|
255
|
-
|
|
256
|
-
const params = new URLSearchParams({
|
|
257
|
-
response_type: 'code',
|
|
258
|
-
client_id: appCredential.clientId,
|
|
259
|
-
redirect_uri: redirectUri,
|
|
260
|
-
scope: this.oauth2Config.scopes.join(' '),
|
|
261
|
-
state,
|
|
262
|
-
})
|
|
263
|
-
|
|
264
|
-
// Add PKCE if enabled
|
|
265
|
-
// Note: code_verifier should be stored and passed to exchangeCode
|
|
266
|
-
// For now, PKCE implementation is simplified
|
|
267
|
-
|
|
268
|
-
// Add any additional params
|
|
269
|
-
if (this.oauth2Config.additionalParams) {
|
|
270
|
-
for (const [key, value] of Object.entries(
|
|
271
|
-
this.oauth2Config.additionalParams
|
|
272
|
-
)) {
|
|
273
|
-
params.set(key, value)
|
|
274
|
-
}
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
return `${this.oauth2Config.authorizationUrl}?${params.toString()}`
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
/**
|
|
281
|
-
* Exchange authorization code for tokens.
|
|
282
|
-
* Used by CLI during `pikku oauth connect`.
|
|
283
|
-
*/
|
|
284
|
-
async exchangeCode(code: string, redirectUri: string): Promise<OAuth2Token> {
|
|
285
|
-
const appCredential = await this.getAppCredential()
|
|
286
|
-
|
|
287
|
-
const params = new URLSearchParams({
|
|
288
|
-
grant_type: 'authorization_code',
|
|
289
|
-
code,
|
|
290
|
-
redirect_uri: redirectUri,
|
|
291
|
-
client_id: appCredential.clientId,
|
|
292
|
-
})
|
|
293
|
-
|
|
294
|
-
if (appCredential.clientSecret) {
|
|
295
|
-
params.set('client_secret', appCredential.clientSecret)
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
const response = await fetchWithTimeout(this.oauth2Config.tokenUrl, {
|
|
299
|
-
method: 'POST',
|
|
300
|
-
headers: {
|
|
301
|
-
'Content-Type': 'application/x-www-form-urlencoded',
|
|
302
|
-
},
|
|
303
|
-
body: params.toString(),
|
|
304
|
-
})
|
|
305
|
-
|
|
306
|
-
if (!response.ok) {
|
|
307
|
-
throw new Error(`Token exchange failed: ${response.status}`)
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
const data = await response.json()
|
|
311
|
-
|
|
312
|
-
if (!data.access_token || typeof data.access_token !== 'string') {
|
|
313
|
-
throw new Error('Invalid token response: missing access_token')
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
const token: OAuth2Token = {
|
|
317
|
-
accessToken: data.access_token,
|
|
318
|
-
refreshToken: data.refresh_token,
|
|
319
|
-
expiresAt: data.expires_in
|
|
320
|
-
? Date.now() + data.expires_in * 1000
|
|
321
|
-
: undefined,
|
|
322
|
-
tokenType: data.token_type || 'Bearer',
|
|
323
|
-
scope: data.scope,
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
// Cache the token
|
|
327
|
-
this.cachedToken = token
|
|
328
|
-
|
|
329
|
-
return token
|
|
330
|
-
}
|
|
331
|
-
}
|
|
@@ -1,234 +0,0 @@
|
|
|
1
|
-
import type { CredentialService } from '../../services/credential-service.js'
|
|
2
|
-
import type { SecretService } from '../../services/secret-service.js'
|
|
3
|
-
import type { JWTService } from '../../services/jwt-service.js'
|
|
4
|
-
import type { CredentialDefinitionsMeta } from '../credential/credential.types.js'
|
|
5
|
-
import type { OAuth2Token } from './oauth2.types.js'
|
|
6
|
-
import { OAuth2Client } from './oauth2-client.js'
|
|
7
|
-
|
|
8
|
-
const TOKEN_EXPIRY_BUFFER_MS = 60_000
|
|
9
|
-
|
|
10
|
-
export type CreateOAuth2HandlerOptions = {
|
|
11
|
-
credentialsMeta: CredentialDefinitionsMeta
|
|
12
|
-
basePath?: string
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
type OAuth2RouteContext = {
|
|
16
|
-
credentialsMeta: CredentialDefinitionsMeta
|
|
17
|
-
basePath: string
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
function getCredentialMeta(ctx: OAuth2RouteContext, name: string) {
|
|
21
|
-
const meta = ctx.credentialsMeta[name]
|
|
22
|
-
if (!meta) {
|
|
23
|
-
throw new Error(`Credential '${name}' not found`)
|
|
24
|
-
}
|
|
25
|
-
if (!meta.oauth2) {
|
|
26
|
-
throw new Error(`Credential '${name}' is not an OAuth2 credential`)
|
|
27
|
-
}
|
|
28
|
-
return meta
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
/**
|
|
32
|
-
* Creates OAuth2 route handlers for user credential management.
|
|
33
|
-
*
|
|
34
|
-
* Returns individual handler functions for connect/callback/disconnect/status
|
|
35
|
-
* that handle the OAuth2 authorization code flow and store tokens in CredentialService.
|
|
36
|
-
*
|
|
37
|
-
* @example
|
|
38
|
-
* ```typescript
|
|
39
|
-
* const oauth2 = createOAuth2Handler({ credentialsMeta })
|
|
40
|
-
*
|
|
41
|
-
* const oauth2Routes = defineHTTPRoutes({
|
|
42
|
-
* auth: true,
|
|
43
|
-
* basePath: '/credentials',
|
|
44
|
-
* routes: {
|
|
45
|
-
* connect: { method: 'get', route: '/:name/connect', func: oauth2.connect },
|
|
46
|
-
* callback: { method: 'get', route: '/:name/callback', func: oauth2.callback, auth: false },
|
|
47
|
-
* disconnect: { method: 'delete', route: '/:name', func: oauth2.disconnect },
|
|
48
|
-
* status: { method: 'get', route: '/:name/status', func: oauth2.status },
|
|
49
|
-
* },
|
|
50
|
-
* })
|
|
51
|
-
*
|
|
52
|
-
* wireHTTPRoutes({ routes: { credentials: oauth2Routes } })
|
|
53
|
-
* ```
|
|
54
|
-
*/
|
|
55
|
-
export const createOAuth2Handler = (options: CreateOAuth2HandlerOptions) => {
|
|
56
|
-
const basePath = options.basePath ?? '/credentials'
|
|
57
|
-
const ctx: OAuth2RouteContext = {
|
|
58
|
-
credentialsMeta: options.credentialsMeta,
|
|
59
|
-
basePath,
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
const connectHandler = async (
|
|
63
|
-
services: {
|
|
64
|
-
secrets: SecretService
|
|
65
|
-
jwt: JWTService
|
|
66
|
-
credentialService: CredentialService
|
|
67
|
-
},
|
|
68
|
-
_data: any,
|
|
69
|
-
wire: any
|
|
70
|
-
) => {
|
|
71
|
-
const { name } = wire.http.request.params()
|
|
72
|
-
const meta = getCredentialMeta(ctx, name)
|
|
73
|
-
const queryParams = wire.http.request.query()
|
|
74
|
-
const redirectUrl = queryParams.redirect_url || queryParams.redirect
|
|
75
|
-
|
|
76
|
-
const oauth2Client = new OAuth2Client(
|
|
77
|
-
meta.oauth2!,
|
|
78
|
-
meta.oauth2!.appCredentialSecretId,
|
|
79
|
-
services.secrets
|
|
80
|
-
)
|
|
81
|
-
|
|
82
|
-
const userId = wire.session?.userId
|
|
83
|
-
if (!userId) {
|
|
84
|
-
throw new Error('Authentication required for OAuth2 connect')
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
const state = await services.jwt.encode(
|
|
88
|
-
{ value: 10, unit: 'minute' },
|
|
89
|
-
{
|
|
90
|
-
userId,
|
|
91
|
-
credentialName: name,
|
|
92
|
-
redirectUrl,
|
|
93
|
-
}
|
|
94
|
-
)
|
|
95
|
-
|
|
96
|
-
let origin = wire.http.request.header('origin')
|
|
97
|
-
if (!origin) {
|
|
98
|
-
const host = wire.http.request.header('host')
|
|
99
|
-
if (!host) {
|
|
100
|
-
throw new Error(
|
|
101
|
-
'Unable to determine request origin for OAuth2 callback'
|
|
102
|
-
)
|
|
103
|
-
}
|
|
104
|
-
const protocol = wire.http.request.header('x-forwarded-proto') || 'http'
|
|
105
|
-
origin = `${protocol}://${host}`
|
|
106
|
-
}
|
|
107
|
-
const callbackUrl = `${origin}${basePath}/${name}/callback`
|
|
108
|
-
const authUrl = await oauth2Client.getAuthorizationUrl(state, callbackUrl)
|
|
109
|
-
|
|
110
|
-
return Response.redirect(authUrl, 302)
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
const callbackHandler = async (
|
|
114
|
-
services: {
|
|
115
|
-
secrets: SecretService
|
|
116
|
-
jwt: JWTService
|
|
117
|
-
credentialService: CredentialService
|
|
118
|
-
logger: any
|
|
119
|
-
},
|
|
120
|
-
_data: any,
|
|
121
|
-
wire: any
|
|
122
|
-
) => {
|
|
123
|
-
const { name } = wire.http.request.params()
|
|
124
|
-
const meta = getCredentialMeta(ctx, name)
|
|
125
|
-
const queryParams = wire.http.request.query()
|
|
126
|
-
const code = queryParams.code as string | undefined
|
|
127
|
-
const state = queryParams.state as string | undefined
|
|
128
|
-
|
|
129
|
-
if (!code || !state) {
|
|
130
|
-
throw new Error('Missing code or state parameter')
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
let statePayload: {
|
|
134
|
-
userId: string
|
|
135
|
-
credentialName: string
|
|
136
|
-
redirectUrl?: string
|
|
137
|
-
}
|
|
138
|
-
try {
|
|
139
|
-
statePayload = await services.jwt.decode(state)
|
|
140
|
-
} catch {
|
|
141
|
-
throw new Error('Invalid or expired OAuth2 state')
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
if (statePayload.credentialName !== name) {
|
|
145
|
-
throw new Error('Credential name mismatch in state')
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
const oauth2Client = new OAuth2Client(
|
|
149
|
-
meta.oauth2!,
|
|
150
|
-
meta.oauth2!.appCredentialSecretId,
|
|
151
|
-
services.secrets
|
|
152
|
-
)
|
|
153
|
-
|
|
154
|
-
let origin = wire.http.request.header('origin')
|
|
155
|
-
if (!origin) {
|
|
156
|
-
const host = wire.http.request.header('host')
|
|
157
|
-
if (!host) {
|
|
158
|
-
throw new Error(
|
|
159
|
-
'Unable to determine request origin for OAuth2 callback'
|
|
160
|
-
)
|
|
161
|
-
}
|
|
162
|
-
const protocol = wire.http.request.header('x-forwarded-proto') || 'http'
|
|
163
|
-
origin = `${protocol}://${host}`
|
|
164
|
-
}
|
|
165
|
-
const callbackUrl = `${origin}${basePath}/${name}/callback`
|
|
166
|
-
const tokens = await oauth2Client.exchangeCode(code, callbackUrl)
|
|
167
|
-
|
|
168
|
-
await services.credentialService.set(name, tokens, statePayload.userId)
|
|
169
|
-
|
|
170
|
-
services.logger.info(
|
|
171
|
-
`OAuth2 tokens stored for user '${statePayload.userId}', credential '${name}'`
|
|
172
|
-
)
|
|
173
|
-
|
|
174
|
-
if (statePayload.redirectUrl) {
|
|
175
|
-
return Response.redirect(statePayload.redirectUrl, 302)
|
|
176
|
-
} else {
|
|
177
|
-
return { success: true, credentialName: name }
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
const disconnectHandler = async (
|
|
182
|
-
services: { credentialService: CredentialService; logger: any },
|
|
183
|
-
_data: any,
|
|
184
|
-
wire: any
|
|
185
|
-
) => {
|
|
186
|
-
const { name } = wire.http.request.params()
|
|
187
|
-
const userId = wire.session?.userId
|
|
188
|
-
if (!userId) {
|
|
189
|
-
throw new Error('Authentication required')
|
|
190
|
-
}
|
|
191
|
-
await services.credentialService.delete(name, userId)
|
|
192
|
-
services.logger.info(
|
|
193
|
-
`OAuth2 credential '${name}' disconnected for user '${userId}'`
|
|
194
|
-
)
|
|
195
|
-
return { success: true }
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
const statusHandler = async (
|
|
199
|
-
services: { credentialService: CredentialService },
|
|
200
|
-
_data: any,
|
|
201
|
-
wire: any
|
|
202
|
-
) => {
|
|
203
|
-
const { name } = wire.http.request.params()
|
|
204
|
-
const userId = wire.session?.userId
|
|
205
|
-
if (!userId) {
|
|
206
|
-
throw new Error('Authentication required')
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
const credential = await services.credentialService.get<OAuth2Token>(
|
|
210
|
-
name,
|
|
211
|
-
userId
|
|
212
|
-
)
|
|
213
|
-
|
|
214
|
-
if (!credential) {
|
|
215
|
-
return { connected: false }
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
return {
|
|
219
|
-
connected: true,
|
|
220
|
-
hasRefreshToken: !!credential.refreshToken,
|
|
221
|
-
expiresAt: credential.expiresAt,
|
|
222
|
-
isExpired: credential.expiresAt
|
|
223
|
-
? credential.expiresAt < Date.now() + TOKEN_EXPIRY_BUFFER_MS
|
|
224
|
-
: false,
|
|
225
|
-
}
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
return {
|
|
229
|
-
connect: { func: connectHandler } as any,
|
|
230
|
-
callback: { func: callbackHandler } as any,
|
|
231
|
-
disconnect: { func: disconnectHandler } as any,
|
|
232
|
-
status: { func: statusHandler } as any,
|
|
233
|
-
}
|
|
234
|
-
}
|