@koda-sl/baker-cli 0.285.0-dev.e3dcdf3eb → 0.287.0-dev.4a5d811dc
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 +53 -85
- package/canvas/tiktok-captions-composition/index.html +2 -23
- package/dist/{chunk-2IUN3S3E.js → chunk-2IB3IL4K.js} +2 -2
- package/dist/{chunk-4YF56EGK.js → chunk-D3HHUBIL.js} +4 -4
- package/dist/{chunk-WFWU3CHS.js → chunk-DZUVUGEP.js} +1 -9
- package/dist/{chunk-WFWU3CHS.js.map → chunk-DZUVUGEP.js.map} +1 -1
- package/dist/{chunk-A2VZOTCV.js → chunk-EX2OIOVL.js} +3 -3
- package/dist/{chunk-KDTHRRAC.js → chunk-GU7IWEBC.js} +3 -3
- package/dist/{chunk-L5XGJNUM.js → chunk-LN5O53BG.js} +43 -269
- package/dist/chunk-LN5O53BG.js.map +1 -0
- package/dist/cli.js +5887 -6880
- package/dist/cli.js.map +1 -1
- package/dist/{client-6KQHCXS2.js → client-VDCCDEHE.js} +4 -4
- package/dist/engine/index.d.ts +0 -13
- package/dist/engine/index.js +3 -3
- package/dist/{env-C7YXL6C6.js → env-FWMZXMQK.js} +2 -6
- package/dist/{output-RBM32FKJ.js → output-4H2RNBXL.js} +5 -5
- package/dist/{shared-WJIJTWST.js → shared-UKL36AXT.js} +6 -6
- package/package.json +1 -1
- package/dist/chunk-L5XGJNUM.js.map +0 -1
- /package/dist/{chunk-2IUN3S3E.js.map → chunk-2IB3IL4K.js.map} +0 -0
- /package/dist/{chunk-4YF56EGK.js.map → chunk-D3HHUBIL.js.map} +0 -0
- /package/dist/{chunk-A2VZOTCV.js.map → chunk-EX2OIOVL.js.map} +0 -0
- /package/dist/{chunk-KDTHRRAC.js.map → chunk-GU7IWEBC.js.map} +0 -0
- /package/dist/{client-6KQHCXS2.js.map → client-VDCCDEHE.js.map} +0 -0
- /package/dist/{env-C7YXL6C6.js.map → env-FWMZXMQK.js.map} +0 -0
- /package/dist/{output-RBM32FKJ.js.map → output-4H2RNBXL.js.map} +0 -0
- /package/dist/{shared-WJIJTWST.js.map → shared-UKL36AXT.js.map} +0 -0
package/README.md
CHANGED
|
@@ -1256,19 +1256,25 @@ baker analytics tracking # do the ad URLs carry the campaign, a
|
|
|
1256
1256
|
baker analytics tracking --platform meta # the same for one platform, read from their own ad account
|
|
1257
1257
|
baker analytics map --platform google --set kw=keyword # name a parameter, seen or not yet
|
|
1258
1258
|
baker analytics map --platform google --remove kw # take that answer back
|
|
1259
|
-
baker analytics conversions # what counts
|
|
1259
|
+
baker analytics conversions # what counts as a conversion, and what each one produced
|
|
1260
1260
|
baker analytics conversions --candidates # everything these pages do, and which of it is counted
|
|
1261
|
-
baker analytics conversions --flow contact # one Form's endings as event keys, traffic or not
|
|
1262
1261
|
baker analytics conversions --event page:request_demo --name "Demo requested"
|
|
1263
|
-
baker analytics conversions --event page:a --event submit:b --name "Lead" # both, as one number
|
|
1264
|
-
baker analytics conversions --event page:a --count-mode every_time # re-mode, keeping its name
|
|
1265
|
-
baker analytics conversions --rename "Lead form" --to "Lead" # merge two into one row
|
|
1266
1262
|
baker analytics conversions --remove page:request_demo
|
|
1267
|
-
baker analytics
|
|
1263
|
+
baker analytics sends # where each conversion is sent, and what happened to each upload
|
|
1264
|
+
baker analytics sends --set --conversion "Deal won" --platform meta --tag <tagId> --event Purchase # staged; spends budget once published
|
|
1265
|
+
baker analytics sends --remove s1 # stop one, when the chat is published
|
|
1268
1266
|
baker analytics delivery --page 2 # the next page of a long list
|
|
1269
1267
|
baker analytics submissions --flow contact # every attempt at a Form, delivered or not
|
|
1268
|
+
baker analytics sources # the addresses the client's own tools send events to
|
|
1269
|
+
baker analytics sources --create --name HubSpot # a new address, learning; hand the URL to the user
|
|
1270
|
+
baker analytics sources --map --source <id> --field email=contact.email --field value=deal.amount
|
|
1271
|
+
baker analytics sources --create-hubspot --property lifecyclestage --value customer # a source Baker reads from HubSpot every five minutes
|
|
1270
1272
|
```
|
|
1271
1273
|
|
|
1274
|
+
**Sources: a URL for a tool that cannot be told what to post.** `sending-events` is the contract a developer writes to; `sources` is for a CRM webhook, an automation step or a billing notification that posts whatever it posts. A new source is `learning`: its first send is kept as a masked sample and counted as nothing until `--map` confirms which path holds which field (`--field` repeats; a body already in Baker's own format needs none). From then on every arrival is an ordinary event carrying `baker_source: <name>`, and `--pause`, `--resume` and `--remove` are by `--source <id>`.
|
|
1275
|
+
|
|
1276
|
+
**`--create-hubspot --property <name> --value <value> [--name <name>]`: a source Baker reads, for a HubSpot plan that cannot send a webhook.** Every five minutes the connected account is checked and every contact whose property becomes that value is one event named after the source (default: the value's label), carrying `baker_source`, the contact's email, phone, id, name, address and HubSpot's `gclid` / `fbclid`. Names come from `baker hubspot contacts properties`. Counts from creation on, never earlier contacts; a contact already at the value counts only when it leaves and re-enters. `counting` at once, nothing to map, no URL; the source carries `hubspot { property, propertyLabel, value, valueLabel }`, `countingSince`, `lastPolledAt` and `lastError`. Deals are not watched — sync the outcome onto the contact and read it there.
|
|
1277
|
+
|
|
1272
1278
|
Shared flags: `--days <n>` (default 30) or `--start-date` / `--end-date` (`YYYY-MM-DD`, end inclusive); `--full`; `--compare` to also return the preceding window of the same length; `--timezone <IANA>` and `--granularity hour|day|week|month` to shape a series. Every call reads live — there is no cache layer, so there is nothing to bypass.
|
|
1273
1279
|
|
|
1274
1280
|
**`--output md` when the numbers are going to be read.** Every report is emitted as compact JSON by default, and `--output md` renders each block as a Markdown table instead — one header rather than the keys repeated on every row, with the identical `warnings` and `hints`. Measured across all sixteen presets against a real deployment, the two changes together take the family from 110 KB to 44 KB — **about 60% smaller overall**, 38% at worst (`ads`, which is dense numeric data with nothing to prune) and 77% at best (`landings`). Three further economies apply to both formats, because the reader is usually an LLM with a context window rather than a browser:
|
|
@@ -1279,11 +1285,9 @@ Shared flags: `--days <n>` (default 30) or `--start-date` / `--end-date` (`YYYY-
|
|
|
1279
1285
|
|
|
1280
1286
|
**Paging the list reports.** `people`, `submissions` and `delivery` are lists of individual things rather than breakdowns — `delivery` and `submissions` are the ones the CLI exposes, `people` backs the People section of the dashboard's landing Analytics screen over the same wire — and only they take `--page <n>` (1-based) and `--page-size <n>` (up to 200, default 50). The response carries `pageInfo` with `hasMore` — when it is true there **are** more rows, so a total must never be reported from one page. Pass neither flag and a list comes back at the size it always did.
|
|
1281
1287
|
|
|
1282
|
-
**A conversion is a company-level named event, not a property of a Form.** `baker analytics conversions` is the only place "what counts" is decided, and an empty `definitions` list means every conversion number in every other report is zero — not because nobody converted, but because nothing is named as an outcome and Baker never guesses which event is the point of a page. `--candidates` lists every event these pages actually produced — each Form step and trigger, every `data-baker-*` event, every outbound destination — with volume and whether it is already counted; that list is where an event key comes from, so never invent one. `--event <key> --name "Booked a call"` starts counting it **immediately and retroactively**: the whole stored history is rescored, so naming the right event today also fixes last month. Nothing here is staged and publishing is not involved. Two events given the same `--name` become one row and one number, which is how a call booked on three different Forms reads as one outcome. `--count-mode every_time` is for the outcomes people genuinely repeat (a guide downloaded twice is two downloads); the default counts once per visit.
|
|
1283
|
-
|
|
1284
|
-
**The name is the grouping, so the CLI defends it.** Repeat `--event` to count several events under one `--name` in a single call; `--rename "<old>" --to "<new>"` renames every event under an outcome, and renaming *onto* a name that already exists **merges** them into one row and one number — the fix for a company whose Forms each invented their own word for the same thing. `--remove-name` drops an outcome entirely, `--remove` a single event key. A name that differs from one already counted only by case, accent, punctuation or a plural is **refused**: Baker groups by exact name, so "Booked a Call" beside "Booked a call" is a number split in half with both halves plausible. Every answer leads with the names already in use, and a company that has named nothing gets a standard set to start from — after which the rule is reuse, in that company's own language.
|
|
1288
|
+
**A conversion is a company-level named event, not a property of a Form.** `baker analytics conversions` is the only place "what counts" is decided, and an empty `definitions` list means every conversion number in every other report is zero — not because nobody converted, but because nothing is named as an outcome and Baker never guesses which event is the point of a page. `--candidates` lists every event these pages actually produced — each Form step and trigger, every `data-baker-*` event, every outbound destination — with volume and whether it is already counted; that list is where an event key comes from, so never invent one. `--event <key> --name "Booked a call"` starts counting it **immediately and retroactively**: the whole stored history is rescored, so naming the right event today also fixes last month. Nothing here is staged and publishing is not involved. Two events given the same `--name` become one row and one number, which is how a call booked on three different Forms reads as one outcome. `--count-mode every_time` is for the outcomes people genuinely repeat (a guide downloaded twice is two downloads); the default counts once per visit. One outcome per call — repeating `--event` is refused rather than silently keeping the last one.
|
|
1285
1289
|
|
|
1286
|
-
|
|
1290
|
+
**A send belongs to the conversion, and `baker analytics sends` is where it is set.** "Deal won" → Meta Purchase means every event counted as "Deal won" — from any Form, page or the client's own systems — is uploaded to Meta with who the person is, the click that brought them and the amount forwarded automatically; there is nothing to map, and a send is never set on a funnel step (the step reads its conversions' sends back as `sends[]`). With no flags it lists the sends in effect with `sends[].outcome` ("12 sent · 1 failed", `lastError` in the platform's words), `platforms[]` with the tags on Tracking that can send, and `conversions[]`, the names that can be sent. `--set --conversion <name> --platform <p> --tag <tagId> --event <e>` **stages** a send: it lands when the chat is published, and from then on it **spends ad budget** on every event under the name — say so plainly; never call a published send a preview. `--tag` is copied from `platforms[].tags[].tagId`, which lists only the tags that hold their server credential — one typed from memory is refused, and a platform with none says what to add on Tracking. A name nothing is counted as is refused too. `--remove <id>` stops one, staged the same way. `warnings[]` names a Form that already sends the same thing itself, so the platform would count it twice.
|
|
1287
1291
|
|
|
1288
1292
|
**Campaign parameters are per platform, and so are the numbers.** `baker analytics ads` returns `adPlatformTraffic` (visits, conversions and *tagged* visits for each of the nine platforms), `adParams` for the platform in `--platform` only, and `adParamsUnattributed` for names arriving on visits with no click id and no recognisable source. Read `adPlatformTraffic` first: a platform with visits and zero tagged visits has untagged ad URLs, and no mapping can fix that from Baker's side. `baker analytics map` needs `--platform` for any change, accepts a parameter name **nobody has sent yet** — which is how a tracking template is configured before its campaign runs — and takes an answer back with `--remove`. `--set <name>=ignore` says a parameter is not campaign information at all, so Baker stops storing it and stops listing it.
|
|
1289
1293
|
|
|
@@ -1318,6 +1322,39 @@ Data is retained for 400 days; a window reaching further back comes back with a
|
|
|
1318
1322
|
|
|
1319
1323
|
---
|
|
1320
1324
|
|
|
1325
|
+
### The company's funnel (`baker funnel`)
|
|
1326
|
+
|
|
1327
|
+
The steps a company's business actually has, wired as a graph, joined to the moments Baker already records — and counted in PEOPLE on every read. It is what makes "how far did the people from this campaign get" answerable: a conversion count can say how many converted, never how many reached Meeting booked and where the rest stopped.
|
|
1328
|
+
|
|
1329
|
+
```bash
|
|
1330
|
+
baker funnel # the funnel this company has, with 30 days of people per step
|
|
1331
|
+
baker funnel --days 90 # a longer window
|
|
1332
|
+
baker funnel --moments # everything a trigger can name — read before writing any trigger
|
|
1333
|
+
baker funnel --templates # the step library and the presets
|
|
1334
|
+
baker funnel --preset b2b_sales # draw the closest graph
|
|
1335
|
+
baker funnel --steps '[{"key":"lead","displayName":"Enquiry","stepClass":"lead","after":[],"triggers":[{"id":"t1","kind":"conversion","eventKey":"submit:contact"}]}]'
|
|
1336
|
+
```
|
|
1337
|
+
|
|
1338
|
+
**Every step comes back counted.** `people` reached it, `atLeast` got at least that far (the step or anything after it), `before` got as far as the step before, `share` is `atLeast / before`, and `money` is what its events said they were worth, by currency. `window` says which days, `worstStep` names where the most people are lost, and the hints say it in a sentence. Counted in people across visits and devices — a landing viewed on a phone, a Form completed on a laptop and a deal a CRM posted under its own id are one person when a contact digest joins them — and unordered, so a deal the CRM posted before the site's submit landed still counts. Never quote these as visits.
|
|
1339
|
+
|
|
1340
|
+
**The vocabulary is the company's; the stage is closed.** A company invents its own steps and names them whatever it calls them — `displayName` is unconstrained. `key` is the stable identifier (lowercase letters, digits, underscores) and never changes when the name does. Every step also carries one of six stages — `visited`, `lead`, `engaged`, `customer`, `repeat`, `lost` — plain words every business has, used to compare one company's funnel with another's.
|
|
1341
|
+
|
|
1342
|
+
**It is a graph, not a ladder.** `after` is a list of predecessors, so two ways into one step is ordinary rather than an anomaly — people arrive by seeing the landing *or* by asking for a demo. An empty `after` means people come in there. A cycle or a predecessor that does not exist is refused with the step named.
|
|
1343
|
+
|
|
1344
|
+
**`--steps` replaces the whole funnel.** Read first and restate everything you want to keep. `--preset` is the shortcut for a company that has none.
|
|
1345
|
+
|
|
1346
|
+
**A step says what triggers it, and every trigger is PICKED from `--moments`.** `triggers` is a list and any one of them firing is enough. Two kinds and no third: `{"kind":"conversion","eventKey":"submit:contact"}` for an outcome the company counts (`moments.conversions`), and `{"kind":"landing","landingId":"<id>"}` for a landing being viewed (`moments.landings`). A moment nobody has named yet — a Form step, a `data-baker-*` event, an event a client's system posts (`moments.events`, `moments.sources`) — is named as a conversion first with `baker analytics conversions --event <key> --name <name>` and then used; the funnel has no vocabulary of its own for an unnamed moment, which is what keeps it and every conversion report agreeing. `moments.events[].origin` tells a page's event (`site`) from one the client's own systems sent (`systems`); `moments.sources` lists the client's systems by the `page:<name>` key each raises, pickable before the first send arrives. A key typed from memory saves, publishes and counts nobody forever — the hint "has a trigger and nobody reached it while N people entered" is that mistake caught after the fact. A step written with `offlineConversions` or an `analytics` trigger is refused before any round trip, with where each now lives.
|
|
1347
|
+
|
|
1348
|
+
**A landing being viewed can be a step** — where most funnels start — and never a conversion: `baker analytics conversions` refuses a `view:` key.
|
|
1349
|
+
|
|
1350
|
+
**A step nothing enters advances nobody, silently.** No `triggers` — or one whose narrowing was never filled in — means the report shows zero, nothing errors, and it looks exactly like a stage people never reach. Every response names these in `hints`. **There is no default door either:** a Form is counted only where a trigger names it, and `hints` names every Form no step counts.
|
|
1351
|
+
|
|
1352
|
+
**Where a step sends is read here and set on the conversion.** Every step comes back with `sends[]` — one entry per send of every conversion among its triggers (`id`, `conversionName`, `platform`, `event`, and `outcome`: "12 sent · 1 failed", or "Nothing sent yet"). It is read-only: a send belongs to the conversion by name, so it is set with `baker analytics sends --set --conversion <name> --platform <p> --tag <tagId> --event <e>`, never on a step, and a step written with `offlineConversions` is refused with that command named. A send that failed is named in the hints (`SEND FAILED on step <key>: …`) with the platform's own error.
|
|
1353
|
+
|
|
1354
|
+
**Staged, and published in one piece.** The funnel becomes the company's when the chat is published; discarding takes it back. The client sees what you staged on the funnel canvas inside the chat and can edit it there — the same canvas, the same publish — so read the funnel back before restating it. The version it replaces is kept, so removing a step does not remove what was measured under it.
|
|
1355
|
+
|
|
1356
|
+
---
|
|
1357
|
+
|
|
1321
1358
|
### Google Analytics 4 (`baker ga4`)
|
|
1322
1359
|
|
|
1323
1360
|
GA4 commands for multi-channel audits **and** for configuring what the property measures. Playbook-aligned report presets, property health audits, free-form Data API queries, and staged Admin API writes.
|
|
@@ -2901,7 +2938,7 @@ baker avatars create --name "Marta" --subject "…" \
|
|
|
2901
2938
|
--source-image "<imageId1>,<imageId2>" --likeness-confirmed
|
|
2902
2939
|
```
|
|
2903
2940
|
|
|
2904
|
-
Optional: `--handle`, `--speech`, `--motion`, `--setting`, `--
|
|
2941
|
+
Optional: `--handle`, `--speech`, `--motion`, `--setting`, `--voice-id`, `--voice-description`. Returns `{ avatarId, handle, likeness, status }`.
|
|
2905
2942
|
|
|
2906
2943
|
#### `baker avatars update <handle>`
|
|
2907
2944
|
|
|
@@ -2910,7 +2947,7 @@ Edit an avatar. Profile flags are merged over the stored profile, so `--persona`
|
|
|
2910
2947
|
```bash
|
|
2911
2948
|
baker avatars update marta --wardrobe "charcoal blazer over a white tee"
|
|
2912
2949
|
baker avatars update marta --subject "woman in her early 50s, silver bob, …" --regenerate-sheet
|
|
2913
|
-
baker avatars update marta --new-handle marta-founder --
|
|
2950
|
+
baker avatars update marta --new-handle marta-founder --voice-id <voiceId>
|
|
2914
2951
|
```
|
|
2915
2952
|
|
|
2916
2953
|
#### `baker avatars delete <handle>`
|
|
@@ -3405,7 +3442,7 @@ baker history list --category ads --full # ad writes with raw metadata
|
|
|
3405
3442
|
|
|
3406
3443
|
---
|
|
3407
3444
|
|
|
3408
|
-
### `baker hubspot forms list | view | submissions` · `baker hubspot workflows list | view` · `baker hubspot pipelines list` · `baker hubspot contacts summary | lookup` · `baker hubspot meetings list | view`
|
|
3445
|
+
### `baker hubspot forms list | view | submissions` · `baker hubspot workflows list | view` · `baker hubspot pipelines list` · `baker hubspot contacts summary | lookup | properties` · `baker hubspot meetings list | view`
|
|
3409
3446
|
|
|
3410
3447
|
Read-only view of the company's **connected HubSpot account** — the forms and meeting links (calendars) that live there, the leads those forms received, and the workflows, deal pipelines, and contact records a lead runs through afterwards. Fetched from HubSpot live on every call; nothing is cached and nothing is ever written back.
|
|
3411
3448
|
|
|
@@ -3426,6 +3463,7 @@ baker hubspot workflows view <workflowId> --full # + the values each branch com
|
|
|
3426
3463
|
baker hubspot pipelines list # where a lead lands, and the stages after it
|
|
3427
3464
|
baker hubspot contacts summary --days 30 # are recent leads being worked? (counts only)
|
|
3428
3465
|
baker hubspot contacts lookup lead@example.com # was this one lead picked up?
|
|
3466
|
+
baker hubspot contacts properties # what a HubSpot-read source can watch, with each value
|
|
3429
3467
|
baker hubspot meetings list # every calendar + whether booking redirects
|
|
3430
3468
|
baker hubspot meetings view <slug> # booking-form fields for one calendar
|
|
3431
3469
|
baker hubspot meetings view <slug> --as-node # the form.external blob for a flow node
|
|
@@ -3439,6 +3477,7 @@ baker hubspot meetings view <slug> --as-node # the form.external blob for a f
|
|
|
3439
3477
|
**`pipelines list`:** no flags; stages come back in the order they run in HubSpot.
|
|
3440
3478
|
**`contacts summary` flags:** `--days <1-365>` (default 30) over contact creation date. Returns counts only — total contacts, how many have an owner, and the lifecycle-stage split — never a name or an email. `truncated: true` means the page budget ran out, so the counts are floors.
|
|
3441
3479
|
**`contacts lookup`:** positional `<email>`; returns that contact's lifecycle stage, owner, the account (company) it was filed under, and associated deals with their stage. `dealsReadable: false` / `companyReadable: false` mean the connection does not cover deals or accounts, so an empty `deals` or a null `company` is unknown rather than absent. `found: false` means no contact with that email — the lead never reached the CRM, which is a finding rather than an error.
|
|
3480
|
+
**`contacts properties`:** no flags; returns every contact property with a closed list of values — `{ name, label, groupName, options: [{ value, label }] }` — the ones a HubSpot-read source can watch. Names are HubSpot's internal ones; hand them to `baker analytics sources --create-hubspot --property <name> --value <value>` as printed. No personal data.
|
|
3442
3481
|
**`meetings list` flags:** `--search <text>`, `--redirecting-only`.
|
|
3443
3482
|
**`meetings view`:** positional `<slug>` — the only reliable source for a calendar's booking fields (HubSpot omits `customParams` from the list endpoint, so `fieldCount: null` there means unknown, not zero); `--as-node` as above.
|
|
3444
3483
|
|
|
@@ -3602,7 +3641,6 @@ Reach for it when you are relaying the **client's** own words. When you are writ
|
|
|
3602
3641
|
|
|
3603
3642
|
```bash
|
|
3604
3643
|
baker studio animate "slow push in, model turns to camera and smiles" --image j57abc123def456ghi789
|
|
3605
|
-
baker studio animate "she looks to camera and says: \"Hola, soy Elena\"" --avatar elena --quality 720p --aspect-ratio 9:16
|
|
3606
3644
|
baker studio animate "handheld drift right, steam rising from the cup" \
|
|
3607
3645
|
--image ./out/hero.png --duration 6 --quality 1080p
|
|
3608
3646
|
baker studio animate "product rotates once on a turntable" --image j57abc…,j57def… --from references
|
|
@@ -3752,65 +3790,6 @@ baker canvas set-prompt my-canvas.json s0_start --text "Close-up, flushed and br
|
|
|
3752
3790
|
baker canvas set-prompt my-canvas.json s0_start --text-file ./hook-frame.txt
|
|
3753
3791
|
```
|
|
3754
3792
|
|
|
3755
|
-
#### The brand, the ending and the cast are drawn, not generated
|
|
3756
|
-
|
|
3757
|
-
`baker canvas scaffold-ad` renders everything with a known, fixed appearance locally,
|
|
3758
|
-
because a video model garbles a wordmark every time:
|
|
3759
|
-
|
|
3760
|
-
- **`brand.logo`** — the repo path to the mark. It is copied into the overlay composition
|
|
3761
|
-
and drawn as a real `<img>`: small top-left for the whole ad, large on the closing card.
|
|
3762
|
-
- **`end_card`** — on by default whenever the spec carries a brand. The last beat's PICTURE
|
|
3763
|
-
becomes a flat brand colour plate (rendered by ffmpeg, not generated), with the mark and
|
|
3764
|
-
the call to action over it. `end_card: false` keeps the footage; `end_card: { cta }` sets
|
|
3765
|
-
the words on the button, which otherwise default to the last line of the script.
|
|
3766
|
-
- **The brand fills itself.** When the spec leaves `brand.logo` or `brand.palette` empty,
|
|
3767
|
-
they are read from the workspace — `src/brand/BRAND.md` for the colours (in document
|
|
3768
|
-
order, page-furniture whites and blacks dropped) and `src/brand/logos/` for the mark
|
|
3769
|
-
(vector first, plain mark over a mono/white/icon variant). Pass them yourself only when
|
|
3770
|
-
the ad needs something other than the client's default.
|
|
3771
|
-
- **The spoken language is read off the script** when the spec does not declare it, so a
|
|
3772
|
-
Spanish ad is cast from Spanish voices without anyone having to say so.
|
|
3773
|
-
- **`sound`** (per beat) — one sound effect for that shot, rendered by ElevenLabs
|
|
3774
|
-
alongside the music bed. Most beats should not have one: it belongs where the picture
|
|
3775
|
-
shows a specific physical event (a drill, a door, rain on glass), not on a talking head
|
|
3776
|
-
or a calm lifestyle shot. A bed plus two well-placed effects beats eight.
|
|
3777
|
-
- **A real brand mark is required.** `scaffold-ad` refuses to build without one in
|
|
3778
|
-
`src/brand/logos/` (SVG or PNG): the mark is drawn on every frame and on the closing
|
|
3779
|
-
card, so a stand-in ships an ad wearing the wrong brand.
|
|
3780
|
-
- **An avatar with no pinned voice cannot speak on camera.** Their lines would be read by
|
|
3781
|
-
a separately cast voice, which is dubbing and looks like it.
|
|
3782
|
-
- **`voiceover: false`** — a music-led ad. No voice, no transcription: the `say` lines
|
|
3783
|
-
become on-screen text and are captioned straight from the script, so the words are
|
|
3784
|
-
exact. Give it `music` too.
|
|
3785
|
-
- **`--avatar <handle>`** — the same flag as on `studio generate` / `studio animate`, and the way to cast a person into an ad. Overrides `cast.avatar` in the spec.
|
|
3786
|
-
- **`cast.avatar`** — the handle of a cast Avatar (`baker avatars list`). The ad grounds
|
|
3787
|
-
every beat they appear in on that avatar's identity sheet and copies its subject
|
|
3788
|
-
description verbatim, so the ad shows the same face as the rest of the company's work.
|
|
3789
|
-
This is the whole journey: `baker avatars create` → `cast.avatar` in the spec → render.
|
|
3790
|
-
- **`cast.description`** — a person described in words, when there is no avatar.
|
|
3791
|
-
|
|
3792
|
-
Each of these also removes a generation: the closing beat costs no image and no clip.
|
|
3793
|
-
|
|
3794
|
-
#### The finished video is reviewed
|
|
3795
|
-
|
|
3796
|
-
When a run's output is a video, `baker canvas run` samples twelve frames across it and
|
|
3797
|
-
looks at them before reporting success. It reports, never blocks — the clips are already
|
|
3798
|
-
paid for by the time it runs — and the findings come back as `review` and `hints`:
|
|
3799
|
-
|
|
3800
|
-
- **Per frame:** anything that could not physically happen, or shows a job being done
|
|
3801
|
-
incorrectly, plus objects floating unattached in shot. Malformed hands and limbs are
|
|
3802
|
-
rolled up into one finding across the whole render rather than repeated per frame.
|
|
3803
|
-
- **Across the reel:** whether the cast stays the same people, whether a brand mark is
|
|
3804
|
-
visible anywhere, and whether the last frames close the ad or simply stop.
|
|
3805
|
-
|
|
3806
|
-
Each per-frame finding names the **scene file** that produced that second, so a defect is
|
|
3807
|
-
an edit rather than a hunt — and re-running re-renders only the scenes you changed.
|
|
3808
|
-
|
|
3809
|
-
It needs `GEMINI_API_KEY` (the Runtime receives it automatically). Without one the run
|
|
3810
|
-
says the video was **not** reviewed rather than reporting it clean, and a frame that
|
|
3811
|
-
could not be read is reported as unchecked for the same reason.
|
|
3812
|
-
|
|
3813
|
-
|
|
3814
3793
|
> Scaffolded canvases use **workspace-relative** composition/asset paths and are validated
|
|
3815
3794
|
> against the engine's hyperframe pre-render gate at `validate` time, so a broken overlay
|
|
3816
3795
|
> (or a moved/renamed workspace) is caught before any billed clip. The scaffold also adds
|
|
@@ -5976,18 +5955,7 @@ This CLI is designed for AI agent consumption. Key patterns:
|
|
|
5976
5955
|
|
|
5977
5956
|
- **0.217.0**: new family `baker avatars list|get|create|update|delete`, plus `--avatar <handle>` on `baker studio generate` and `baker studio animate` — company-scoped reusable presenters, typed from the shared `@baker/api` avatars contract. `--avatar` is the way to cast one: it grounds the render on the identity sheet, reuses the subject description verbatim, and records the avatar on the batch. Passing the sheet through `--reference` instead does none of those and leaves the batch unattributed, so the avatar can never show what it has appeared in; the route refuses a handle that is not `ready`, because generating a stranger is worse than an error. `get` returns the sheet URL and that description; every command's `hints` carry the next move (still building → re-check with `get`; changed `--subject` without `--regenerate-sheet` → the sheet is stale). `create` refuses `--source-image` without `--likeness-confirmed` and returns an `error.fix` telling the agent to ask the user for that permission, since only they can give it; an unknown handle returns an `error.fix` pointing at `baker avatars list`. Profile flags on `update` merge over the stored profile instead of replacing it.
|
|
5978
5957
|
|
|
5979
|
-
|
|
5980
|
-
- **0.251.0**: `--accent` on `baker avatars create|update` — the region an avatar's voice comes from, e.g. `"Spanish from Spain, Madrid accent"`. Its own field rather than a sentence inside `--speech`, because that is where it went to die: asked for "pace, register, accent", the agent wrote *"clear Spanish accent"* for a Madrid presenter, the video model read that as no accent, and the clip came out sounding like nowhere. A place can be checked; an adjective cannot. It reaches the render through `buildDeliveryDirection` as its own `ACCENT:` instruction, and `thinProfileHint` now names it first among the missing fields since it is the only one that fails silently — a clip with no accent still looks finished.
|
|
5981
|
-
- **0.252.0**: avatar voices removed. `--voice-id` / `--voice-description` are gone from `baker avatars create|update`, and an avatar no longer holds a voice at all — every clip is voiced by the video model as it renders, directed by the profile's `--accent`. The picker they replaced offered a voice, played its preview, and then shipped a clip that did not sound like it: the re-voice was speech-to-speech, which replaces timbre and keeps pronunciation, so the accent the user picked could never survive. Three other routes were built and judged on video before removing it — TTS + lip-sync (mouth), audio-driven avatar models (picture), and Seedance 2.5 driven by audio (refuses AI faces). Reasoning and the re-open condition: `docs/adr/0005-an-avatar-is-a-face-not-a-voice.md`.
|
|
5982
|
-
- **0.253.0**: the pre-render gate runs `hyperframes check` — lint, runtime, layout, motion and contrast in one browser session, sampled at transition seams — instead of the static `lint` + `inspect` pair, falling back to them when the installed binary predates `check`. The browser half is signal the parser could never produce: measured on HyperFrames' own `warm-grain` example, lint/runtime/layout/motion all reported `ok: true` while the composition rendered an almost entirely blank ten-second video, the only trace being six `GSAP target #a-roll not found` warnings — a tween pointed at an element that does not exist, so nothing it animates ever appears. Baker promotes that warning to blocking, alongside `sweep_static` ("the timeline never advanced", i.e. a still image billed as a video). Warnings are grouped by code with a count (one run returned 62, 53 of them identical) and the highest-value findings carry advice on what to do rather than only what was seen. Requires `hyperframes@0.8.17` in the sandbox, bumped from 0.7.5 in the same change. Reasoning: `docs/adr/0007-the-render-gate-runs-in-a-browser.md`.
|
|
5983
|
-
- **0.254.0**: the nested-composition smoke test now looks at the frames instead of trusting the exit code — exiting 0 is what a blank render does too. `hyperframes snapshot --describe` sends the captured frames to Gemini and writes back what is actually in them, and the credential Studio already uses for Gemini and Omni (`GOOGLE_GENERATIVE_AI_API_KEY`) is the `GEMINI_API_KEY` it reads; the backend now ships it to the sandbox by omission, so a deployment without one keeps the plain smoke test. Verified against a real render: the pass reported "the image is completely empty" and named the single visible element without being told what to look for. Frame verdicts are condensed into the node log, one line per frame — a file the agent would have to know to open is a file it does not read.
|
|
5984
|
-
- **0.263.0**: shot briefs refuse the frame that contradicts its own caption, and the vision pass reads the numbers back. Observed verbatim: a phone screen reading "$215.40 SAVED THIS MONTH" under a caption reading "Save over £900 a year" — wrong currency, and a figure that contradicts the claim it sits beneath, since 215.40 a month is 2,584 a year. The model invents a number whenever a screen or document is in shot, so the frames no longer carry legible text at all: the caption track is where an ad's numbers live. Same brief also pins each object's real-world FORM after a phone rendered with a second screen on its back, and names the market's currency for anything unavoidably visible. The frame-vision question now reads out every number and word inside the picture and says whether it contradicts the caption — the defect only exists between the two, so neither alone can see it.
|
|
5985
|
-
- **0.262.0**: `scaffold-ad` carries the brand. The spec takes `brand.palette` (hex tokens) and `brand.logo` (repo path to the mark), and emits them as the `style.palette` and `branding` the engine already reads to colour its brand plate and CTA. The palette dresses the plate and the type and is deliberately kept OUT of the shot briefs — hex in a frame prompt makes the model paint the whole scene that colour, which reads as a filter. Also takes `market`, since an ad that does not say where it is set is an ad the model sets wherever it likes.
|
|
5986
|
-
- **0.261.0**: four defects that reached a finished ad, fixed in `scaffold-ad` where they originate. Frames render on **`openai/gpt-image-2`**, pinned rather than inheriting the catalogue default (Gemini Flash — right for cheap high-volume work, wrong for the opening still a five-second clip is built from). Every shot brief now states the MARKET, inferred from the voice language when unstated: an ad for Spain came back full of British houses because nothing said where it was. Every shot brief also rules out the impossible — paper and screens are opaque, objects are at real-world scale — after a bill rendered with the text from its reverse showing through, and a solar panel five metres tall. And beat durations snap to lengths the video model can actually render: free-form durations were rounded down per clip, so 11 beats summed to 27.0s of picture against a 31.4s timeline and the 4.4s difference rendered as a black tail. The read rate was re-measured off a real Spanish ad (81 words in 26.48s = 3.06 w/s); the old 2.6 over-predicted every beat by 18%.
|
|
5987
|
-
- **0.260.0**: burned-in captions break where the script punctuates instead of every N words. The karaoke composition sliced the transcript into fixed groups, and a word count knows nothing about the sentence it is cutting — an ad the engine rendered showed cards reading "OTRA VEZ, EN" and "TU CASA GENERA", half-thoughts ending on a preposition. A card now closes when the clause does; the word count stays as a ceiling so a long clause is still split, and a linking word ("de", "al", "y") never ends a card because it belongs to what follows. Same rule the pre-render gate already checked for, now enforced where the cards are actually built.
|
|
5988
|
-
- **0.259.0**: `baker canvas scaffold-ad` is listed in `baker canvas --help` with a description, and the three video routes are separated in the skill. It had a usage line and no description row, so an agent reading help saw two scaffolders explained and a bare name — and three runs from the same prompt took three different wrong paths: hand-directing the flow, reproducing a competitor ad found via `winning-ads`, and hand-authoring a canvas. The routes now state their own boundary: `scaffold-ad` is the default for a new ad, `scaffold-video` only reproduces a video the USER supplied, and `video-flow.md` opens by saying so rather than calling itself the default eleven times.
|
|
5989
|
-
- **0.257.0**: the frame-vision pass now asks whether what is on screen could physically happen, not just whether the frame is empty. The defect that motivated it was a generated shot of a solar panel roughly five metres tall being lifted onto a roof by one person — well lit, on brief, perfectly legible, and impossible. That is what reads loudest as "AI-generated", and the previous question could not see it: nothing was missing, something present was the wrong size. Asked the new question, the pass called the rejected shot *"physically implausible due to the immense weight and surface area"* and the replacement *"consistent with what two people could reasonably maneuver"* — the same verdict a person reached, unprompted.
|
|
5990
|
-
- **0.256.0**: the pre-render gate reports caption cards that end mid-clause. The first cut of this check asked for consistency and got it the wrong way round — stripping terminal punctuation makes the cards agree and leaves them wrong, because "En solo nueve días tu casa" is not a line anyone wrote, it is a sentence halved by a word count. Captions split where the script punctuates; each card is then a clause carrying its own mark, grammatical, and consistent as a by-product. The last card is exempt, since a CTA legitimately ends bare. Reported, never rewritten — the copy belongs to whoever wrote it.
|
|
5958
|
+
## Publishing
|
|
5991
5959
|
|
|
5992
5960
|
### Auto-publish (CI)
|
|
5993
5961
|
|
|
@@ -35,10 +35,6 @@
|
|
|
35
35
|
|
|
36
36
|
.caption-group {
|
|
37
37
|
position: absolute;
|
|
38
|
-
/* Absolutely positioned with no width, a card sizes to its content and
|
|
39
|
-
runs off the 1080px frame rather than wrapping. Matches the container's
|
|
40
|
-
60px gutters, so a long clause becomes two lines instead of an overflow. */
|
|
41
|
-
max-width: 960px;
|
|
42
38
|
font-family: 'Arial Black', 'Helvetica Neue', sans-serif;
|
|
43
39
|
font-size: {{font_size}}px;
|
|
44
40
|
font-weight: 900;
|
|
@@ -84,27 +80,10 @@
|
|
|
84
80
|
const container = document.getElementById('captions');
|
|
85
81
|
const tl = gsap.timeline({ paused: true });
|
|
86
82
|
|
|
87
|
-
// A card closes where the SCRIPT closes, not on a word count. Slicing every
|
|
88
|
-
// N words gave cards reading "OTRA VEZ, EN" — half a thought ending on a
|
|
89
|
-
// preposition. The count stays as a ceiling (a long clause still has to be
|
|
90
|
-
// split), and a linking word never ends a card: it belongs to what follows.
|
|
91
|
-
const CLOSES = /[.,;:!?\u2026]$/;
|
|
92
|
-
const CARRIES_ON = new Set([
|
|
93
|
-
'de','del','al','a','en','y','e','o','u','que','la','el','los','las','un','una',
|
|
94
|
-
'con','por','para','su','tu','mi','más','mas','the','of','to','and','in','for','your',
|
|
95
|
-
]);
|
|
96
|
-
const carriesOn = (w) => CARRIES_ON.has(String(w.text || '').replace(/[.,;:!?\u2026]$/, '').toLowerCase());
|
|
97
|
-
|
|
98
83
|
const groups = [];
|
|
99
|
-
let
|
|
100
|
-
|
|
101
|
-
current.push(word);
|
|
102
|
-
const last = current[current.length - 1];
|
|
103
|
-
const closes = CLOSES.test(String(last.text || '')) ||
|
|
104
|
-
(current.length >= WORDS_PER_GROUP && !carriesOn(last));
|
|
105
|
-
if (closes) { groups.push(current); current = []; }
|
|
84
|
+
for (let i = 0; i < transcript.length; i += WORDS_PER_GROUP) {
|
|
85
|
+
groups.push(transcript.slice(i, i + WORDS_PER_GROUP));
|
|
106
86
|
}
|
|
107
|
-
if (current.length > 0) groups.push(current);
|
|
108
87
|
|
|
109
88
|
groups.forEach((group, gi) => {
|
|
110
89
|
const el = document.createElement('div');
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
debugLogSetting
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-DZUVUGEP.js";
|
|
4
4
|
|
|
5
5
|
// src/debugLog.ts
|
|
6
6
|
import { appendFileSync, mkdirSync, renameSync, statSync } from "fs";
|
|
@@ -150,4 +150,4 @@ export {
|
|
|
150
150
|
readBodyForLog,
|
|
151
151
|
installStreamTaps
|
|
152
152
|
};
|
|
153
|
-
//# sourceMappingURL=chunk-
|
|
153
|
+
//# sourceMappingURL=chunk-2IB3IL4K.js.map
|
|
@@ -2,13 +2,13 @@ import {
|
|
|
2
2
|
handleConnectionError,
|
|
3
3
|
needsConnectionFix,
|
|
4
4
|
writeAdsJson
|
|
5
|
-
} from "./chunk-
|
|
5
|
+
} from "./chunk-EX2OIOVL.js";
|
|
6
6
|
import {
|
|
7
7
|
ApiError
|
|
8
|
-
} from "./chunk-
|
|
8
|
+
} from "./chunk-GU7IWEBC.js";
|
|
9
9
|
import {
|
|
10
10
|
getEnv
|
|
11
|
-
} from "./chunk-
|
|
11
|
+
} from "./chunk-DZUVUGEP.js";
|
|
12
12
|
|
|
13
13
|
// src/commands/ads/meta/shared.ts
|
|
14
14
|
var DAY_MS = 864e5;
|
|
@@ -108,4 +108,4 @@ export {
|
|
|
108
108
|
csvOrJson,
|
|
109
109
|
resolveEffectiveStatus
|
|
110
110
|
};
|
|
111
|
-
//# sourceMappingURL=chunk-
|
|
111
|
+
//# sourceMappingURL=chunk-D3HHUBIL.js.map
|
|
@@ -311,12 +311,6 @@ function requireChatId() {
|
|
|
311
311
|
function resolveChatId(chat) {
|
|
312
312
|
return typeof chat === "string" && chat.length > 0 ? chat : requireChatId();
|
|
313
313
|
}
|
|
314
|
-
function hasFrameVisionKey() {
|
|
315
|
-
return Boolean(frameVisionKey());
|
|
316
|
-
}
|
|
317
|
-
function frameVisionKey() {
|
|
318
|
-
return process.env.GEMINI_API_KEY;
|
|
319
|
-
}
|
|
320
314
|
function captureBudgetMs() {
|
|
321
315
|
const raw = Number(process.env.BAKER_CAPTURE_BUDGET_MS);
|
|
322
316
|
return Number.isFinite(raw) && raw > 0 ? raw : null;
|
|
@@ -346,11 +340,9 @@ export {
|
|
|
346
340
|
debugLogSetting,
|
|
347
341
|
requireChatId,
|
|
348
342
|
resolveChatId,
|
|
349
|
-
hasFrameVisionKey,
|
|
350
|
-
frameVisionKey,
|
|
351
343
|
captureBudgetMs,
|
|
352
344
|
captureProxyCredentials,
|
|
353
345
|
captureVimeoCookie,
|
|
354
346
|
childEnvWith
|
|
355
347
|
};
|
|
356
|
-
//# sourceMappingURL=chunk-
|
|
348
|
+
//# sourceMappingURL=chunk-DZUVUGEP.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../proxy/src/challenge.ts","../../proxy/src/tiers.ts","../../proxy/src/credentials.ts","../../proxy/src/escalate.ts","../../proxy/src/privateAddress.ts","../../proxy/src/publicAddress.ts","../../proxy/src/route.ts","../../proxy/src/ladder.ts","../src/env.ts"],"sourcesContent":["/**\n * Was that a page, or the wall in front of it?\n *\n * A bot filter answers 200. The body parses, it has a title, and every check\n * downstream — `response.ok`, the status, the content type — says the read\n * succeeded. So a challenge is the one block that cannot be seen from the\n * status line, and the only place it is visible is the bytes.\n *\n * This lives in `@baker/proxy` rather than beside any one caller because the\n * escalation ladder is only coherent if every surface agrees on what \"blocked\"\n * means: the rung that gets climbed is chosen from this answer, and a detector\n * that differs per caller would mean the same wall costs money on one surface\n * and is filed as content on another.\n *\n * Pure and dependency-free, so it stays reachable from Convex's V8 runtime\n * through the package barrel.\n */\n\n/**\n * Phrases that only appear on a block or challenge page.\n *\n * Deliberately specific — \"access denied\" alone would match a page *about*\n * access control, so each phrase is one a real marketing page has no reason to\n * use as its title.\n */\nconst CHALLENGE_PHRASES = [\n \"just a moment\",\n \"attention required\",\n \"verify you are human\",\n \"checking your browser\",\n \"enable javascript and cookies to continue\",\n \"unusual traffic\",\n \"access denied\",\n \"you have been blocked\",\n \"request unsuccessful\",\n \"are you a robot\",\n \"security check\",\n \"ddos protection\",\n \"captcha\",\n];\n\n/** Vendors whose block pages carry a fingerprint even when the title does not. */\nconst CHALLENGE_MARKERS = [\"cf-browser-verification\", \"cf_chl_\", \"px-captcha\", \"_incapsula_\", \"distil_r_captcha\"];\n\n/**\n * How long a response may be and still be judged by its *wording*.\n *\n * Phrases like \"captcha\" or \"access denied\" are ordinary English that a real\n * article can legitimately contain, so matching them anywhere would refuse\n * pages we read perfectly well. Length is the precision guard: a page that is\n * mostly content is content.\n */\nexport const CHALLENGE_LENGTH_CEILING = 2_000;\n\n/**\n * Whether these bytes are a bot wall rather than the thing that was asked for.\n *\n * Reads only what the caller already paid for. Nothing here issues a request —\n * asking the host again would deepen the very block this is detecting.\n *\n * The two lists are searched differently, and that asymmetry is the point.\n * Phrases are judged only on a short response, for the precision reason above.\n * Vendor markers are fingerprints nothing but the vendor emits, so they are\n * searched at any length — which is what makes a real interstitial detectable\n * at all. Cloudflare's is 5–15 KB of inlined script wrapped around one visible\n * sentence, so gating markers behind the same length ceiling as the phrases put\n * every genuine large wall in a dead band where neither list could reach it.\n */\nexport function isChallengeBody(content: string, title?: string | null): boolean {\n if (content.length <= CHALLENGE_LENGTH_CEILING) {\n const haystack = `${title ?? \"\"}\\n${content}`.toLowerCase();\n if (CHALLENGE_PHRASES.some((phrase) => haystack.includes(phrase))) return true;\n }\n\n const whole = content.toLowerCase();\n return CHALLENGE_MARKERS.some((marker) => whole.includes(marker));\n}\n","/**\n * The two Oxylabs products, and everything that differs between them.\n *\n * They are NOT credential-swappable. Endpoint, username prefix, geo token and\n * sticky-session mechanism all differ, so \"same URL, other username/password\"\n * fails — and it fails as a 407, which Chromium reports as a plain network\n * error. Before this module existed there was no code path that could tell that\n * apart from the site refusing us, so the failure would have been recorded\n * against the page rather than against our own configuration.\n *\n * That asymmetry is the reason this table exists in exactly one place. Every\n * caller builds its route through `buildProxyRoute`; nobody concatenates an\n * Oxylabs URL by hand.\n */\n\nexport type ProxyTier = \"datacenter\" | \"residential\";\n\nexport interface ProxyTierSpec {\n readonly tier: ProxyTier;\n readonly host: string;\n readonly port: number;\n /** Oxylabs requires this in front of the account name. The two differ. */\n readonly usernamePrefix: \"user-\" | \"customer-\";\n /** Country targeting token. `-country-` on datacenter, `-cc-` on residential. */\n readonly countryKey: \"-country-\" | \"-cc-\";\n /**\n * Sticky-session token, or null when the product has none.\n *\n * Datacenter does stickiness by *port* (8001 for IP #1, 8002 for #2), not by\n * username. We do not implement that: a capture is a single page load, so a\n * reusable IP buys nothing and the knob would only add a way to get it wrong.\n */\n readonly sessionKey: \"-sessid-\" | null;\n}\n\nexport const PROXY_TIERS: Readonly<Record<ProxyTier, ProxyTierSpec>> = {\n datacenter: {\n tier: \"datacenter\",\n host: \"dc.oxylabs.io\",\n port: 8000,\n usernamePrefix: \"user-\",\n countryKey: \"-country-\",\n sessionKey: null,\n },\n residential: {\n tier: \"residential\",\n host: \"pr.oxylabs.io\",\n port: 7777,\n usernamePrefix: \"customer-\",\n countryKey: \"-cc-\",\n sessionKey: \"-sessid-\",\n },\n};\n\n/**\n * Cheapest useful route first, and the ladder never skips a rung.\n *\n * Residential costs materially more than datacenter, so it is only ever reached\n * because datacenter was tried and refused — never as a first guess and never\n * speculatively.\n */\nexport const ESCALATION_ORDER = [\"datacenter\", \"residential\"] as const satisfies readonly ProxyTier[];\n\n/** Every prefix this module knows, used to spot a credential in the wrong slot. */\nexport const KNOWN_USERNAME_PREFIXES = [\"user-\", \"customer-\"] as const;\n","import type { ProxyTier } from \"./tiers.ts\";\nimport { ESCALATION_ORDER } from \"./tiers.ts\";\n\n/**\n * The four environment variable names, in one place.\n *\n * Convex holds these and hands them to the sandbox; the CLI reads them back\n * out. A typo on either side is a *silent* no-op — the capability appears to\n * ship, every fetch quietly takes the direct route, and nothing fails. That is\n * the worst failure mode available to this feature, so the names are a shared\n * constant with a contract test rather than eight string literals spread across\n * two packages.\n */\nexport const PROXY_ENV_VARS = {\n datacenter: { username: \"OXYLABS_DATACENTER_USERNAME\", password: \"OXYLABS_DATACENTER_PASSWORD\" },\n residential: { username: \"OXYLABS_RESIDENTIAL_USERNAME\", password: \"OXYLABS_RESIDENTIAL_PASSWORD\" },\n} as const satisfies Record<ProxyTier, { username: string; password: string }>;\n\nexport interface TierCredentials {\n readonly username: string;\n readonly password: string;\n}\n\nexport type ProxyCredentials = Partial<Readonly<Record<ProxyTier, TierCredentials>>>;\n\n/**\n * Read whichever tiers are fully configured.\n *\n * Takes the env record rather than reading `process.env` itself, so the whole\n * package stays pure and the Convex and CLI sides can be tested identically.\n *\n * Half a pair is not a credential. A username with no password would build a\n * route that authenticates as nobody, and Oxylabs answers that with the same\n * 407 as a wrong password — so it would look like a broken proxy rather than an\n * unconfigured one. Dropping it here means `configuredTiers` tells the truth.\n */\nexport function readProxyCredentials(env: Record<string, string | undefined>): ProxyCredentials {\n const credentials: { -readonly [K in ProxyTier]?: TierCredentials } = {};\n for (const tier of ESCALATION_ORDER) {\n const username = env[PROXY_ENV_VARS[tier].username]?.trim();\n const password = env[PROXY_ENV_VARS[tier].password]?.trim();\n if (username && password) credentials[tier] = { username, password };\n }\n return credentials;\n}\n\n/**\n * The inverse of `readProxyCredentials`: credentials back out as env vars.\n *\n * The writing side used to be a loop inside one Convex function, which is how\n * the ladder came to exist everywhere and work in one place. That loop ran only\n * for `baker landing inspiration scrape`, so every other CLI fetch surface in a\n * Session — `images download`, canvas `ingest`, the two scaffolders, yt-dlp —\n * read four variables nobody had set and went direct, silently, exactly as\n * `PROXY_ENV_VARS` warns.\n *\n * Pairing it with the reader here is the point: the round trip is a test, so the\n * two sides cannot drift into agreeing about three variables out of four.\n */\nexport function proxyCredentialEnvs(credentials: ProxyCredentials): Record<string, string> {\n const envs: Record<string, string> = {};\n for (const tier of ESCALATION_ORDER) {\n const held = credentials[tier];\n if (!held) continue;\n envs[PROXY_ENV_VARS[tier].username] = held.username;\n envs[PROXY_ENV_VARS[tier].password] = held.password;\n }\n return envs;\n}\n\n/**\n * Which tiers this process could actually reach, cheapest first.\n *\n * There is no ceiling to apply. A deployment holding both pairs may climb to\n * residential, full stop — the brake is the credential itself. Removing\n * `OXYLABS_RESIDENTIAL_PASSWORD` takes effect on the next restart, needs no\n * deploy, and withholds the tier by the same mechanism a ceiling would have,\n * so the valve that used to sit here only added a second way to say it. It was\n * also never wired into ad-dna's task definition, which is the highest-volume\n * egress in the repo — a control that could not be turned where it mattered\n * most.\n */\nexport function configuredTiers(credentials: ProxyCredentials): ProxyTier[] {\n return ESCALATION_ORDER.filter((tier) => credentials[tier] !== undefined);\n}\n\n/** Whether there is any proxy to escalate to at all. */\nexport function proxyEscalationConfigured(credentials: ProxyCredentials): boolean {\n return configuredTiers(credentials).length > 0;\n}\n","/**\n * When is a failure worth spending a more expensive route on?\n *\n * The ladder only ever climbs on evidence that *who we are* was the problem. A\n * page that 404s, a domain that does not resolve, a certificate that will not\n * negotiate and a site that timed out all fail identically from every IP on\n * earth, so retrying them through a metered exit buys a second identical\n * failure and a bill. That asymmetry — cheap to be wrong in one direction,\n * expensive in the other — is why this is a closed allow-list rather than\n * \"escalate unless we recognise the error\".\n *\n * Shared deliberately: the capture engine, the image fetchers and the ad-media\n * downloaders all have to agree on what \"blocked\" means, or the corpus and the\n * bill disagree about the same event.\n */\n\n/** What one attempt observed, normalised across Playwright and `fetch`. */\nexport interface BlockSignal {\n /** Main-document / response status, when there was one. */\n readonly status?: number | null;\n /** Chromium `net::` name or a Node error code, when the attempt threw one. */\n readonly netError?: string | null;\n /** A challenge body was detected behind an otherwise successful response. */\n readonly challenge?: boolean;\n /** The attempt ran out of time rather than being refused. */\n readonly timedOut?: boolean;\n}\n\n/** \"We don't like *you*\" — a different exit IP is a different you. */\nconst ESCALATABLE_STATUSES = new Set([403, 429, 451]);\n\n/**\n * A TCP reset on ClientHello is how a WAF null-routes a datacenter range.\n *\n * Today all three of these collapse into one \"site didn't respond\" message and\n * are filed non-retryable, which is precisely why this class of block has been\n * invisible: it looks identical to a site that is genuinely down.\n */\nconst ESCALATABLE_NET_ERRORS = new Set([\"ERR_CONNECTION_RESET\", \"ERR_CONNECTION_CLOSED\", \"ERR_EMPTY_RESPONSE\"]);\n\n/**\n * Failures that belong to our route, not to the site.\n *\n * A 407 cannot come from an origin server — only something speaking proxy\n * produces one — so this needs no \"was this attempt proxied\" flag to be safe.\n */\nconst PROXY_NET_ERRORS = new Set([\n \"ERR_TUNNEL_CONNECTION_FAILED\",\n \"ERR_PROXY_CONNECTION_FAILED\",\n \"ERR_PROXY_AUTH_REQUESTED\",\n \"ERR_PROXY_CERTIFICATE_INVALID\",\n \"ERR_UNEXPECTED_PROXY_AUTH\",\n \"ERR_MANDATORY_PROXY_CONFIGURATION_FAILED\",\n \"ERR_HTTPS_PROXY_TUNNEL_RESPONSE_REDIRECT\",\n]);\n\nexport function isProxyFailure(signal: BlockSignal): boolean {\n if (signal.status === 407) return true;\n return signal.netError ? PROXY_NET_ERRORS.has(signal.netError) : false;\n}\n\nexport function shouldEscalate(signal: BlockSignal): boolean {\n // Our own broken route never advances the ladder — it degrades it. Climbing\n // here would spend a more expensive rung reproducing our misconfiguration.\n if (isProxyFailure(signal)) return false;\n if (signal.timedOut) return false;\n if (signal.challenge) return true;\n if (signal.status != null && ESCALATABLE_STATUSES.has(signal.status)) return true;\n return signal.netError ? ESCALATABLE_NET_ERRORS.has(signal.netError) : false;\n}\n","/**\n * The same question as `refuseNonPublicUrl`, asked of an address instead of a name.\n *\n * A URL carries a hostname, and a hostname is not an address. `localtest.me`\n * and `127.0.0.1.nip.io` are public names, with public DNS records, that\n * resolve to `127.0.0.1` — and nothing stops an attacker pointing a domain they\n * own at `169.254.169.254`. Proven against a real loopback server: the hostname\n * guard passed `http://localtest.me:4321/secret.txt` straight through, the bytes\n * were downloaded, and only the image parser complained afterwards.\n *\n * IPv6 is parsed rather than pattern-matched, and that is the second lesson\n * here. The first version tested prefixes with regexes and unwrapped an\n * IPv4-mapped address only when it still had a dot in it — but a URL parser\n * normalises `::ffff:127.0.0.1` to `::ffff:7f00:1`, so the dot is gone by the\n * time anyone looks. `[::ffff:169.254.169.254]` therefore read as public and the\n * ladder returned the metadata body with `outcome: \"ok\"`. One address has many\n * spellings; comparing spellings is the wrong operation.\n *\n * Anything unparseable is treated as private. This is a safety gate, and the\n * only safe reading of \"I do not know what this is\" is \"do not fetch it\".\n */\n\n/** Every IPv4 range that is not routable on the public internet. */\nfunction isPrivateIpv4Octets(octets: readonly number[]): boolean {\n const [a, b] = octets as [number, number, number, number];\n if (a === 0) return true; // \"this network\" — 0.0.0.0/8\n if (a === 10) return true; // private\n if (a === 127) return true; // loopback\n if (a === 169 && b === 254) return true; // link-local, and the cloud metadata address\n if (a === 172 && b >= 16 && b <= 31) return true; // private /12 — NOT all of 172.*\n if (a === 192 && b === 168) return true; // private\n if (a === 100 && b >= 64 && b <= 127) return true; // carrier-grade NAT\n if (a >= 224) return true; // multicast and reserved\n return false;\n}\n\n/** Dotted-quad to octets, or `null` when it is not one. */\nfunction parseIpv4(value: string): number[] | null {\n const parts = value.split(\".\");\n if (parts.length !== 4) return null;\n const octets = parts.map((part) => (/^\\d{1,3}$/.test(part) ? Number(part) : Number.NaN));\n if (octets.some((octet) => !Number.isInteger(octet) || octet < 0 || octet > 255)) return null;\n return octets;\n}\n\n/**\n * One side of a `::` to its 16-bit groups, or `null` if any part is not one.\n *\n * A trailing dotted-quad stands for the final two groups and is only legal in\n * last position — that is the spelling `::ffff:127.0.0.1` uses before a URL\n * parser rewrites it to `::ffff:7f00:1`.\n */\nfunction expandGroups(part: string): number[] | null {\n if (part === \"\") return [];\n const chunks = part.split(\":\");\n const groups: number[] = [];\n for (let index = 0; index < chunks.length; index++) {\n const chunk = chunks[index] as string;\n if (chunk.includes(\".\")) {\n if (index !== chunks.length - 1) return null;\n const octets = parseIpv4(chunk);\n if (!octets) return null;\n groups.push(((octets[0] as number) << 8) | (octets[1] as number));\n groups.push(((octets[2] as number) << 8) | (octets[3] as number));\n continue;\n }\n if (!/^[0-9a-f]{1,4}$/.test(chunk)) return null;\n groups.push(Number.parseInt(chunk, 16));\n }\n return groups;\n}\n\n/**\n * IPv6 to its eight 16-bit groups, or `null` when it is not one.\n *\n * Handles the `::` elision and a trailing dotted-quad, which together are why\n * one address has so many spellings.\n */\nfunction parseIpv6(value: string): number[] | null {\n const halves = value.split(\"::\");\n if (halves.length > 2) return null;\n\n const head = expandGroups(halves[0] as string);\n const tail = halves.length === 2 ? expandGroups(halves[1] as string) : [];\n if (head === null || tail === null) return null;\n\n if (halves.length === 1) return head.length === 8 ? head : null;\n const missing = 8 - head.length - tail.length;\n if (missing < 1) return null;\n return [...head, ...new Array<number>(missing).fill(0), ...tail];\n}\n\nexport function isPrivateAddress(ip: string): boolean {\n const address = ip.trim().toLowerCase();\n if (!address) return true;\n\n const v4 = parseIpv4(address);\n if (v4) return isPrivateIpv4Octets(v4);\n\n const v6 = parseIpv6(address);\n // Not an address we can read. A safety gate may not guess.\n if (!v6) return true;\n\n const [g0, g1, g2, g3, g4, g5, g6, g7] = v6 as [number, number, number, number, number, number, number, number];\n\n // `::` (unspecified) and `::1` (loopback).\n if (g0 === 0 && g1 === 0 && g2 === 0 && g3 === 0 && g4 === 0 && g5 === 0 && g6 === 0) return true;\n\n // IPv4-mapped (`::ffff:a.b.c.d`) and IPv4-translated (`::ffff:0:a.b.c.d`).\n // The whole point: by the time this is read the tail is hex, not dotted.\n const mappedV4 = g0 === 0 && g1 === 0 && g2 === 0 && g3 === 0;\n if (mappedV4 && g4 === 0 && g5 === 0xffff) {\n return isPrivateIpv4Octets([g6 >> 8, g6 & 0xff, g7 >> 8, g7 & 0xff]);\n }\n if (mappedV4 && g4 === 0xffff && g5 === 0) {\n return isPrivateIpv4Octets([g6 >> 8, g6 & 0xff, g7 >> 8, g7 & 0xff]);\n }\n\n if ((g0 & 0xfe00) === 0xfc00) return true; // fc00::/7 unique-local\n if ((g0 & 0xffc0) === 0xfe80) return true; // fe80::/10 link-local\n return false;\n}\n","/**\n * Addresses that must never be fetched, and must never be proxied.\n *\n * Lifted from `convex/landingLibrary/utils.ts`, where these three regexes were\n * the only copy in the repo — reachable from Convex and from nowhere else. That\n * was survivable while the capture ran one hop after the check. It is not\n * survivable now: the CLI can be invoked directly, two Convex paths enqueue\n * without re-validating, and a proxy adds a rung where \"which network am I on\"\n * stops being rhetorical.\n *\n * Two distinct reasons live here, and both point the same way:\n *\n * - **There is nothing to read.** A private address only resolves from inside\n * the network asking, so a capture of one produces a screenshot of an error.\n * - **There is something to read, and we must not.** `169.254.169.254` is the\n * cloud metadata endpoint that every capture runner has a route to.\n *\n * The rule for the proxy is *refuse*, not *bypass*. Bypassing would leave a\n * request that still happens, just unproxied; refusing means there is no\n * request to route. That is a stronger guarantee and a simpler one to check.\n */\n\nimport { isPrivateAddress } from \"./privateAddress.ts\";\n\n/**\n * Names — not addresses. Anything that looks like an address goes to\n * `isPrivateAddress`, which parses it instead of matching its spelling.\n */\nconst PRIVATE_HOST = /^(localhost|.*\\.localhost|.*\\.local)$/;\n/** Where a Session's own preview is served while the page is being built. */\nconst SANDBOX_HOST = /\\.e2b\\.(app|dev)$/;\n\n/**\n * A bracketed IPv6 literal, unwrapped.\n *\n * `new URL(\"http://[::1]/\").hostname` keeps the brackets, so a hostname check\n * that does not strip them is comparing against a string no address will ever\n * equal. The previous version tried to absorb that with `\\[?::1\\]?` and caught\n * exactly one address out of the whole of IPv6 — `[::ffff:169.254.169.254]`\n * sailed through and the ladder returned the metadata body with `outcome: \"ok\"`.\n */\nfunction unwrapIpLiteral(host: string): string | null {\n if (host.startsWith(\"[\") && host.endsWith(\"]\")) return host.slice(1, -1);\n // A bare v4 literal, or a v6 one that arrived without brackets.\n if (/^[0-9.]+$/.test(host) || host.includes(\":\")) return host;\n return null;\n}\n\nexport function isPrivateHostname(hostname: string): boolean {\n const host = hostname.trim().toLowerCase();\n if (!host) return true;\n if (PRIVATE_HOST.test(host) || SANDBOX_HOST.test(host)) return true;\n\n // Every address form — v4, v6, bracketed, mapped, elided — is decided by one\n // parser. Spelling an address in a regex is what let four of them through.\n const literal = unwrapIpLiteral(host);\n return literal === null ? false : isPrivateAddress(literal);\n}\n\n/**\n * The same rule again, in the only notation a browser's proxy settings speak.\n *\n * Chromium — and so Playwright and `agent-browser` — matches a bypass entry\n * against the hostname and understands one wildcard and no CIDR at all. The\n * ranges above therefore cannot be handed over as regexes; `172.16/12` has to\n * become sixteen entries, and there is no way to express \"any private address\".\n *\n * That makes this a second copy of one fact, which is worth stating plainly:\n * the day it disagrees with `isPrivateHostname` is the day a Session's own\n * preview at `localhost:4321` goes out through a metered exit and comes back\n * refused. Its test pins the correspondence.\n *\n * Note the direction reverses here, and deliberately. Everywhere else the rule\n * for a private address is *refuse*, because a request that still happens is\n * not a guarantee. A browser the agent drives has to reach `localhost:4321` —\n * that is its main job — so here the private address is the legitimate traffic\n * and the list says \"go direct\", not \"do not go\".\n */\nexport const BROWSER_PROXY_BYPASS = [\n \"localhost\",\n \"*.localhost\",\n \"127.0.0.1\",\n \"0.0.0.0\",\n \"::1\",\n \"*.local\",\n // A Session's own preview, which is served from the sandbox's public host —\n // public in DNS, ours in every sense that matters here.\n \"*.e2b.app\",\n \"*.e2b.dev\",\n \"10.*\",\n \"192.168.*\",\n \"169.254.*\",\n ...Array.from({ length: 16 }, (_, i) => `172.${16 + i}.*`),\n].join(\",\");\n\nexport type NonPublicReason = \"not_public\" | \"unparseable\";\n\n/**\n * `null` when the URL is safe to fetch; a reason when it must be refused.\n *\n * Anything that is not plain http(s) is refused too. A capture is handed URLs\n * from agents and from stored rows, and `file:` reaching a browser we launched\n * is a local file read wearing a URL.\n */\nexport function refuseNonPublicUrl(url: string): NonPublicReason | null {\n let parsed: URL;\n try {\n parsed = new URL(url);\n } catch {\n return \"unparseable\";\n }\n if (parsed.protocol !== \"http:\" && parsed.protocol !== \"https:\") return \"not_public\";\n return isPrivateHostname(parsed.hostname) ? \"not_public\" : null;\n}\n","import type { ProxyCredentials } from \"./credentials.ts\";\nimport type { ProxyTier, ProxyTierSpec } from \"./tiers.ts\";\nimport { KNOWN_USERNAME_PREFIXES, PROXY_TIERS } from \"./tiers.ts\";\n\n/**\n * How one attempt reaches the internet.\n *\n * Credentials stay in separate fields instead of being embedded in the URL:\n * Playwright's `proxy` option wants them that way, undici's `ProxyAgent` takes\n * them as a header we build once, and — the real reason — a password that never\n * exists as a substring of a URL cannot be leaked by anything that logs a URL.\n */\nexport type ProxyRoute =\n | { readonly kind: \"direct\" }\n | {\n readonly kind: \"proxy\";\n readonly tier: ProxyTier;\n /** e.g. `http://dc.oxylabs.io:8000` */\n readonly server: string;\n readonly username: string;\n readonly password: string;\n };\n\nexport const DIRECT_ROUTE: ProxyRoute = { kind: \"direct\" };\n\nexport interface RouteOptions {\n /** ISO-3166 alpha-2, upper-cased here. Omit for \"wherever\" — the default. */\n readonly country?: string;\n /** Sticky-session id. Ignored by a tier whose product has no session syntax. */\n readonly session?: string;\n}\n\nexport type RouteProblem = \"missing\" | \"wrong_prefix\";\n\nexport type RouteResult = { ok: true; route: ProxyRoute } | { ok: false; problem: RouteProblem };\n\n/**\n * Put the account name into the shape this specific product expects.\n *\n * Three cases, and the third is the one worth being strict about:\n *\n * 1. Bare (`acme`) — someone pasted the account name. Prepend the prefix.\n * 2. Already correct (`user-acme` in the datacenter slot) — someone pasted the\n * full Oxylabs username. Leave it alone.\n * 3. Carrying the OTHER product's prefix (`customer-acme` in the datacenter\n * slot) — refuse.\n *\n * Case 3 must not be \"helpfully\" rewritten. A `customer-` value in the\n * datacenter slot means the residential credentials were pasted into the wrong\n * variable, and rewriting the prefix would authenticate a residential account\n * against the datacenter endpoint. That either 407s — wasting the cheap rung\n * for no reason — or it works, and bills residential rates from the tier whose\n * entire purpose is to be the cheap one. Refusing is the only answer that\n * cannot silently cost money.\n */\nexport function proxyUsername(\n spec: ProxyTierSpec,\n rawUsername: string,\n options?: RouteOptions,\n): { ok: true; username: string } | { ok: false; problem: \"wrong_prefix\" } {\n const raw = rawUsername.trim();\n const foreignPrefix = KNOWN_USERNAME_PREFIXES.find(\n (prefix) => prefix !== spec.usernamePrefix && raw.startsWith(prefix),\n );\n if (foreignPrefix) return { ok: false, problem: \"wrong_prefix\" };\n\n let username = raw.startsWith(spec.usernamePrefix) ? raw : `${spec.usernamePrefix}${raw}`;\n // Geo before session — Oxylabs reads the username left to right and rejects\n // the pair in the other order.\n if (options?.country) username += `${spec.countryKey}${options.country.toUpperCase()}`;\n if (options?.session && spec.sessionKey) username += `${spec.sessionKey}${options.session}`;\n return { ok: true, username };\n}\n\n/** Build the route for one tier, or say why there isn't one. */\nexport function buildProxyRoute(tier: ProxyTier, credentials: ProxyCredentials, options?: RouteOptions): RouteResult {\n const held = credentials[tier];\n if (!held) return { ok: false, problem: \"missing\" };\n\n const spec = PROXY_TIERS[tier];\n const username = proxyUsername(spec, held.username, options);\n if (!username.ok) return username;\n\n return {\n ok: true,\n route: {\n kind: \"proxy\",\n tier,\n server: `http://${spec.host}:${spec.port}`,\n username: username.username,\n password: held.password,\n },\n };\n}\n","import type { ProxyCredentials } from \"./credentials.ts\";\nimport { configuredTiers } from \"./credentials.ts\";\nimport { refuseNonPublicUrl } from \"./publicAddress.ts\";\nimport type { ProxyRoute, RouteOptions } from \"./route.ts\";\nimport { buildProxyRoute, DIRECT_ROUTE } from \"./route.ts\";\n\n/**\n * Every route this URL is allowed to be attempted on, in the order to try them.\n *\n * Direct is always first and always present: an unblocked page must cost\n * nothing, and a deployment with no credentials has to behave exactly as it did\n * before any of this existed.\n *\n * A tier whose credentials are malformed is *skipped*, not fatal. One\n * mis-pasted variable should cost the use of that rung, not the whole ladder —\n * the alternative is that a typo in the cheap tier silently disables the\n * expensive one that would have worked.\n */\nexport function plannedRoutes(url: string, credentials: ProxyCredentials, options?: RouteOptions): ProxyRoute[] {\n // A private address is refused upstream; if one reaches here anyway it must\n // not become a billed request, and there is no exit node on earth from which\n // `localhost` means us.\n if (refuseNonPublicUrl(url) !== null) return [DIRECT_ROUTE];\n\n const routes: ProxyRoute[] = [DIRECT_ROUTE];\n for (const tier of configuredTiers(credentials)) {\n const built = buildProxyRoute(tier, credentials, options);\n if (built.ok) routes.push(built.route);\n }\n return routes;\n}\n","import type { ProxyCredentials } from \"@baker/proxy\";\nimport { readProxyCredentials } from \"@baker/proxy\";\nimport { createEnv } from \"@t3-oss/env-core\";\nimport { z } from \"zod\";\n\ntype Env = {\n BAKER_API_KEY: string;\n BAKER_API_URL: string;\n BAKER_CHAT_ID?: string;\n BAKER_ACTING_USER_ID?: string;\n BAKER_GOOGLE_ADS_CUSTOMER_ID?: string;\n BAKER_GA4_PROPERTY_ID?: string;\n BAKER_GSC_SITE_URL?: string;\n BAKER_X_ADS_ACCOUNT_ID?: string;\n BAKER_META_AD_ACCOUNT_ID?: string;\n BAKER_LINKEDIN_AD_ACCOUNT_ID?: string;\n};\n\nlet cached: Env | undefined;\n\nexport function getEnv(): Env {\n if (!cached) {\n cached = createEnv({\n server: {\n BAKER_API_KEY: z.string().startsWith(\"bk_\", \"API key must start with 'bk_'\"),\n BAKER_API_URL: z.url(\"BAKER_API_URL must be a valid URL\"),\n BAKER_CHAT_ID: z.string().optional(),\n BAKER_ACTING_USER_ID: z.string().optional(),\n BAKER_GOOGLE_ADS_CUSTOMER_ID: z\n .string()\n .regex(/^\\d{10}$/)\n .optional(),\n BAKER_GA4_PROPERTY_ID: z.string().optional(),\n BAKER_GSC_SITE_URL: z.string().optional(),\n BAKER_X_ADS_ACCOUNT_ID: z\n .string()\n .regex(/^[a-z0-9]+$/, \"X Ads account ID must be a base36 string\")\n .optional(),\n BAKER_META_AD_ACCOUNT_ID: z.string().optional(),\n BAKER_LINKEDIN_AD_ACCOUNT_ID: z\n .string()\n .regex(/^\\d+$/, \"LinkedIn ad account ID must be the numeric portion of urn:li:sponsoredAccount:N\")\n .optional(),\n },\n runtimeEnv: process.env,\n });\n }\n return cached;\n}\n\n// Read directly (not via the validated schema): debug logging must work even when\n// the required BAKER_API_KEY/BAKER_API_URL are missing or malformed.\nexport function debugLogSetting(): string | undefined {\n const raw = process.env.BAKER_DEBUG_LOG?.trim();\n return raw ? raw : undefined;\n}\n\nexport function requireChatId(): string {\n const env = getEnv();\n if (!env.BAKER_CHAT_ID) {\n throw new Error(\n \"BAKER_CHAT_ID is not set. This command stages changes against a chat — run it from a chat-attached environment.\",\n );\n }\n return env.BAKER_CHAT_ID;\n}\n\n/**\n * Which chat a *read* addresses. A `--chat` value names another chat in the same company — how an\n * earlier chat's staged changes are recovered verbatim instead of rebuilt from a summary — and with\n * no flag it is this session's own chat.\n *\n * Read paths only. Staging, amending and discarding keep calling `requireChatId`, so one session can\n * never edit another's draft; the backend enforces the same thing twice over, since every mutating\n * internal requires the draft be `active` and every route checks the chat belongs to the caller's\n * company.\n */\nexport function resolveChatId(chat?: unknown): string {\n return typeof chat === \"string\" && chat.length > 0 ? chat : requireChatId();\n}\n\n/**\n * Whether the frame-vision pass can run at all.\n *\n * `hyperframes snapshot --describe` reads `GEMINI_API_KEY`, and it is the only\n * check that can tell a rendered frame apart from an empty one. The backend\n * ships the credential Studio already uses for Gemini, by omission — a\n * deployment without one simply gets the plain smoke test.\n *\n * Kept out of `getEnv()` for the same reason as the capture budget: the render\n * engine also runs where none of the chat-attached variables exist.\n */\nexport function hasFrameVisionKey(): boolean {\n return Boolean(frameVisionKey());\n}\n\n/** The credential itself, for the pass that calls the model directly. */\nexport function frameVisionKey(): string | undefined {\n return process.env.GEMINI_API_KEY;\n}\n\n/**\n * Wall clock the whole capture may spend, when the caller bounded it.\n *\n * Read from the environment rather than taken as a flag, deliberately. A\n * `--budget-ms` argument would land in the command schema, and the schema is\n * what the agent reads — one question away from \"what is the budget for?\", and\n * from there the escalation ladder stops being invisible to it.\n *\n * Kept out of `getEnv()` because that validator is for a chat-attached run; the\n * capture engine also runs where none of those variables exist.\n */\nexport function captureBudgetMs(): number | null {\n const raw = Number(process.env.BAKER_CAPTURE_BUDGET_MS);\n return Number.isFinite(raw) && raw > 0 ? raw : null;\n}\n\n/**\n * Egress-proxy credentials for a capture, or none at all.\n *\n * These arrive as sandbox *command* env, never as arguments — a credential in\n * argv is readable by any `ps` sharing the sandbox. With none set,\n * `plannedRoutes` yields the direct route alone and a capture behaves exactly\n * as it did before the ladder existed.\n *\n * Whichever pairs arrive are the rungs this process may climb; there is nothing\n * to filter. A tier the deployment withholds is simply absent from this env, so\n * a ceiling read on this side could only ever disagree with the one place that\n * decides.\n */\nexport function captureProxyCredentials(): ProxyCredentials {\n return readProxyCredentials(process.env);\n}\n\n/**\n * The Vimeo session cookie the deployment supplied, or undefined.\n *\n * Absent is the normal, supported state: a deployment that has not configured\n * one gets the same \"download it and upload the file\" refusal Vimeo gave before\n * this existed. Read here with the other env, so nothing else reaches into\n * `process.env` for it.\n */\nexport function captureVimeoCookie(): string | undefined {\n const raw = process.env.VIMEO_COOKIE?.trim();\n return raw ? raw : undefined;\n}\n\n/**\n * This process's environment plus `extra`, for handing to a child.\n *\n * Lives here because `process.env` reads belong in this file — but it earns its\n * place beyond the lint rule: `spawn`'s `env` *replaces* the environment rather\n * than extending it, so a caller that passes only its additions silently strips\n * `PATH` and the child fails to start for a reason that looks nothing like the\n * cause.\n */\nexport function childEnvWith(extra: Record<string, string>): NodeJS.ProcessEnv {\n return { ...process.env, ...extra };\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../proxy/src/challenge.ts","../../proxy/src/tiers.ts","../../proxy/src/credentials.ts","../../proxy/src/escalate.ts","../../proxy/src/privateAddress.ts","../../proxy/src/publicAddress.ts","../../proxy/src/route.ts","../../proxy/src/ladder.ts","../src/env.ts"],"sourcesContent":["/**\n * Was that a page, or the wall in front of it?\n *\n * A bot filter answers 200. The body parses, it has a title, and every check\n * downstream — `response.ok`, the status, the content type — says the read\n * succeeded. So a challenge is the one block that cannot be seen from the\n * status line, and the only place it is visible is the bytes.\n *\n * This lives in `@baker/proxy` rather than beside any one caller because the\n * escalation ladder is only coherent if every surface agrees on what \"blocked\"\n * means: the rung that gets climbed is chosen from this answer, and a detector\n * that differs per caller would mean the same wall costs money on one surface\n * and is filed as content on another.\n *\n * Pure and dependency-free, so it stays reachable from Convex's V8 runtime\n * through the package barrel.\n */\n\n/**\n * Phrases that only appear on a block or challenge page.\n *\n * Deliberately specific — \"access denied\" alone would match a page *about*\n * access control, so each phrase is one a real marketing page has no reason to\n * use as its title.\n */\nconst CHALLENGE_PHRASES = [\n \"just a moment\",\n \"attention required\",\n \"verify you are human\",\n \"checking your browser\",\n \"enable javascript and cookies to continue\",\n \"unusual traffic\",\n \"access denied\",\n \"you have been blocked\",\n \"request unsuccessful\",\n \"are you a robot\",\n \"security check\",\n \"ddos protection\",\n \"captcha\",\n];\n\n/** Vendors whose block pages carry a fingerprint even when the title does not. */\nconst CHALLENGE_MARKERS = [\"cf-browser-verification\", \"cf_chl_\", \"px-captcha\", \"_incapsula_\", \"distil_r_captcha\"];\n\n/**\n * How long a response may be and still be judged by its *wording*.\n *\n * Phrases like \"captcha\" or \"access denied\" are ordinary English that a real\n * article can legitimately contain, so matching them anywhere would refuse\n * pages we read perfectly well. Length is the precision guard: a page that is\n * mostly content is content.\n */\nexport const CHALLENGE_LENGTH_CEILING = 2_000;\n\n/**\n * Whether these bytes are a bot wall rather than the thing that was asked for.\n *\n * Reads only what the caller already paid for. Nothing here issues a request —\n * asking the host again would deepen the very block this is detecting.\n *\n * The two lists are searched differently, and that asymmetry is the point.\n * Phrases are judged only on a short response, for the precision reason above.\n * Vendor markers are fingerprints nothing but the vendor emits, so they are\n * searched at any length — which is what makes a real interstitial detectable\n * at all. Cloudflare's is 5–15 KB of inlined script wrapped around one visible\n * sentence, so gating markers behind the same length ceiling as the phrases put\n * every genuine large wall in a dead band where neither list could reach it.\n */\nexport function isChallengeBody(content: string, title?: string | null): boolean {\n if (content.length <= CHALLENGE_LENGTH_CEILING) {\n const haystack = `${title ?? \"\"}\\n${content}`.toLowerCase();\n if (CHALLENGE_PHRASES.some((phrase) => haystack.includes(phrase))) return true;\n }\n\n const whole = content.toLowerCase();\n return CHALLENGE_MARKERS.some((marker) => whole.includes(marker));\n}\n","/**\n * The two Oxylabs products, and everything that differs between them.\n *\n * They are NOT credential-swappable. Endpoint, username prefix, geo token and\n * sticky-session mechanism all differ, so \"same URL, other username/password\"\n * fails — and it fails as a 407, which Chromium reports as a plain network\n * error. Before this module existed there was no code path that could tell that\n * apart from the site refusing us, so the failure would have been recorded\n * against the page rather than against our own configuration.\n *\n * That asymmetry is the reason this table exists in exactly one place. Every\n * caller builds its route through `buildProxyRoute`; nobody concatenates an\n * Oxylabs URL by hand.\n */\n\nexport type ProxyTier = \"datacenter\" | \"residential\";\n\nexport interface ProxyTierSpec {\n readonly tier: ProxyTier;\n readonly host: string;\n readonly port: number;\n /** Oxylabs requires this in front of the account name. The two differ. */\n readonly usernamePrefix: \"user-\" | \"customer-\";\n /** Country targeting token. `-country-` on datacenter, `-cc-` on residential. */\n readonly countryKey: \"-country-\" | \"-cc-\";\n /**\n * Sticky-session token, or null when the product has none.\n *\n * Datacenter does stickiness by *port* (8001 for IP #1, 8002 for #2), not by\n * username. We do not implement that: a capture is a single page load, so a\n * reusable IP buys nothing and the knob would only add a way to get it wrong.\n */\n readonly sessionKey: \"-sessid-\" | null;\n}\n\nexport const PROXY_TIERS: Readonly<Record<ProxyTier, ProxyTierSpec>> = {\n datacenter: {\n tier: \"datacenter\",\n host: \"dc.oxylabs.io\",\n port: 8000,\n usernamePrefix: \"user-\",\n countryKey: \"-country-\",\n sessionKey: null,\n },\n residential: {\n tier: \"residential\",\n host: \"pr.oxylabs.io\",\n port: 7777,\n usernamePrefix: \"customer-\",\n countryKey: \"-cc-\",\n sessionKey: \"-sessid-\",\n },\n};\n\n/**\n * Cheapest useful route first, and the ladder never skips a rung.\n *\n * Residential costs materially more than datacenter, so it is only ever reached\n * because datacenter was tried and refused — never as a first guess and never\n * speculatively.\n */\nexport const ESCALATION_ORDER = [\"datacenter\", \"residential\"] as const satisfies readonly ProxyTier[];\n\n/** Every prefix this module knows, used to spot a credential in the wrong slot. */\nexport const KNOWN_USERNAME_PREFIXES = [\"user-\", \"customer-\"] as const;\n","import type { ProxyTier } from \"./tiers.ts\";\nimport { ESCALATION_ORDER } from \"./tiers.ts\";\n\n/**\n * The four environment variable names, in one place.\n *\n * Convex holds these and hands them to the sandbox; the CLI reads them back\n * out. A typo on either side is a *silent* no-op — the capability appears to\n * ship, every fetch quietly takes the direct route, and nothing fails. That is\n * the worst failure mode available to this feature, so the names are a shared\n * constant with a contract test rather than eight string literals spread across\n * two packages.\n */\nexport const PROXY_ENV_VARS = {\n datacenter: { username: \"OXYLABS_DATACENTER_USERNAME\", password: \"OXYLABS_DATACENTER_PASSWORD\" },\n residential: { username: \"OXYLABS_RESIDENTIAL_USERNAME\", password: \"OXYLABS_RESIDENTIAL_PASSWORD\" },\n} as const satisfies Record<ProxyTier, { username: string; password: string }>;\n\nexport interface TierCredentials {\n readonly username: string;\n readonly password: string;\n}\n\nexport type ProxyCredentials = Partial<Readonly<Record<ProxyTier, TierCredentials>>>;\n\n/**\n * Read whichever tiers are fully configured.\n *\n * Takes the env record rather than reading `process.env` itself, so the whole\n * package stays pure and the Convex and CLI sides can be tested identically.\n *\n * Half a pair is not a credential. A username with no password would build a\n * route that authenticates as nobody, and Oxylabs answers that with the same\n * 407 as a wrong password — so it would look like a broken proxy rather than an\n * unconfigured one. Dropping it here means `configuredTiers` tells the truth.\n */\nexport function readProxyCredentials(env: Record<string, string | undefined>): ProxyCredentials {\n const credentials: { -readonly [K in ProxyTier]?: TierCredentials } = {};\n for (const tier of ESCALATION_ORDER) {\n const username = env[PROXY_ENV_VARS[tier].username]?.trim();\n const password = env[PROXY_ENV_VARS[tier].password]?.trim();\n if (username && password) credentials[tier] = { username, password };\n }\n return credentials;\n}\n\n/**\n * The inverse of `readProxyCredentials`: credentials back out as env vars.\n *\n * The writing side used to be a loop inside one Convex function, which is how\n * the ladder came to exist everywhere and work in one place. That loop ran only\n * for `baker landing inspiration scrape`, so every other CLI fetch surface in a\n * Session — `images download`, canvas `ingest`, the two scaffolders, yt-dlp —\n * read four variables nobody had set and went direct, silently, exactly as\n * `PROXY_ENV_VARS` warns.\n *\n * Pairing it with the reader here is the point: the round trip is a test, so the\n * two sides cannot drift into agreeing about three variables out of four.\n */\nexport function proxyCredentialEnvs(credentials: ProxyCredentials): Record<string, string> {\n const envs: Record<string, string> = {};\n for (const tier of ESCALATION_ORDER) {\n const held = credentials[tier];\n if (!held) continue;\n envs[PROXY_ENV_VARS[tier].username] = held.username;\n envs[PROXY_ENV_VARS[tier].password] = held.password;\n }\n return envs;\n}\n\n/**\n * Which tiers this process could actually reach, cheapest first.\n *\n * There is no ceiling to apply. A deployment holding both pairs may climb to\n * residential, full stop — the brake is the credential itself. Removing\n * `OXYLABS_RESIDENTIAL_PASSWORD` takes effect on the next restart, needs no\n * deploy, and withholds the tier by the same mechanism a ceiling would have,\n * so the valve that used to sit here only added a second way to say it. It was\n * also never wired into ad-dna's task definition, which is the highest-volume\n * egress in the repo — a control that could not be turned where it mattered\n * most.\n */\nexport function configuredTiers(credentials: ProxyCredentials): ProxyTier[] {\n return ESCALATION_ORDER.filter((tier) => credentials[tier] !== undefined);\n}\n\n/** Whether there is any proxy to escalate to at all. */\nexport function proxyEscalationConfigured(credentials: ProxyCredentials): boolean {\n return configuredTiers(credentials).length > 0;\n}\n","/**\n * When is a failure worth spending a more expensive route on?\n *\n * The ladder only ever climbs on evidence that *who we are* was the problem. A\n * page that 404s, a domain that does not resolve, a certificate that will not\n * negotiate and a site that timed out all fail identically from every IP on\n * earth, so retrying them through a metered exit buys a second identical\n * failure and a bill. That asymmetry — cheap to be wrong in one direction,\n * expensive in the other — is why this is a closed allow-list rather than\n * \"escalate unless we recognise the error\".\n *\n * Shared deliberately: the capture engine, the image fetchers and the ad-media\n * downloaders all have to agree on what \"blocked\" means, or the corpus and the\n * bill disagree about the same event.\n */\n\n/** What one attempt observed, normalised across Playwright and `fetch`. */\nexport interface BlockSignal {\n /** Main-document / response status, when there was one. */\n readonly status?: number | null;\n /** Chromium `net::` name or a Node error code, when the attempt threw one. */\n readonly netError?: string | null;\n /** A challenge body was detected behind an otherwise successful response. */\n readonly challenge?: boolean;\n /** The attempt ran out of time rather than being refused. */\n readonly timedOut?: boolean;\n}\n\n/** \"We don't like *you*\" — a different exit IP is a different you. */\nconst ESCALATABLE_STATUSES = new Set([403, 429, 451]);\n\n/**\n * A TCP reset on ClientHello is how a WAF null-routes a datacenter range.\n *\n * Today all three of these collapse into one \"site didn't respond\" message and\n * are filed non-retryable, which is precisely why this class of block has been\n * invisible: it looks identical to a site that is genuinely down.\n */\nconst ESCALATABLE_NET_ERRORS = new Set([\"ERR_CONNECTION_RESET\", \"ERR_CONNECTION_CLOSED\", \"ERR_EMPTY_RESPONSE\"]);\n\n/**\n * Failures that belong to our route, not to the site.\n *\n * A 407 cannot come from an origin server — only something speaking proxy\n * produces one — so this needs no \"was this attempt proxied\" flag to be safe.\n */\nconst PROXY_NET_ERRORS = new Set([\n \"ERR_TUNNEL_CONNECTION_FAILED\",\n \"ERR_PROXY_CONNECTION_FAILED\",\n \"ERR_PROXY_AUTH_REQUESTED\",\n \"ERR_PROXY_CERTIFICATE_INVALID\",\n \"ERR_UNEXPECTED_PROXY_AUTH\",\n \"ERR_MANDATORY_PROXY_CONFIGURATION_FAILED\",\n \"ERR_HTTPS_PROXY_TUNNEL_RESPONSE_REDIRECT\",\n]);\n\nexport function isProxyFailure(signal: BlockSignal): boolean {\n if (signal.status === 407) return true;\n return signal.netError ? PROXY_NET_ERRORS.has(signal.netError) : false;\n}\n\nexport function shouldEscalate(signal: BlockSignal): boolean {\n // Our own broken route never advances the ladder — it degrades it. Climbing\n // here would spend a more expensive rung reproducing our misconfiguration.\n if (isProxyFailure(signal)) return false;\n if (signal.timedOut) return false;\n if (signal.challenge) return true;\n if (signal.status != null && ESCALATABLE_STATUSES.has(signal.status)) return true;\n return signal.netError ? ESCALATABLE_NET_ERRORS.has(signal.netError) : false;\n}\n","/**\n * The same question as `refuseNonPublicUrl`, asked of an address instead of a name.\n *\n * A URL carries a hostname, and a hostname is not an address. `localtest.me`\n * and `127.0.0.1.nip.io` are public names, with public DNS records, that\n * resolve to `127.0.0.1` — and nothing stops an attacker pointing a domain they\n * own at `169.254.169.254`. Proven against a real loopback server: the hostname\n * guard passed `http://localtest.me:4321/secret.txt` straight through, the bytes\n * were downloaded, and only the image parser complained afterwards.\n *\n * IPv6 is parsed rather than pattern-matched, and that is the second lesson\n * here. The first version tested prefixes with regexes and unwrapped an\n * IPv4-mapped address only when it still had a dot in it — but a URL parser\n * normalises `::ffff:127.0.0.1` to `::ffff:7f00:1`, so the dot is gone by the\n * time anyone looks. `[::ffff:169.254.169.254]` therefore read as public and the\n * ladder returned the metadata body with `outcome: \"ok\"`. One address has many\n * spellings; comparing spellings is the wrong operation.\n *\n * Anything unparseable is treated as private. This is a safety gate, and the\n * only safe reading of \"I do not know what this is\" is \"do not fetch it\".\n */\n\n/** Every IPv4 range that is not routable on the public internet. */\nfunction isPrivateIpv4Octets(octets: readonly number[]): boolean {\n const [a, b] = octets as [number, number, number, number];\n if (a === 0) return true; // \"this network\" — 0.0.0.0/8\n if (a === 10) return true; // private\n if (a === 127) return true; // loopback\n if (a === 169 && b === 254) return true; // link-local, and the cloud metadata address\n if (a === 172 && b >= 16 && b <= 31) return true; // private /12 — NOT all of 172.*\n if (a === 192 && b === 168) return true; // private\n if (a === 100 && b >= 64 && b <= 127) return true; // carrier-grade NAT\n if (a >= 224) return true; // multicast and reserved\n return false;\n}\n\n/** Dotted-quad to octets, or `null` when it is not one. */\nfunction parseIpv4(value: string): number[] | null {\n const parts = value.split(\".\");\n if (parts.length !== 4) return null;\n const octets = parts.map((part) => (/^\\d{1,3}$/.test(part) ? Number(part) : Number.NaN));\n if (octets.some((octet) => !Number.isInteger(octet) || octet < 0 || octet > 255)) return null;\n return octets;\n}\n\n/**\n * One side of a `::` to its 16-bit groups, or `null` if any part is not one.\n *\n * A trailing dotted-quad stands for the final two groups and is only legal in\n * last position — that is the spelling `::ffff:127.0.0.1` uses before a URL\n * parser rewrites it to `::ffff:7f00:1`.\n */\nfunction expandGroups(part: string): number[] | null {\n if (part === \"\") return [];\n const chunks = part.split(\":\");\n const groups: number[] = [];\n for (let index = 0; index < chunks.length; index++) {\n const chunk = chunks[index] as string;\n if (chunk.includes(\".\")) {\n if (index !== chunks.length - 1) return null;\n const octets = parseIpv4(chunk);\n if (!octets) return null;\n groups.push(((octets[0] as number) << 8) | (octets[1] as number));\n groups.push(((octets[2] as number) << 8) | (octets[3] as number));\n continue;\n }\n if (!/^[0-9a-f]{1,4}$/.test(chunk)) return null;\n groups.push(Number.parseInt(chunk, 16));\n }\n return groups;\n}\n\n/**\n * IPv6 to its eight 16-bit groups, or `null` when it is not one.\n *\n * Handles the `::` elision and a trailing dotted-quad, which together are why\n * one address has so many spellings.\n */\nfunction parseIpv6(value: string): number[] | null {\n const halves = value.split(\"::\");\n if (halves.length > 2) return null;\n\n const head = expandGroups(halves[0] as string);\n const tail = halves.length === 2 ? expandGroups(halves[1] as string) : [];\n if (head === null || tail === null) return null;\n\n if (halves.length === 1) return head.length === 8 ? head : null;\n const missing = 8 - head.length - tail.length;\n if (missing < 1) return null;\n return [...head, ...new Array<number>(missing).fill(0), ...tail];\n}\n\nexport function isPrivateAddress(ip: string): boolean {\n const address = ip.trim().toLowerCase();\n if (!address) return true;\n\n const v4 = parseIpv4(address);\n if (v4) return isPrivateIpv4Octets(v4);\n\n const v6 = parseIpv6(address);\n // Not an address we can read. A safety gate may not guess.\n if (!v6) return true;\n\n const [g0, g1, g2, g3, g4, g5, g6, g7] = v6 as [number, number, number, number, number, number, number, number];\n\n // `::` (unspecified) and `::1` (loopback).\n if (g0 === 0 && g1 === 0 && g2 === 0 && g3 === 0 && g4 === 0 && g5 === 0 && g6 === 0) return true;\n\n // IPv4-mapped (`::ffff:a.b.c.d`) and IPv4-translated (`::ffff:0:a.b.c.d`).\n // The whole point: by the time this is read the tail is hex, not dotted.\n const mappedV4 = g0 === 0 && g1 === 0 && g2 === 0 && g3 === 0;\n if (mappedV4 && g4 === 0 && g5 === 0xffff) {\n return isPrivateIpv4Octets([g6 >> 8, g6 & 0xff, g7 >> 8, g7 & 0xff]);\n }\n if (mappedV4 && g4 === 0xffff && g5 === 0) {\n return isPrivateIpv4Octets([g6 >> 8, g6 & 0xff, g7 >> 8, g7 & 0xff]);\n }\n\n if ((g0 & 0xfe00) === 0xfc00) return true; // fc00::/7 unique-local\n if ((g0 & 0xffc0) === 0xfe80) return true; // fe80::/10 link-local\n return false;\n}\n","/**\n * Addresses that must never be fetched, and must never be proxied.\n *\n * Lifted from `convex/landingLibrary/utils.ts`, where these three regexes were\n * the only copy in the repo — reachable from Convex and from nowhere else. That\n * was survivable while the capture ran one hop after the check. It is not\n * survivable now: the CLI can be invoked directly, two Convex paths enqueue\n * without re-validating, and a proxy adds a rung where \"which network am I on\"\n * stops being rhetorical.\n *\n * Two distinct reasons live here, and both point the same way:\n *\n * - **There is nothing to read.** A private address only resolves from inside\n * the network asking, so a capture of one produces a screenshot of an error.\n * - **There is something to read, and we must not.** `169.254.169.254` is the\n * cloud metadata endpoint that every capture runner has a route to.\n *\n * The rule for the proxy is *refuse*, not *bypass*. Bypassing would leave a\n * request that still happens, just unproxied; refusing means there is no\n * request to route. That is a stronger guarantee and a simpler one to check.\n */\n\nimport { isPrivateAddress } from \"./privateAddress.ts\";\n\n/**\n * Names — not addresses. Anything that looks like an address goes to\n * `isPrivateAddress`, which parses it instead of matching its spelling.\n */\nconst PRIVATE_HOST = /^(localhost|.*\\.localhost|.*\\.local)$/;\n/** Where a Session's own preview is served while the page is being built. */\nconst SANDBOX_HOST = /\\.e2b\\.(app|dev)$/;\n\n/**\n * A bracketed IPv6 literal, unwrapped.\n *\n * `new URL(\"http://[::1]/\").hostname` keeps the brackets, so a hostname check\n * that does not strip them is comparing against a string no address will ever\n * equal. The previous version tried to absorb that with `\\[?::1\\]?` and caught\n * exactly one address out of the whole of IPv6 — `[::ffff:169.254.169.254]`\n * sailed through and the ladder returned the metadata body with `outcome: \"ok\"`.\n */\nfunction unwrapIpLiteral(host: string): string | null {\n if (host.startsWith(\"[\") && host.endsWith(\"]\")) return host.slice(1, -1);\n // A bare v4 literal, or a v6 one that arrived without brackets.\n if (/^[0-9.]+$/.test(host) || host.includes(\":\")) return host;\n return null;\n}\n\nexport function isPrivateHostname(hostname: string): boolean {\n const host = hostname.trim().toLowerCase();\n if (!host) return true;\n if (PRIVATE_HOST.test(host) || SANDBOX_HOST.test(host)) return true;\n\n // Every address form — v4, v6, bracketed, mapped, elided — is decided by one\n // parser. Spelling an address in a regex is what let four of them through.\n const literal = unwrapIpLiteral(host);\n return literal === null ? false : isPrivateAddress(literal);\n}\n\n/**\n * The same rule again, in the only notation a browser's proxy settings speak.\n *\n * Chromium — and so Playwright and `agent-browser` — matches a bypass entry\n * against the hostname and understands one wildcard and no CIDR at all. The\n * ranges above therefore cannot be handed over as regexes; `172.16/12` has to\n * become sixteen entries, and there is no way to express \"any private address\".\n *\n * That makes this a second copy of one fact, which is worth stating plainly:\n * the day it disagrees with `isPrivateHostname` is the day a Session's own\n * preview at `localhost:4321` goes out through a metered exit and comes back\n * refused. Its test pins the correspondence.\n *\n * Note the direction reverses here, and deliberately. Everywhere else the rule\n * for a private address is *refuse*, because a request that still happens is\n * not a guarantee. A browser the agent drives has to reach `localhost:4321` —\n * that is its main job — so here the private address is the legitimate traffic\n * and the list says \"go direct\", not \"do not go\".\n */\nexport const BROWSER_PROXY_BYPASS = [\n \"localhost\",\n \"*.localhost\",\n \"127.0.0.1\",\n \"0.0.0.0\",\n \"::1\",\n \"*.local\",\n // A Session's own preview, which is served from the sandbox's public host —\n // public in DNS, ours in every sense that matters here.\n \"*.e2b.app\",\n \"*.e2b.dev\",\n \"10.*\",\n \"192.168.*\",\n \"169.254.*\",\n ...Array.from({ length: 16 }, (_, i) => `172.${16 + i}.*`),\n].join(\",\");\n\nexport type NonPublicReason = \"not_public\" | \"unparseable\";\n\n/**\n * `null` when the URL is safe to fetch; a reason when it must be refused.\n *\n * Anything that is not plain http(s) is refused too. A capture is handed URLs\n * from agents and from stored rows, and `file:` reaching a browser we launched\n * is a local file read wearing a URL.\n */\nexport function refuseNonPublicUrl(url: string): NonPublicReason | null {\n let parsed: URL;\n try {\n parsed = new URL(url);\n } catch {\n return \"unparseable\";\n }\n if (parsed.protocol !== \"http:\" && parsed.protocol !== \"https:\") return \"not_public\";\n return isPrivateHostname(parsed.hostname) ? \"not_public\" : null;\n}\n","import type { ProxyCredentials } from \"./credentials.ts\";\nimport type { ProxyTier, ProxyTierSpec } from \"./tiers.ts\";\nimport { KNOWN_USERNAME_PREFIXES, PROXY_TIERS } from \"./tiers.ts\";\n\n/**\n * How one attempt reaches the internet.\n *\n * Credentials stay in separate fields instead of being embedded in the URL:\n * Playwright's `proxy` option wants them that way, undici's `ProxyAgent` takes\n * them as a header we build once, and — the real reason — a password that never\n * exists as a substring of a URL cannot be leaked by anything that logs a URL.\n */\nexport type ProxyRoute =\n | { readonly kind: \"direct\" }\n | {\n readonly kind: \"proxy\";\n readonly tier: ProxyTier;\n /** e.g. `http://dc.oxylabs.io:8000` */\n readonly server: string;\n readonly username: string;\n readonly password: string;\n };\n\nexport const DIRECT_ROUTE: ProxyRoute = { kind: \"direct\" };\n\nexport interface RouteOptions {\n /** ISO-3166 alpha-2, upper-cased here. Omit for \"wherever\" — the default. */\n readonly country?: string;\n /** Sticky-session id. Ignored by a tier whose product has no session syntax. */\n readonly session?: string;\n}\n\nexport type RouteProblem = \"missing\" | \"wrong_prefix\";\n\nexport type RouteResult = { ok: true; route: ProxyRoute } | { ok: false; problem: RouteProblem };\n\n/**\n * Put the account name into the shape this specific product expects.\n *\n * Three cases, and the third is the one worth being strict about:\n *\n * 1. Bare (`acme`) — someone pasted the account name. Prepend the prefix.\n * 2. Already correct (`user-acme` in the datacenter slot) — someone pasted the\n * full Oxylabs username. Leave it alone.\n * 3. Carrying the OTHER product's prefix (`customer-acme` in the datacenter\n * slot) — refuse.\n *\n * Case 3 must not be \"helpfully\" rewritten. A `customer-` value in the\n * datacenter slot means the residential credentials were pasted into the wrong\n * variable, and rewriting the prefix would authenticate a residential account\n * against the datacenter endpoint. That either 407s — wasting the cheap rung\n * for no reason — or it works, and bills residential rates from the tier whose\n * entire purpose is to be the cheap one. Refusing is the only answer that\n * cannot silently cost money.\n */\nexport function proxyUsername(\n spec: ProxyTierSpec,\n rawUsername: string,\n options?: RouteOptions,\n): { ok: true; username: string } | { ok: false; problem: \"wrong_prefix\" } {\n const raw = rawUsername.trim();\n const foreignPrefix = KNOWN_USERNAME_PREFIXES.find(\n (prefix) => prefix !== spec.usernamePrefix && raw.startsWith(prefix),\n );\n if (foreignPrefix) return { ok: false, problem: \"wrong_prefix\" };\n\n let username = raw.startsWith(spec.usernamePrefix) ? raw : `${spec.usernamePrefix}${raw}`;\n // Geo before session — Oxylabs reads the username left to right and rejects\n // the pair in the other order.\n if (options?.country) username += `${spec.countryKey}${options.country.toUpperCase()}`;\n if (options?.session && spec.sessionKey) username += `${spec.sessionKey}${options.session}`;\n return { ok: true, username };\n}\n\n/** Build the route for one tier, or say why there isn't one. */\nexport function buildProxyRoute(tier: ProxyTier, credentials: ProxyCredentials, options?: RouteOptions): RouteResult {\n const held = credentials[tier];\n if (!held) return { ok: false, problem: \"missing\" };\n\n const spec = PROXY_TIERS[tier];\n const username = proxyUsername(spec, held.username, options);\n if (!username.ok) return username;\n\n return {\n ok: true,\n route: {\n kind: \"proxy\",\n tier,\n server: `http://${spec.host}:${spec.port}`,\n username: username.username,\n password: held.password,\n },\n };\n}\n","import type { ProxyCredentials } from \"./credentials.ts\";\nimport { configuredTiers } from \"./credentials.ts\";\nimport { refuseNonPublicUrl } from \"./publicAddress.ts\";\nimport type { ProxyRoute, RouteOptions } from \"./route.ts\";\nimport { buildProxyRoute, DIRECT_ROUTE } from \"./route.ts\";\n\n/**\n * Every route this URL is allowed to be attempted on, in the order to try them.\n *\n * Direct is always first and always present: an unblocked page must cost\n * nothing, and a deployment with no credentials has to behave exactly as it did\n * before any of this existed.\n *\n * A tier whose credentials are malformed is *skipped*, not fatal. One\n * mis-pasted variable should cost the use of that rung, not the whole ladder —\n * the alternative is that a typo in the cheap tier silently disables the\n * expensive one that would have worked.\n */\nexport function plannedRoutes(url: string, credentials: ProxyCredentials, options?: RouteOptions): ProxyRoute[] {\n // A private address is refused upstream; if one reaches here anyway it must\n // not become a billed request, and there is no exit node on earth from which\n // `localhost` means us.\n if (refuseNonPublicUrl(url) !== null) return [DIRECT_ROUTE];\n\n const routes: ProxyRoute[] = [DIRECT_ROUTE];\n for (const tier of configuredTiers(credentials)) {\n const built = buildProxyRoute(tier, credentials, options);\n if (built.ok) routes.push(built.route);\n }\n return routes;\n}\n","import type { ProxyCredentials } from \"@baker/proxy\";\nimport { readProxyCredentials } from \"@baker/proxy\";\nimport { createEnv } from \"@t3-oss/env-core\";\nimport { z } from \"zod\";\n\ntype Env = {\n BAKER_API_KEY: string;\n BAKER_API_URL: string;\n BAKER_CHAT_ID?: string;\n BAKER_ACTING_USER_ID?: string;\n BAKER_GOOGLE_ADS_CUSTOMER_ID?: string;\n BAKER_GA4_PROPERTY_ID?: string;\n BAKER_GSC_SITE_URL?: string;\n BAKER_X_ADS_ACCOUNT_ID?: string;\n BAKER_META_AD_ACCOUNT_ID?: string;\n BAKER_LINKEDIN_AD_ACCOUNT_ID?: string;\n};\n\nlet cached: Env | undefined;\n\nexport function getEnv(): Env {\n if (!cached) {\n cached = createEnv({\n server: {\n BAKER_API_KEY: z.string().startsWith(\"bk_\", \"API key must start with 'bk_'\"),\n BAKER_API_URL: z.url(\"BAKER_API_URL must be a valid URL\"),\n BAKER_CHAT_ID: z.string().optional(),\n BAKER_ACTING_USER_ID: z.string().optional(),\n BAKER_GOOGLE_ADS_CUSTOMER_ID: z\n .string()\n .regex(/^\\d{10}$/)\n .optional(),\n BAKER_GA4_PROPERTY_ID: z.string().optional(),\n BAKER_GSC_SITE_URL: z.string().optional(),\n BAKER_X_ADS_ACCOUNT_ID: z\n .string()\n .regex(/^[a-z0-9]+$/, \"X Ads account ID must be a base36 string\")\n .optional(),\n BAKER_META_AD_ACCOUNT_ID: z.string().optional(),\n BAKER_LINKEDIN_AD_ACCOUNT_ID: z\n .string()\n .regex(/^\\d+$/, \"LinkedIn ad account ID must be the numeric portion of urn:li:sponsoredAccount:N\")\n .optional(),\n },\n runtimeEnv: process.env,\n });\n }\n return cached;\n}\n\n// Read directly (not via the validated schema): debug logging must work even when\n// the required BAKER_API_KEY/BAKER_API_URL are missing or malformed.\nexport function debugLogSetting(): string | undefined {\n const raw = process.env.BAKER_DEBUG_LOG?.trim();\n return raw ? raw : undefined;\n}\n\nexport function requireChatId(): string {\n const env = getEnv();\n if (!env.BAKER_CHAT_ID) {\n throw new Error(\n \"BAKER_CHAT_ID is not set. This command stages changes against a chat — run it from a chat-attached environment.\",\n );\n }\n return env.BAKER_CHAT_ID;\n}\n\n/**\n * Which chat a *read* addresses. A `--chat` value names another chat in the same company — how an\n * earlier chat's staged changes are recovered verbatim instead of rebuilt from a summary — and with\n * no flag it is this session's own chat.\n *\n * Read paths only. Staging, amending and discarding keep calling `requireChatId`, so one session can\n * never edit another's draft; the backend enforces the same thing twice over, since every mutating\n * internal requires the draft be `active` and every route checks the chat belongs to the caller's\n * company.\n */\nexport function resolveChatId(chat?: unknown): string {\n return typeof chat === \"string\" && chat.length > 0 ? chat : requireChatId();\n}\n\n/**\n * Wall clock the whole capture may spend, when the caller bounded it.\n *\n * Read from the environment rather than taken as a flag, deliberately. A\n * `--budget-ms` argument would land in the command schema, and the schema is\n * what the agent reads — one question away from \"what is the budget for?\", and\n * from there the escalation ladder stops being invisible to it.\n *\n * Kept out of `getEnv()` because that validator is for a chat-attached run; the\n * capture engine also runs where none of those variables exist.\n */\nexport function captureBudgetMs(): number | null {\n const raw = Number(process.env.BAKER_CAPTURE_BUDGET_MS);\n return Number.isFinite(raw) && raw > 0 ? raw : null;\n}\n\n/**\n * Egress-proxy credentials for a capture, or none at all.\n *\n * These arrive as sandbox *command* env, never as arguments — a credential in\n * argv is readable by any `ps` sharing the sandbox. With none set,\n * `plannedRoutes` yields the direct route alone and a capture behaves exactly\n * as it did before the ladder existed.\n *\n * Whichever pairs arrive are the rungs this process may climb; there is nothing\n * to filter. A tier the deployment withholds is simply absent from this env, so\n * a ceiling read on this side could only ever disagree with the one place that\n * decides.\n */\nexport function captureProxyCredentials(): ProxyCredentials {\n return readProxyCredentials(process.env);\n}\n\n/**\n * The Vimeo session cookie the deployment supplied, or undefined.\n *\n * Absent is the normal, supported state: a deployment that has not configured\n * one gets the same \"download it and upload the file\" refusal Vimeo gave before\n * this existed. Read here with the other env, so nothing else reaches into\n * `process.env` for it.\n */\nexport function captureVimeoCookie(): string | undefined {\n const raw = process.env.VIMEO_COOKIE?.trim();\n return raw ? raw : undefined;\n}\n\n/**\n * This process's environment plus `extra`, for handing to a child.\n *\n * Lives here because `process.env` reads belong in this file — but it earns its\n * place beyond the lint rule: `spawn`'s `env` *replaces* the environment rather\n * than extending it, so a caller that passes only its additions silently strips\n * `PATH` and the child fails to start for a reason that looks nothing like the\n * cause.\n */\nexport function childEnvWith(extra: Record<string, string>): NodeJS.ProcessEnv {\n return { ...process.env, ...extra };\n}\n"],"mappings":"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","names":[]}
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import {
|
|
2
2
|
ApiError,
|
|
3
3
|
apiGet
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-GU7IWEBC.js";
|
|
5
5
|
import {
|
|
6
6
|
getEnv
|
|
7
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-DZUVUGEP.js";
|
|
8
8
|
|
|
9
9
|
// src/error-handler.ts
|
|
10
10
|
var REQUEST_CONNECTION_PLATFORM = {
|
|
@@ -294,4 +294,4 @@ export {
|
|
|
294
294
|
writeAdsOutput,
|
|
295
295
|
resolveCustomerId
|
|
296
296
|
};
|
|
297
|
-
//# sourceMappingURL=chunk-
|
|
297
|
+
//# sourceMappingURL=chunk-EX2OIOVL.js.map
|