create-loopengine 0.1.43 → 0.1.44

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-loopengine",
3
- "version": "0.1.43",
3
+ "version": "0.1.44",
4
4
  "description": "Scaffold a new loopengine agent project — a runnable HTTP server with one example agent, in your own repo.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -2038,6 +2038,108 @@ async function respondAfterResolution(res: ServerResponse, finalCheckpoint: Turn
2038
2038
  )
2039
2039
  }
2040
2040
 
2041
+ // A generated image/archive's own signed cloud-storage URL is a long,
2042
+ // high-entropy string (a GCS V4 Signature can run ~300 base64
2043
+ // characters) that an agent's own reply has to reproduce character-for-
2044
+ // character to embed as a markdown image/link — expensive in output
2045
+ // tokens, slow to generate, and one flipped character anywhere in it
2046
+ // breaks the whole thing. This route lets a tool return a short, stable
2047
+ // object reference instead — bucket + object name, both human-readable,
2048
+ // not random — and defer the actual signing to request time, right
2049
+ // here, freshly, on every click: shorter for the model to write,
2050
+ // nothing random left to transcribe wrong, and a much shorter
2051
+ // signed-URL lifetime than an ability calling getSignedUrl itself at
2052
+ // generation time ever needed (this one only has to stay valid for as
2053
+ // long as the click that requested it takes to resolve, not however
2054
+ // long a chat reply might sit around unopened).
2055
+ //
2056
+ // Deliberately generic, not tied to any one ability — any ability using
2057
+ // GCS storage can point a tool's own returned URL here instead of
2058
+ // signing itself, as long as it authenticates the same way
2059
+ // lp-product-ad-images'/lp-file-archiver's own buildGcsStorageClient
2060
+ // already do (GOOGLE_APPLICATION_CREDENTIALS_JSON, falling back to
2061
+ // Application Default Credentials) — this is that same logic,
2062
+ // centralized, rather than every GCS-backed ability duplicating it
2063
+ // (which they already do today for the *upload* side; this just does
2064
+ // the same for signing).
2065
+ //
2066
+ // Sits behind the same isAuthorized() Basic Auth gate every other route
2067
+ // on this server already requires (checked once, unconditionally,
2068
+ // before any routing below even runs) — not a separately public
2069
+ // endpoint. A browser that already loaded /playground has that same
2070
+ // origin's Basic Auth credentials cached, so a plain
2071
+ // <img src="/gcs-redirect?..."> or download link both just work with no
2072
+ // extra wiring; anything without those credentials gets the same 401
2073
+ // every other route already gives it.
2074
+ async function handleGcsRedirect(req: IncomingMessage, res: ServerResponse): Promise<void> {
2075
+ const url = new URL(req.url ?? '/', 'http://localhost')
2076
+ const bucket = url.searchParams.get('bucket')
2077
+ const object = url.searchParams.get('object')
2078
+ const disposition = url.searchParams.get('disposition')
2079
+ const filename = url.searchParams.get('filename')
2080
+
2081
+ if (!bucket || !object) {
2082
+ res.writeHead(400, { 'content-type': 'application/json' }).end(JSON.stringify({ error: 'gcs-redirect requires both bucket and object query params' }))
2083
+ return
2084
+ }
2085
+ if (disposition && disposition !== 'inline' && disposition !== 'attachment') {
2086
+ res.writeHead(400, { 'content-type': 'application/json' }).end(JSON.stringify({ error: `disposition must be "inline" or "attachment" — got "${disposition}"` }))
2087
+ return
2088
+ }
2089
+
2090
+ // Imported by a variable, not a string literal, so tsc treats this as
2091
+ // Promise<any> instead of trying to resolve @google-cloud/storage's
2092
+ // own types at compile time — same reasoning
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.' }),
2104
+ )
2105
+ return
2106
+ }
2107
+
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
+ try {
2128
+ const file = client.bucket(bucket).file(object)
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)
2135
+ res.writeHead(302, { location: signedUrl }).end()
2136
+ } catch (err) {
2137
+ res
2138
+ .writeHead(502, { 'content-type': 'application/json' })
2139
+ .end(JSON.stringify({ error: `gcs-redirect: could not sign a URL for gs://${bucket}/${object}: ${err instanceof Error ? err.message : String(err)}` }))
2140
+ }
2141
+ }
2142
+
2041
2143
  const server = createServer(async (req, res) => {
2042
2144
  if (!isAuthorized(req)) {
2043
2145
  res
@@ -2484,6 +2586,11 @@ const server = createServer(async (req, res) => {
2484
2586
  return
2485
2587
  }
2486
2588
 
2589
+ if (req.method === 'GET' && pathname === '/gcs-redirect') {
2590
+ await handleGcsRedirect(req, res)
2591
+ return
2592
+ }
2593
+
2487
2594
  const match = req.method === 'POST' && pathname.match(/^\/agents\/([^/]+)\/messages$/)
2488
2595
  if (!match) {
2489
2596
  res.writeHead(404, { 'content-type': 'application/json' }).end(JSON.stringify({ error: 'not found' }))