create-loopengine 0.1.44 → 0.1.45
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/package.json +1 -1
- package/template/adapters/http.ts +27 -58
package/package.json
CHANGED
|
@@ -137,6 +137,7 @@ import {
|
|
|
137
137
|
import { SkillGarden } from 'loopengine'
|
|
138
138
|
import type { Decision, PendingApproval } from 'actauth'
|
|
139
139
|
import type { LoopEvent } from 'loopengine'
|
|
140
|
+
import { storageSigners } from 'loopengine'
|
|
140
141
|
|
|
141
142
|
const sessions = createSessionStore()
|
|
142
143
|
const checkpoints = createCheckpointStore()
|
|
@@ -2053,90 +2054,58 @@ async function respondAfterResolution(res: ServerResponse, finalCheckpoint: Turn
|
|
|
2053
2054
|
// long as the click that requested it takes to resolve, not however
|
|
2054
2055
|
// long a chat reply might sit around unopened).
|
|
2055
2056
|
//
|
|
2056
|
-
// Deliberately generic, not tied to any one ability
|
|
2057
|
-
//
|
|
2058
|
-
// signing itself, as long as it authenticates the same way
|
|
2057
|
+
// Deliberately generic on two axes, not tied to any one ability or
|
|
2058
|
+
// cloud provider: any ability can point a tool's own returned URL here
|
|
2059
|
+
// instead of signing itself, as long as it authenticates the same way
|
|
2060
|
+
// loopengine's own storageSigners.gcs already does
|
|
2061
|
+
// (GOOGLE_APPLICATION_CREDENTIALS_JSON, falling back to Application
|
|
2062
|
+
// Default Credentials) — that's the same logic
|
|
2059
2063
|
// lp-product-ad-images'/lp-file-archiver's own buildGcsStorageClient
|
|
2060
|
-
// already
|
|
2061
|
-
//
|
|
2062
|
-
//
|
|
2063
|
-
//
|
|
2064
|
-
//
|
|
2064
|
+
// already duplicate for the *upload* side, centralized in loopengine
|
|
2065
|
+
// core for signing instead. `provider` picks which one of
|
|
2066
|
+
// loopengine's own storageSigners actually does the signing (only
|
|
2067
|
+
// `gcs` exists today) — adding S3/Azure/etc. later is a loopengine
|
|
2068
|
+
// core change, not something this scaffolded file needs to know about.
|
|
2065
2069
|
//
|
|
2066
2070
|
// Sits behind the same isAuthorized() Basic Auth gate every other route
|
|
2067
2071
|
// on this server already requires (checked once, unconditionally,
|
|
2068
2072
|
// before any routing below even runs) — not a separately public
|
|
2069
2073
|
// endpoint. A browser that already loaded /playground has that same
|
|
2070
2074
|
// origin's Basic Auth credentials cached, so a plain
|
|
2071
|
-
// <img src="/
|
|
2072
|
-
// extra wiring; anything without those credentials gets the
|
|
2073
|
-
// every other route already gives it.
|
|
2074
|
-
async function
|
|
2075
|
+
// <img src="/storage-redirect?..."> or download link both just work
|
|
2076
|
+
// with no extra wiring; anything without those credentials gets the
|
|
2077
|
+
// same 401 every other route already gives it.
|
|
2078
|
+
async function handleStorageRedirect(req: IncomingMessage, res: ServerResponse): Promise<void> {
|
|
2075
2079
|
const url = new URL(req.url ?? '/', 'http://localhost')
|
|
2080
|
+
const provider = url.searchParams.get('provider')
|
|
2076
2081
|
const bucket = url.searchParams.get('bucket')
|
|
2077
2082
|
const object = url.searchParams.get('object')
|
|
2078
2083
|
const disposition = url.searchParams.get('disposition')
|
|
2079
2084
|
const filename = url.searchParams.get('filename')
|
|
2080
2085
|
|
|
2081
|
-
if (!bucket || !object) {
|
|
2082
|
-
res.writeHead(400, { 'content-type': 'application/json' }).end(JSON.stringify({ error: '
|
|
2086
|
+
if (!provider || !bucket || !object) {
|
|
2087
|
+
res.writeHead(400, { 'content-type': 'application/json' }).end(JSON.stringify({ error: 'storage-redirect requires provider, bucket, and object query params' }))
|
|
2083
2088
|
return
|
|
2084
2089
|
}
|
|
2085
2090
|
if (disposition && disposition !== 'inline' && disposition !== 'attachment') {
|
|
2086
2091
|
res.writeHead(400, { 'content-type': 'application/json' }).end(JSON.stringify({ error: `disposition must be "inline" or "attachment" — got "${disposition}"` }))
|
|
2087
2092
|
return
|
|
2088
2093
|
}
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
|
|
2093
|
-
// lp-product-ad-images'/lp-file-archiver's own saveImage/saveArchive
|
|
2094
|
-
// already use, for the same reason: installing it is only required at
|
|
2095
|
-
// runtime for a deployment that actually uses this route, not every
|
|
2096
|
-
// scaffolded project.
|
|
2097
|
-
const gcsModuleName = '@google-cloud/storage'
|
|
2098
|
-
let gcs: any
|
|
2099
|
-
try {
|
|
2100
|
-
gcs = await import(gcsModuleName)
|
|
2101
|
-
} catch {
|
|
2102
|
-
res.writeHead(500, { 'content-type': 'application/json' }).end(
|
|
2103
|
-
JSON.stringify({ error: 'gcs-redirect requires the @google-cloud/storage package — npm install @google-cloud/storage in your own project.' }),
|
|
2094
|
+
const signer = storageSigners[provider]
|
|
2095
|
+
if (!signer) {
|
|
2096
|
+
res.writeHead(400, { 'content-type': 'application/json' }).end(
|
|
2097
|
+
JSON.stringify({ error: `storage-redirect: unknown provider "${provider}" — supported: ${Object.keys(storageSigners).join(', ')}` }),
|
|
2104
2098
|
)
|
|
2105
2099
|
return
|
|
2106
2100
|
}
|
|
2107
2101
|
|
|
2108
|
-
let client: any
|
|
2109
|
-
const credentialsJson = process.env.GOOGLE_APPLICATION_CREDENTIALS_JSON
|
|
2110
|
-
if (credentialsJson) {
|
|
2111
|
-
let credentials: { project_id?: string }
|
|
2112
|
-
try {
|
|
2113
|
-
credentials = JSON.parse(credentialsJson)
|
|
2114
|
-
} catch {
|
|
2115
|
-
res.writeHead(500, { 'content-type': 'application/json' }).end(
|
|
2116
|
-
JSON.stringify({
|
|
2117
|
-
error: 'GOOGLE_APPLICATION_CREDENTIALS_JSON is not valid JSON — paste the entire contents of the downloaded service-account key file, unedited.',
|
|
2118
|
-
}),
|
|
2119
|
-
)
|
|
2120
|
-
return
|
|
2121
|
-
}
|
|
2122
|
-
client = new gcs.Storage({ credentials, projectId: credentials.project_id })
|
|
2123
|
-
} else {
|
|
2124
|
-
client = new gcs.Storage()
|
|
2125
|
-
}
|
|
2126
|
-
|
|
2127
2102
|
try {
|
|
2128
|
-
const
|
|
2129
|
-
const expires = Date.now() + 5 * 60 * 1000
|
|
2130
|
-
const signOptions: Record<string, unknown> = { action: 'read', expires }
|
|
2131
|
-
if (disposition === 'attachment') {
|
|
2132
|
-
signOptions.responseDisposition = `attachment; filename="${filename || object.split('/').pop()}"`
|
|
2133
|
-
}
|
|
2134
|
-
const [signedUrl] = await file.getSignedUrl(signOptions)
|
|
2103
|
+
const signedUrl = await signer({ bucket, object, disposition: (disposition as 'inline' | 'attachment' | null) ?? undefined, filename: filename ?? undefined })
|
|
2135
2104
|
res.writeHead(302, { location: signedUrl }).end()
|
|
2136
2105
|
} catch (err) {
|
|
2137
2106
|
res
|
|
2138
2107
|
.writeHead(502, { 'content-type': 'application/json' })
|
|
2139
|
-
.end(JSON.stringify({ error: `
|
|
2108
|
+
.end(JSON.stringify({ error: `storage-redirect: could not sign a URL for ${provider}://${bucket}/${object}: ${err instanceof Error ? err.message : String(err)}` }))
|
|
2140
2109
|
}
|
|
2141
2110
|
}
|
|
2142
2111
|
|
|
@@ -2586,8 +2555,8 @@ const server = createServer(async (req, res) => {
|
|
|
2586
2555
|
return
|
|
2587
2556
|
}
|
|
2588
2557
|
|
|
2589
|
-
if (req.method === 'GET' && pathname === '/
|
|
2590
|
-
await
|
|
2558
|
+
if (req.method === 'GET' && pathname === '/storage-redirect') {
|
|
2559
|
+
await handleStorageRedirect(req, res)
|
|
2591
2560
|
return
|
|
2592
2561
|
}
|
|
2593
2562
|
|