diff --git a/skills/.curated/openai-ads-conversions-setup/LICENSE.txt b/skills/.curated/openai-ads-conversions-setup/LICENSE.txt new file mode 100644 index 0000000..7a4a3ea --- /dev/null +++ b/skills/.curated/openai-ads-conversions-setup/LICENSE.txt @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. \ No newline at end of file diff --git a/skills/.curated/openai-ads-conversions-setup/SKILL.md b/skills/.curated/openai-ads-conversions-setup/SKILL.md new file mode 100644 index 0000000..dae9afa --- /dev/null +++ b/skills/.curated/openai-ads-conversions-setup/SKILL.md @@ -0,0 +1,170 @@ +--- +name: openai-ads-conversions-setup +description: Guide Codex through instrumenting or extending repositories with OpenAI Ads Measurement Pixel and optional Conversions API (CAPI). Use when adding Ads conversion tracking, browser pixel events, server-side conversion events, event_id deduplication, CAPI secret placeholders, incremental conversion coverage, or validating Ads conversion setup. Applies to local repositories and PR review contexts; prioritize safe, reviewable diffs and never place API keys or secrets in source code or client bundles. +--- + +# OpenAI Ads Conversions Setup + +## Overview + +Use this skill to add, extend, or review OpenAI Ads conversion instrumentation in an advertiser repository. It supports browser Pixel setup, server-side Conversions API setup, Pixel+CAPI deduplication, and incremental reruns after the app adds new pages or flows, while keeping diffs small and avoiding secret exposure. + +Use only public OpenAI Ads documentation and repository-local patterns for implementation choices. If the docs are unavailable or unclear, ask for clarification or leave a documented follow-up instead of relying on undocumented behavior. + +## Required Inputs + +Before editing code, identify: + +- Setup mode: `pixel`, `capi`, or `pixel+capi`. +- Frontend/runtime surface: browser JavaScript/TypeScript, server-rendered web, backend-only, mobile/native, or unknown. +- Pixel ID, if Pixel is in scope. +- CAPI authentication plan, if CAPI is in scope: existing secret/env var name, secret manager path, or placeholder-only. +- Conversion events to instrument and where they occur in the product flow. +- Whether this is a first-time setup or an incremental rerun extending existing OpenAI Ads instrumentation. +- Whether the desired behavior is `propose patch`, `apply local patch`, or `review existing integration`. + +Treat the linked OpenAI Ads docs as the source of truth for exact SDK syntax, endpoint shape, supported events, request fields, and validation rules. When browsing is available, check the current docs before writing concrete calls; if the docs conflict with this skill, follow the docs and mention the conflict in the setup report. When browsing is unavailable, use this skill's references as a fallback. Do not invent SDK functions, event names, request schemas, or endpoint paths. + +Primary docs to prefer when available: + +- `https://developers.openai.com/ads/measurement-pixel` +- `https://developers.openai.com/ads/conversions-api` +- `https://developers.openai.com/ads/supported-events` + +## Workflow + +1. Inspect the repository shape. + - Detect framework, package manager, router, server runtime, frontend surface, env convention, test commands, and existing analytics integrations. + - Determine whether a browser JavaScript Pixel is supportable. If the customer-facing surface is mobile/native, backend-only, or otherwise lacks a safe browser JS insertion point, use CAPI-only and report that Pixel was skipped. + - Search for existing OpenAI Ads instrumentation: Pixel initialization, CAPI clients, event helpers, config/env vars, dedupe IDs, attribution context, tests, and setup docs. + - Search for existing pixels or server conversion APIs such as Meta Pixel/CAPI, Google Ads/gtag, TikTok Events API, Pinterest, Snap, Segment, RudderStack, or custom analytics wrappers. + - Use other ad platform integrations as evidence for local conversion boundaries, consent gates, config/secret patterns, non-blocking dispatch, dedupe IDs, and tests. Do not copy their event names, payload schemas, user matching fields, credential exposure patterns, or retry behavior. + - Prefer the repository's existing analytics abstraction over adding a parallel abstraction. If OpenAI Ads instrumentation already exists, treat its helper/client/config pattern as canonical. + - Search for existing config and secret retrieval patterns before choosing env vars: framework settings, typed config objects, deployment env conventions, Vault, cloud secret managers, Kubernetes secrets, Doppler, 1Password, or similar. + +2. Confirm the setup plan. + - For Pixel, identify a single browser-only initialization point, event call sites, and any approved browser user-data path. + - For CAPI, identify a server-only execution point, secret retrieval pattern, and outbound HTTP/client pattern. + - For Pixel+CAPI, define one logical Pixel ID and a stable `event_id` strategy so browser and server events can deduplicate. + - For CAPI web events, define an `oppref` strategy and a sanitized `source_url` strategy before editing. Prefer a server-readable raw `__oppref` cookie value; if unavailable, pass minimal optional conversion context from the browser to an existing server conversion request. Validate any browser-provided `sourceUrl` against a trusted request/configured origin before using it. If a configured canonical site origin exists, prefer it for fallback `source_url` construction while still allowing the request origin as trusted. + - For CAPI-only web setup, explicitly plan how the server event will include matching and attribution context that the Pixel would otherwise help provide: `oppref`, `source_url`, client `user_agent`, trusted client IP, and documented hashed user identifiers when safely available. + - If adding a CAPI validation toggle, keep checked-in defaults production-capable. `validate_only: true` is documented for local smoke tests, but committed env templates and runtime defaults should be blank or false unless the user explicitly asks for validation-only behavior. + - Select supported event names from current docs. Evaluate the primary supported events (`page_viewed`, `contents_viewed`, `items_added`, `checkout_started`, `order_created`, `lead_created`, `registration_completed`, `appointment_scheduled`, `subscription_created`, and `trial_started`) and report high-confidence events you will instrument plus supported events you intentionally skip. + - If Pixel is installed and `page_viewed` is not instrumented, explicitly say why: too noisy, SPA route semantics unclear, no meaningful landing pages, already covered by an existing helper, or intentionally deferred. + - Do not silently skip plausible events. For each skipped supported event, state why: not applicable, no confirmed success boundary found, ambiguous product semantics, unsupported surface, or safe follow-up available if the advertiser wants broader coverage. + - If the target flow is ambiguous, ask one concise question before editing. + +3. Implement the smallest useful patch. + - Add initialization once. + - Instrument only clearly identified conversion events. + - Favor a small high-confidence initial patch over speculative broad coverage. Put plausible but unconfirmed events in the setup report as optional follow-ups, not half-guessed instrumentation. + - Keep config names explicit. Public framework aliases such as `NEXT_PUBLIC_OPENAI_ADS_PIXEL_ID` or `VITE_OPENAI_ADS_PIXEL_ID` are allowed for browser Pixel code, but document that they must contain the same Pixel ID as the server-side Pixel ID when CAPI sends the same event. + - Never put CAPI keys in browser-visible env vars, frontend code, logs, comments, docs, test snapshots, or generated reports. + - Use documented OpenAI Ads event names and request field names only. Do not use legacy CAPI field names such as `event_name`, `event_time_epoch_ms`, `event_source_url`, or `event_data`. + - For commerce flows, ensure each instrumented event covers the actual success path. For `items_added`, cover all successful add-to-cart and quantity-increment paths you can confirm; if you only cover a subset, say that clearly in the setup report and offer the missing paths as follow-ups. + - If Pixel user data becomes available after the initial Pixel init, add a second `oaiq("init", { user })` call near the point where that data becomes available. Do not include `pixelId` again after a successful first init, and do not put Pixel user data in `measure` calls. + - Conversion reporting must not fail, block, or materially slow the core business flow. Pixel helpers should catch/suppress their own errors; CAPI calls should run in an existing non-blocking path, background task, or short fire-and-log path when possible. + +4. Verify locally. + - Run relevant typecheck, lint, unit tests, or framework-specific checks when available. + - Resolve the directory containing this installed skill before using helper scripts. + - Run this skill's static helpers when useful: + - `python3 /scripts/verify_capi_secret_not_exposed.py ` + - `python3 /scripts/verify_ads_setup.py --pixel-id --require pixel --require capi` + - For Pixel+CAPI web dedupe, add `--require dedupe --require shared-pixel-id --require oppref --require source-url`. + - These helpers run locally. By default, they report paths, line numbers, rules, and summaries without printing source-line context or transmitting repository contents. + - If verification cannot run, report the blocker and the residual risk. + +5. Produce a setup report. + - Include changed files, detected stack, setup mode, existing OpenAI Ads instrumentation found, events added or changed in this run, events already covered, supported events skipped with reasons, optional follow-up events the advertiser can add if desired, skipped surfaces/platforms, Pixel ID/config reference, Pixel user-data strategy, Pixel opt-out behavior, CAPI secret reference, CAPI `oppref` strategy, CAPI `source_url` strategy, browser-provided `sourceUrl` trust boundary, CAPI user-data strategy, event deduplication strategy, checks run, manual follow-ups, and risks. + - Do not include raw secrets or sensitive advertiser source snippets. + - End with a clear deployment review warning: before deploying, the advertiser should review that the implementation satisfies their privacy, security, consent, and data handling requirements. + +## Incremental Reruns + +When rerunning this skill on a repository that may already have OpenAI Ads instrumentation: + +- Build an inventory first: Pixel init locations, CAPI clients, event helpers, analytics sinks, event call sites, Pixel ID/config sources, `event_id` generation, `oppref` and `source_url` handling, user-data handling, tests, and docs. +- Treat existing OpenAI Ads integration surfaces as canonical. Extend the existing helper/client/adapter instead of creating a second Pixel initializer, second CAPI client, second config convention, or scattered one-off call sites. +- Compare current app flows against the existing event inventory. Add only missing coverage at confirmed success boundaries. +- Check duplicate-risk before editing: a new page or flow may already publish through a shared cart service, checkout service, confirmation component, analytics wrapper, or server conversion helper. +- Preserve existing dedupe, Pixel ID, attribution, user-data, failure-handling, test, and reporting patterns unless they are unsafe or incompatible with current docs. +- If another ad platform already publishes from a new flow but OpenAI Ads does not, treat that as a strong signal of missing OpenAI Ads coverage. Still map it to documented OpenAI Ads event names and payload fields. +- Keep rerun diffs focused. Do not churn env templates, setup docs, or shared helpers unless the extension requires it or they are stale. +- In the final report, separate existing events found, new events added, events skipped because they are already covered, and optional follow-ups. + +## Pixel Rules + +Use `references/measurement-pixel.md` for browser Pixel details. + +- Initialize in a client-only location that runs once per page/app load. +- Use the documented browser SDK pattern: load `https://bzrcdn.openai.com/sdk/oaiq.min.js`, initialize with `oaiq("init", { pixelId })`, and emit events with `oaiq("measure", ...)`. +- Use Pixel only for browser JavaScript/TypeScript surfaces supported by current docs. For mobile/native or unsupported frontends, skip Pixel and prefer CAPI-only when a server conversion boundary exists. +- Avoid duplicate initialization across route transitions, hydration, tests, or nested layouts. +- Instrument conversion events close to the confirmed success boundary, not merely on button click unless the click itself is the conversion. +- Respect consent and privacy gating patterns already present in the repository. +- Do not manually send Pixel `oppref`, `source_url`, timestamps, or batching metadata; the browser SDK handles those transport details. Add `debug: true` only for local/test debugging, not as a committed production default. +- If documented Pixel user data becomes available after initial page-load initialization, call `oaiq("init", { user })` again with only documented, normalized fields. Preserve the repository's existing consent gates for whether measurement and user-data collection are allowed. Put `opt_out` on the Pixel event options when measurement is allowed but the event should be opted out of future user-level personalization. +- Use supported standard event names from current docs, such as `page_viewed`, `contents_viewed`, `items_added`, `checkout_started`, `order_created`, `lead_created`, `registration_completed`, `appointment_scheduled`, `subscription_created`, or `trial_started`. If uncertain, use the closest documented event or leave an explicit follow-up instead of inventing a name. +- Custom events are a fallback when no standard event fits. Use `custom` only with a valid, documented `custom_event_name`. + +## CAPI Rules + +Use `references/conversions-api.md` for server-side setup details. + +- CAPI code must run server-side only. +- Do not create, request, store, or print CAPI keys. +- Prefer an existing secret manager/env retrieval pattern; otherwise add a placeholder config reference and report that the user must provision the secret. +- Use existing HTTP clients, retry conventions, telemetry, and privacy filters. +- Do not add new queues, databases, background jobs, or broad infra unless the user explicitly asks. +- If Pixel and CAPI both emit the same conversion, use the same Pixel ID, event name, and `event_id` in both paths. +- For web CAPI events, include `action_source: "web"` and a sanitized HTTP(S) `source_url` containing only origin plus pathname. Strip query strings and fragments before sending; reject or replace non-HTTP(S) URL schemes with a trusted configured web app URL. If accepting a browser-provided `sourceUrl`, require its origin to match the current request origin or a configured canonical site origin before using it; otherwise fall back to a server-derived URL. When a configured canonical site origin exists, use it as the preferred fallback origin and do not let request/internal proxy origins override it. For non-web server conversions, choose the documented `action_source` that matches the source (`mobile_app`, `offline`, `physical_store`, `phone_call`, `email`, or `other`) instead of forcing `web`. +- Treat `oppref` as opaque attribution context. Prefer a server-readable `__oppref` cookie over client-supplied request context; pass the raw value unchanged and do not parse, URL-decode, cookie-decode, generate, transform, or log it. +- Use CAPI `events[].user` only for documented fields already available through an approved repository pattern. Never send raw email or raw external IDs. For CAPI-only web conversions, make a best effort to include client `user_agent`, trusted client IP, and hashed email or external ID when available; these fields are especially important when there is no paired Pixel event. +- Use current event timestamps in `timestamp_ms`; do not send stale backfills older than the documented window or future-dated events. +- Preserve existing opt-out/consent semantics and map them to documented `opt_out` fields when the repository has a relevant user-level personalization opt-out. +- CAPI failure handling must not throw into checkout, signup, lead submission, or other core success paths. Prefer existing background-task or queue patterns; if none exist, use a bounded fire-and-log implementation and document remaining latency risk. +- For `contents` payloads, use top-level `data.amount` for the event total. For item-level `contents[].amount`, use the unit price when the repository exposes it; omit item-level amount if only line totals are available or the semantics are unclear. + +## Framework Hints + +Use `references/framework-patterns.md` for common locations and pitfalls in Next.js, React/Vite, Remix, Express, Rails, Django, Flask, and monorepos. + +Treat these as hints, not rules. The repository's existing conventions win. + +## Verification And Reporting + +Use `references/verification-checklist.md` for validation steps and `references/report-schema.md` for the final setup report format. + +High-priority failure cases: + +- CAPI secret or API key appears in client code, browser-visible env vars, logs, comments, generated docs, snapshots, or reports. +- Pixel initialization can run multiple times in normal navigation. +- CAPI code can run from a browser bundle. +- Pixel and CAPI duplicate the same event without the same logical Pixel ID and shared dedupe key. +- OpenAI Ads event names are invented or unsupported by current docs. +- CAPI payload uses undocumented or legacy field names, mismatched event/data types, or web events without a sanitized `source_url`. +- Pixel user data is attached to `measure` instead of `init`, includes undocumented/raw identity fields, or bypasses the repository's existing consent gates. +- CAPI `timestamp_ms` is stale, future-dated beyond the documented allowance, or not tied to the actual conversion time. +- CAPI action source is forced to `web` for non-web/mobile/offline events instead of using an appropriate documented `action_source`. +- CAPI custom event names are placed in the wrong field, or Pixel custom events omit the options-level `custom_event_name`. +- CAPI web events ignore available `oppref` context or log `oppref` alongside raw identifiers. +- CAPI `source_url` accepts non-HTTP(S) schemes or preserves query strings/fragments. +- CAPI decodes or transforms `oppref` instead of passing the raw opaque value through unchanged. +- CAPI uses a browser-provided `sourceUrl` with an untrusted origin instead of falling back to a server-derived URL. +- CAPI lets request/internal proxy origins override a configured canonical site origin when constructing fallback `source_url`. +- Existing consent or user-level personalization opt-out behavior is bypassed or lost. +- The setup report does not explain why plausible supported events were skipped. +- The patch instruments likely pre-conversion intent instead of confirmed conversion success. +- Conversion reporting errors can fail or materially delay the core business flow. + +## When To Stop And Ask + +Ask before proceeding when: + +- The conversion event boundary is unclear. +- CAPI is requested but no server runtime or secret pattern exists. +- Exact OpenAI Ads SDK/API syntax cannot be verified from docs or provided context. +- Pixel is requested but the repository has no browser JavaScript/TypeScript surface and no safe browser insertion point. +- The only viable implementation would require new infrastructure, a deploy step, or changing authentication/checkout behavior. +- The user provides or asks you to embed a raw CAPI key in the repository. diff --git a/skills/.curated/openai-ads-conversions-setup/agents/openai.yaml b/skills/.curated/openai-ads-conversions-setup/agents/openai.yaml new file mode 100644 index 0000000..7d46ee6 --- /dev/null +++ b/skills/.curated/openai-ads-conversions-setup/agents/openai.yaml @@ -0,0 +1,4 @@ +interface: + display_name: "OpenAI Ads Conversions Setup" + short_description: "Instrument Ads Pixel and CAPI safely" + default_prompt: "Use $openai-ads-conversions-setup to instrument or extend this repository with OpenAI Ads Measurement Pixel and optional Conversions API, then produce a setup report." diff --git a/skills/.curated/openai-ads-conversions-setup/references/conversions-api.md b/skills/.curated/openai-ads-conversions-setup/references/conversions-api.md new file mode 100644 index 0000000..7a1154d --- /dev/null +++ b/skills/.curated/openai-ads-conversions-setup/references/conversions-api.md @@ -0,0 +1,137 @@ +# Conversions API Reference + +## Purpose + +Conversions API (CAPI) sends conversion events from trusted server-side code. Use it for conversions that are confirmed on the server, for improved reliability, or to pair with Pixel events through deduplication. + +## Non-Negotiable Safety Rules + +- CAPI authentication material must never be written to source code, frontend code, browser-visible env vars, logs, generated reports, snapshots, or comments. +- Do not mint or store secrets. Add only a config reference or placeholder unless the user explicitly asks for secret provisioning through an approved system. +- Do not expose server conversion endpoints that allow arbitrary client event injection. +- Do not log full request payloads if they contain identifiers, customer data, or auth metadata. +- Treat `oppref` as opaque attribution context, not a user identifier. Pass the raw value unchanged when available, but do not parse, URL-decode, cookie-decode, generate, transform, store broadly, or log it with raw identifiers. + +## Implementation Checklist + +1. Find server runtime and outbound API conventions. + - Look for route handlers, API controllers, background jobs, webhook handlers, service objects, typed HTTP clients, retries, and logging standards. + - Prefer existing conversion/event service patterns when present. + - On reruns, inventory existing OpenAI Ads CAPI clients, payload builders, event boundaries, tests, and config before adding new code. Extend the existing CAPI path instead of creating a second client or config convention. + - Use server-side conversion publishers for other ad platforms as evidence for boundaries, secret retrieval, non-blocking dispatch, dedupe IDs, and tests. Do not copy their request schema, user matching fields, retry policy, or credential handling. + - Search for existing config and secret retrieval patterns before defaulting to env vars: typed settings, framework config, deployment env conventions, Vault, cloud secret managers, Kubernetes secrets, Doppler, 1Password, or similar. + +2. Choose the event boundary. + - Purchase: order/payment success persisted on the server. + - Lead: server accepts and stores the lead, or the upstream CRM request succeeds. + - Signup: user/account creation succeeds. + - Subscription/trial: entitlement or plan activation succeeds. + +3. Add safe configuration. + - Use an existing config/env abstraction. + - Use a clear placeholder such as `OPENAI_ADS_CONVERSIONS_API_KEY` for server-only auth. + - If the framework distinguishes public env vars, never use a public prefix for CAPI keys. + - Use one logical Pixel ID for Pixel and CAPI when both emit the same event. A browser public env alias may exist, but it should resolve to the same Pixel ID used by server-side CAPI. + - If adding a `validate_only` toggle, make checked-in env templates and runtime defaults blank or false. `validate_only: true` is for local smoke tests and should not be the default deployed behavior unless the user explicitly asks for validation-only instrumentation. + +4. Build the request using documented schema only. + - Send `POST https://bzr.openai.com/v1/events?pid=` with `Content-Type: application/json` and `Authorization: Bearer `. + - Use current field names: top-level `validate_only` and `events`, and per-event `id`, `type`, `timestamp_ms`, `oppref`, `source_url`, `action_source`, `user`, and `data`. + - Include `custom_event_name` as a CAPI event field when `type` is `custom`. Do not put it inside `data` for CAPI. + - Include `opt_out` when the repository has an existing opt-out or consent concept that should opt the event out of future user-level personalization. + - Do not use legacy field names such as `event_name`, `event_time_epoch_ms`, `event_id`, `event_source_url`, or `event_data` in the CAPI request body. `event_id` is valid for Pixel options, but CAPI uses `id`. + - Verify endpoint path, auth header, event names, field names, and required identifiers from current docs. + - Reuse existing HTTP client and timeout/retry conventions. + - Keep payload construction small and testable. + +5. Add deduplication for Pixel+CAPI. + - Include the same Pixel ID, event name, and `event_id` in Pixel and CAPI events for the same conversion. + - Prefer existing order/payment/lead IDs when stable and non-sensitive. + +6. Add failure behavior consistent with product semantics. + - Conversion reporting failure must not fail, block, or materially slow checkout, signup, lead submission, or other core success flows unless the user explicitly asks for fail-closed behavior. + - Prefer existing background-task, queue, or after-commit patterns. If none exist and adding one would be too broad, use a bounded fire-and-log implementation and report any remaining latency risk. + - Log a redacted warning/metric if the repository has a standard observability pattern. + - Avoid retry loops that can duplicate conversions unless the API supports idempotency/deduplication. + +7. Add web attribution context for CAPI when available. + - If the conversion request is same-site and the server can read cookies, read the raw `__oppref` cookie value and pass it as `events[].oppref` without decoding or normalizing it. + - If the server cannot read the cookie but an existing browser-to-server conversion request exists, add a minimal optional context object such as `openaiAds: { oppref, sourceUrl }`. Treat this as a fallback attribution path, not as trusted business state. + - On the browser, source `oppref` from the `oppref` URL parameter or existing `__oppref` cookie when available. Pass it unchanged and omit it when absent. + - For `action_source: "web"`, send `source_url` as HTTP(S) origin plus pathname only. Strip query strings and fragments before sending, and reject or replace non-HTTP(S) schemes with a trusted configured app URL. + - If using browser-provided `sourceUrl`, validate its origin against the current request origin or a configured canonical site origin before using it. If it is missing, malformed, non-HTTP(S), or from an untrusted origin, fall back to a server-derived URL such as the current route, checkout/confirmation route, or configured canonical site URL. + - If the repository has a configured canonical site origin, such as `OPENAI_ADS_SITE_ORIGIN` or an existing public app URL setting, prefer that origin when constructing fallback `source_url`. The request origin can still be trusted for validation, but it should not override the configured canonical origin for fallback URLs. + - For non-web conversions, choose the documented `action_source` that matches the source: `mobile_app`, `offline`, `physical_store`, `phone_call`, `email`, or `other`. Do not force `web` unless a browser/webpage source URL is available and meaningful. + - Do not build a new public endpoint solely to accept arbitrary conversion events from the browser. + +## Event Contract + +Every CAPI event needs a stable non-empty `id`, supported `type`, integer `timestamp_ms`, and `data` object. `timestamp_ms`, `amount`, and `quantity` values should be integers. `timestamp_ms` must represent the event time in milliseconds, be within the last 7 days, and not be more than 10 minutes in the future. If a `data.amount` is present, include a valid ISO 4217 `data.currency`. + +For `action_source: "web"`, `source_url` is required. For other action sources, include `source_url` only when it is documented and meaningful for the source. Use one of the documented action sources: `web`, `mobile_app`, `offline`, `physical_store`, `phone_call`, `email`, or `other`. + +Use this event-to-data mapping: + +| Event type | `data.type` | Typical boundary | +| --- | --- | --- | +| `page_viewed` | `contents` | meaningful page view, usually Pixel-first | +| `contents_viewed` | `contents` | product/content detail shown | +| `items_added` | `contents` | item added to cart after success | +| `checkout_started` | `contents` | checkout session started | +| `order_created` | `contents` | order/payment success persisted | +| `lead_created` | `customer_action` | lead accepted/stored | +| `registration_completed` | `customer_action` | account creation succeeds | +| `appointment_scheduled` | `customer_action` | appointment booking succeeds | +| `subscription_created` | `plan_enrollment` | paid subscription activates | +| `trial_started` | `plan_enrollment` | trial entitlement activates | +| `custom` | `custom` | no standard event fits | + +For `contents` payloads, use documented content item fields such as `id`, `name`, `content_type`, `quantity`, `amount`, and `currency`. Do not use removed object shapes such as `checkout_session`, `line_items`, or `order`. For commerce totals, use top-level `data.amount` and `data.currency` for the order, checkout, or cart total. If item-level `contents[].amount` is included, prefer unit price. Omit item-level amount when only line totals are available or the repository does not make unit price semantics clear. + +For custom events, set event-level `type: "custom"` and event-level `custom_event_name`, with `data.type: "custom"`. Use lowercase names where possible. Names must be 1-64 ASCII letters, numbers, underscores, or dashes, start and end with an alphanumeric character, and not overlap with standard event names. + +## User Data + +CAPI user data belongs on each event as `events[].user`. Pixel user data belongs in Pixel initialization, not in each Pixel `measure` call. + +Only include documented user fields that the repository already has an approved pattern to collect or derive: + +- `email_sha256` +- `external_id_sha256` +- `country` +- `city` +- `zip_code` +- `ip_address` +- `user_agent` + +Hash `email_sha256` and `external_id_sha256` before sending; values must be lowercase 64-character SHA-256 hex strings. Do not send raw email, raw external IDs, phone numbers, or phone hashes. For `ip_address` and `user_agent`, only pass values already available on the server request path and consistent with the repository's privacy/consent model. + +## Validation And Smoke Testing + +When the repository supports it, add a dry-run or test path that sends `validate_only: true` to CAPI. `validate_only` validates the request but should not be used for Pixel/SDK traffic, and it should not be the default in committed env templates or production runtime config. Keep conversion reporting failures non-blocking for core business logic even when validation or network calls fail. + +If batching events, cap batches at 1000 events. One invalid event fails the full batch, so prefer one event per conversion unless the repository already has a safe batching abstraction, validation path, and retry behavior. + +Add focused tests when practical: + +- Payload builder test for documented field names, event/data type pairing, integer amount/quantity values, and shared dedupe `id`. +- Rerun/extension test or review note showing the new flow uses the existing OpenAI Ads helper/client rather than duplicating CAPI setup. +- `source_url` sanitizer tests that strip query/fragment and reject or replace non-HTTP(S) schemes. +- Browser-provided `sourceUrl` tests that reject untrusted origins, prefer a configured canonical origin for fallback when present, and otherwise fall back to a server-derived URL. +- `oppref` tests or review notes showing cookie/URL values are passed through as raw opaque values without decoding or normalization. +- Conversion boundary test showing reporting is attempted only after the core action succeeds, and skipped when the core action fails. +- Failure-path test or review note showing CAPI network/API failures do not fail checkout, signup, lead submission, or the relevant core flow. + +## When To Defer CAPI + +Defer to a manual follow-up if: + +- There is no server-side conversion boundary in the repository. +- There is no safe secret/config pattern. +- The exact CAPI schema/auth behavior cannot be verified. +- The implementation cannot safely capture `oppref` or `source_url`; omit/report those fields rather than creating a brittle or privacy-risky path. +- Implementing CAPI would require new infrastructure such as queues, cron, secret provisioning, database migrations, or webhook contracts beyond the user's request. + +## Suggested Output + +Report the server files changed, event boundaries, secret reference name, `oppref` strategy, `source_url` strategy, user-data strategy, dedupe strategy, tests/checks run, and manual secret provisioning steps. If no user data is sent, say so and explain why, such as no approved email/customer identifier path. Never include a raw key, token, raw user data, or sensitive source snippets. diff --git a/skills/.curated/openai-ads-conversions-setup/references/framework-patterns.md b/skills/.curated/openai-ads-conversions-setup/references/framework-patterns.md new file mode 100644 index 0000000..07d875d --- /dev/null +++ b/skills/.curated/openai-ads-conversions-setup/references/framework-patterns.md @@ -0,0 +1,80 @@ +# Framework Patterns + +Use these as starting points while inspecting a repository. Existing local conventions should override these hints. + +## Next.js + +- App Router Pixel candidates: client providers imported by `app/layout.tsx`, analytics provider components, or client-only tag modules. +- Pages Router Pixel candidates: `_app.tsx`, analytics providers, or browser entry modules. +- CAPI candidates: route handlers, API routes, server actions, checkout webhooks, order services, or backend package code. +- `NEXT_PUBLIC_*` values are browser-visible. Pixel IDs may be public if docs require it; CAPI keys must not use this prefix. +- Watch for server components. Pixel code must be inside a client component or browser-only module. +- For CAPI web attribution, prefer reading `__oppref` from request cookies in route handlers/server actions. If the conversion is submitted from a client component and cookies are not available server-side, pass a minimal optional context object through the existing request body. + +## React/Vite/SPA + +- Pixel candidates: `src/main.tsx`, `src/App.tsx`, analytics providers, or router-level providers. +- CAPI usually requires a backend package or API service. If the repo is frontend-only, add Pixel and report that CAPI needs server ownership. +- Avoid embedding CAPI calls in frontend API clients. +- If the SPA posts conversion data to a backend, pass only optional attribution context such as `oppref` and sanitized `sourceUrl`; never pass the CAPI key or let the browser choose arbitrary event names. + +## Mobile / Native Apps + +- Do not add the browser Measurement Pixel to iOS, Android, React Native, Flutter, or other native-only surfaces unless current docs explicitly provide a supported SDK for that platform. +- Prefer CAPI-only from a trusted backend conversion boundary when available. +- If the repo has both web and mobile apps, instrument only the supported web surface with Pixel and report mobile as CAPI-only or deferred. + +## Remix + +- Pixel candidates: root document scripts, client analytics modules, or route success pages. +- CAPI candidates: actions/loaders that confirm conversion, server utilities, or webhook handlers. +- Keep server-only code out of files imported by browser bundles. + +## Express/Fastify/Nest + +- Pixel is usually in a separate frontend package. +- CAPI candidates: controllers/routes for checkout, lead submission, signup, or webhook processing. +- Use existing config providers and HTTP client abstractions. +- Read `__oppref` from request cookies when same-site; otherwise accept optional attribution context only on existing trusted conversion routes. + +## Rails + +- Pixel candidates: layout templates, Stimulus controllers, frontend packs, or tag manager partials. +- CAPI candidates: controllers after successful persistence, service objects, ActiveJob jobs, or webhook handlers. +- Use credentials/env patterns already present. +- Prefer controller/request cookie access for `__oppref` before adding browser payload plumbing. + +## Django/Flask + +- Pixel candidates: base templates, frontend bundles, or success-page templates. +- CAPI candidates: views after successful persistence, service modules, Celery tasks, or webhook handlers. +- Use existing settings/secrets patterns. +- Prefer request cookie access for `__oppref` before adding browser payload plumbing. + +## Monorepos + +- Identify the deployed web app and backend package before editing. +- Avoid adding dependencies at the repo root unless the repository already centralizes analytics there. +- Run the package-specific checks rather than broad repo-wide checks when the monorepo is large. + +## Existing Analytics Abstractions + +If an analytics abstraction already exists, prefer adding an OpenAI Ads adapter/sink. This usually gives better consent handling, fewer call sites, and easier testing. + +Avoid bypassing a mature analytics layer unless it cannot represent the required OpenAI Ads event fields. + +## Existing Advertising Platform Integrations + +Search for Meta Pixel/CAPI, Google Ads/gtag, TikTok Events API, Pinterest, Snap, Segment, RudderStack, or custom/first-party ad conversion publishers before adding new OpenAI Ads code. + +Use those integrations to infer: + +- Confirmed conversion boundaries such as checkout success, lead accepted, signup completed, trial started, or subscription activated. +- Existing analytics adapters or event dispatchers to extend. +- Consent/privacy gates. +- Server-side secret/config retrieval patterns. +- Non-blocking dispatch, queue, background-task, or fire-and-log patterns. +- Dedupe or idempotency IDs such as order ID, payment intent ID, lead ID, or signup transaction ID. +- Local tests and fixtures for conversion behavior. + +Do not copy platform-specific event names, request schemas, user matching fields, retry semantics, or credential exposure patterns. Map each event back to current OpenAI Ads docs. diff --git a/skills/.curated/openai-ads-conversions-setup/references/measurement-pixel.md b/skills/.curated/openai-ads-conversions-setup/references/measurement-pixel.md new file mode 100644 index 0000000..b373486 --- /dev/null +++ b/skills/.curated/openai-ads-conversions-setup/references/measurement-pixel.md @@ -0,0 +1,96 @@ +# Measurement Pixel Reference + +## Purpose + +The Measurement Pixel captures browser-side conversion events for OpenAI Ads. Use it when the repository has a client-rendered flow or a browser-visible success page that can emit a conversion after user consent and after the conversion has actually happened. + +The Pixel is a browser JavaScript integration. If the customer-facing surface is mobile/native, backend-only, or otherwise lacks a safe browser JavaScript/TypeScript insertion point, skip Pixel setup and prefer CAPI-only when a server conversion boundary exists. + +## Inputs + +- Pixel ID or repository config key that resolves to the Pixel ID. +- Event list and event boundaries. +- Consent/privacy gating requirements. +- Current OpenAI Ads Pixel docs or SDK reference for exact syntax. Prefer the docs when available; use this reference as a fallback when browsing is unavailable. + +## Implementation Checklist + +1. Find the existing analytics layer. + - Prefer wrappers such as `analytics.track`, `trackEvent`, `useAnalytics`, `gtag`, `fbq`, Segment, RudderStack, or custom/first-party event dispatchers. + - If a wrapper exists, add OpenAI Ads as another sink rather than scattering calls throughout the app. + - On reruns, inventory existing OpenAI Ads browser helpers and event call sites before adding new ones. Extend the existing helper or analytics sink instead of adding a second Pixel initialization or parallel wrapper. + - Use other ad platform browser events as clues for local boundaries and consent gates, but map event names and payloads to OpenAI Ads docs. + +2. Initialize once in browser code. + - Good candidates include app/root layout client providers, analytics providers, main browser entrypoints, or tag manager modules. + - The current SDK pattern is: insert the loader script near the top of ``, load `https://bzrcdn.openai.com/sdk/oaiq.min.js`, initialize with `oaiq("init", { pixelId: "" })`, and emit events with `oaiq("measure", ...)`. + - Use the exact current docs snippet when possible. If framework conventions require a component/module wrapper, preserve the same loader URL, global queue name, init command, and measure command semantics. + - `pixelId` is required. `debug` is optional and should be enabled only for local/test troubleshooting unless the user explicitly asks otherwise. + - Avoid server components, SSR-only files, route handlers, tests, stories, and build scripts. + - Guard against repeated initialization during client-side navigation. + - If user data is already available through an approved browser analytics path, pass documented user fields during Pixel initialization. Do not attach user data to each `measure` call unless current docs explicitly require it. + - Pixel user fields are `email_sha256`, `external_id_sha256`, `country`, `city`, and `zip_code`. Hash raw email or external IDs before sending, or omit them if the repository has no approved browser hashing/normalization pattern. + - If user data becomes available after the first init, such as during login, checkout, lead submission, or signup, call `oaiq("init", { user })` again with the complete documented `user` object before or near the relevant conversion event. After a successful first init, the follow-up init does not need to include `pixelId`. + - Follow the repository's existing consent gates for whether Pixel measurement and Pixel user-data updates are allowed. If measurement is allowed but the event should be opted out of future user-level personalization, send the conversion event with Pixel options `{ opt_out: true }`. + +3. Emit events at confirmed conversion boundaries. + - Use only supported event names from current docs. + - Commerce funnel examples: `page_viewed`, `contents_viewed`, `items_added`, `checkout_started`, `order_created`. + - Lead/signup examples: `lead_created`, `registration_completed`, `appointment_scheduled`. + - Subscription examples: `subscription_created`, `trial_started`. + - Purchase: after payment/order confirmation, not on checkout click. + - Add-to-cart: after the cart mutation succeeds. Cover each confirmed add path, including product-card add buttons, product-detail add buttons, and cart quantity-increment controls when they create or increase a cart line. If only some paths are safe to instrument, report the uncovered paths as optional follow-ups. + - Lead: after lead submission succeeds, not when a form opens. + - Signup: after account creation succeeds, not when signup starts. + - Subscription/trial: after plan/trial activation succeeds. + - Custom events: only if current docs support them, no standard event fits, and the event has a valid `custom_event_name`. + - If you install Pixel but intentionally omit `page_viewed`, include the reason in the report: too noisy, SPA route semantics unclear, no meaningful landing pages, already covered elsewhere, or deferred for a smaller first patch. + +4. Preserve existing privacy behavior. + - Follow existing consent, cookie, and data minimization gates. + - Do not add user identifiers or hashed PII unless current docs require/allow it and the repository already has an approved normalization path. + - For Pixel-only events, do not manually pass `oppref`, `source_url`, timestamps, or batching metadata. The SDK captures `oppref` from the landing page URL, stores it in the first-party `__oppref` cookie, adds `source_url`, timestamps events, and batches `measure` calls. + - If Pixel and CAPI are both in scope, preserve attribution context for CAPI by passing `oppref` from the URL or `__oppref` cookie to the server only through an existing conversion request path when the server cannot read the cookie itself. + +5. Keep config explicit. + - Browser-visible Pixel ID may use the repository's public env convention, such as `NEXT_PUBLIC_OPENAI_ADS_PIXEL_ID` in Next.js. + - If CAPI sends the same event, the browser-visible Pixel ID must be the same logical Pixel ID used by server-side CAPI. Separate env variable names are fine only as framework aliases for the same value. + - Do not use browser-visible config for CAPI credentials. + +## Event Deduplication + +When Pixel and CAPI both emit the same conversion, use the same Pixel ID, same event name, and same `event_id` in both the browser and server paths. Prefer an existing order ID, payment intent ID, lead ID, or signup transaction ID when it is stable and non-sensitive. If no stable ID exists, generate one at conversion start and persist it through the successful completion path. + +For Pixel, pass `event_id` in the optional fourth `oaiq("measure", ...)` options object. For CAPI, the same value is the event's `id`. For custom events, keep the same `custom_event_name` on both Pixel and CAPI. + +## Standard Event Selection + +Evaluate all primary supported event types before choosing the smallest safe set. Instrument high-confidence events now; put plausible but unconfirmed events in the report as optional follow-ups the advertiser can add if desired. + +- `page_viewed`: page load or important landing page view; usually rely on Pixel if browser support exists. +- `contents_viewed`: specific product, listing, article, plan, or content unit view, including interactions that happen after a page has loaded. +- `items_added`: add-to-cart or quantity increment succeeds; cover all confirmed successful add paths or report partial coverage. +- `checkout_started`: checkout session starts. +- `order_created`: order or payment is confirmed. +- `lead_created`: lead submission is accepted. +- `registration_completed`: account creation completes. +- `appointment_scheduled`: booking completes. +- `subscription_created`: subscription activates. +- `trial_started`: trial starts. +- `custom`: use only when no standard event maps cleanly. + +For Pixel custom events, call `oaiq("measure", "custom", { type: "custom" }, { custom_event_name: "" })`. Custom event names must be 1-64 characters, contain only letters, numbers, underscores, or dashes, start and end with a letter or number, and not match a standard event name. Use lowercase names for consistency. + +## Common Risks + +- Initializing the Pixel in both a root layout and a nested page. +- Rerunning the setup and adding duplicate event calls to a page that already publishes through a shared analytics helper. +- Emitting a conversion on attempted action instead of success. +- Putting Pixel user data on `measure` calls instead of `init`, or sending raw email/raw external IDs as Pixel user fields. +- Bypassing the repository's existing consent gates for Pixel measurement or Pixel user-data updates. +- Using a standard event name that is not supported by current docs. +- Duplicating a Pixel and CAPI event without a shared `event_id`. +- Adding browser plumbing for `oppref` when the server already has the `__oppref` cookie. +- Silently skipping obvious funnel events such as product views, add-to-cart, or checkout start without explaining whether they were inapplicable, ambiguous, or simply left as optional follow-ups. +- Adding Pixel logic before consent checks that other analytics obey. +- Coupling Pixel setup to checkout internals more than necessary. diff --git a/skills/.curated/openai-ads-conversions-setup/references/report-schema.md b/skills/.curated/openai-ads-conversions-setup/references/report-schema.md new file mode 100644 index 0000000..c8444b6 --- /dev/null +++ b/skills/.curated/openai-ads-conversions-setup/references/report-schema.md @@ -0,0 +1,71 @@ +# Setup Report Schema + +Use this structure for the final response after applying or reviewing instrumentation. + +```markdown +**OpenAI Ads Conversions Setup Report** + +Setup mode: pixel | capi | pixel+capi | review-only +Detected stack: + +Changed files: +- : + +Existing instrumentation found: +- + +Existing ad platform patterns used: +- + +Events instrumented: +- : + +Events already covered: +- : + +Events/surfaces skipped: +- : + +Optional follow-up events: +- : + +Pixel: +- Pixel ID/config: +- Initialization point: +- Docs syntax source: +- debug behavior: +- Pixel user data: +- Pixel opt_out: +- Consent handling: + +CAPI: +- Server boundary: +- Secret reference: +- Request/auth docs verified from: +- validate_only behavior: +- action_source/source_url: +- Browser-provided source_url validation: +- timestamp_ms: +- oppref handling: +- user data: +- opt_out handling: +- Failure behavior: + +Deduplication: +- event_id source: +- Pixel+CAPI pairing: + +Verification: +- : passed | failed | not run () + +Manual follow-ups: +- + +Risks / unknowns: +- + +Deployment review warning: +- Before deploying, review that this implementation satisfies your privacy, security, consent, and data handling requirements. +``` + +Never paste raw CAPI keys, customer data, or sensitive advertiser code snippets into the report. diff --git a/skills/.curated/openai-ads-conversions-setup/references/verification-checklist.md b/skills/.curated/openai-ads-conversions-setup/references/verification-checklist.md new file mode 100644 index 0000000..5e2e022 --- /dev/null +++ b/skills/.curated/openai-ads-conversions-setup/references/verification-checklist.md @@ -0,0 +1,71 @@ +# Verification Checklist + +## Static Checks + +- Pixel initialization appears once in the normal browser startup path. +- Pixel user data, if used, is sent through `oaiq("init", { user })`, not through `measure` calls. +- Late-arriving Pixel user data, such as checkout/login/signup fields, triggers a follow-up `init` without repeating `pixelId` after the first successful init. +- Existing OpenAI Ads instrumentation was inventoried before changes on reruns. +- Pixel code is not imported by server-only execution paths unless guarded correctly. +- CAPI code is server-only and never imported by browser bundles. +- CAPI auth config uses a server-only env/secret convention. +- New event coverage extends existing OpenAI Ads helpers/clients/adapters instead of creating duplicate initialization, duplicate CAPI clients, or new config conventions. +- Pixel+CAPI duplicate events use the same logical Pixel ID, same event name, and same stable `event_id`. +- New call sites are not already covered by a shared analytics wrapper, cart/checkout service, confirmation component, or server conversion helper. +- Existing Meta/Google/TikTok/other ad platform integrations were used only as local pattern evidence, not as the OpenAI Ads API contract. +- Event names and payload fields match current OpenAI Ads docs. +- Checked-in env templates and runtime defaults do not set CAPI `validate_only` to true unless the user explicitly requested validation-only behavior. +- CAPI request body uses current field names (`id`, `type`, `timestamp_ms`, `source_url`, `action_source`, `data`) and not removed legacy names. +- CAPI `timestamp_ms` is generated from the actual event time, not a stale backfill or future timestamp. +- CAPI event `type` matches the documented `data.type` family. +- CAPI custom events use event-level `custom_event_name`; Pixel custom events use the Pixel options object. +- Commerce CAPI totals use top-level `data.amount`; item-level `contents[].amount` is unit price or omitted when semantics are unclear. +- CAPI web events include `action_source: "web"` and a sanitized HTTP(S) `source_url` with origin plus pathname only. +- Non-web CAPI events use an appropriate documented `action_source` such as `mobile_app`, `offline`, `physical_store`, `phone_call`, `email`, or `other`. +- CAPI `source_url` sanitizer strips query/fragment and rejects or replaces non-HTTP(S) schemes. +- Browser-provided CAPI `sourceUrl` is accepted only when its origin matches the current request origin or a configured canonical site origin; otherwise the implementation falls back to a server-derived URL. +- If a configured canonical site origin exists, fallback `source_url` construction prefers it over request/internal proxy origins. +- Available `oppref` context is passed as a raw opaque value to CAPI and not decoded, normalized, transformed, or logged; server cookie access is preferred over client-supplied context. +- CAPI user data is event-scoped, documented, and uses hashed identity fields rather than raw email or raw external IDs. If no user data is sent, the report explains why. +- Existing consent or personalization opt-out behavior is preserved and mapped to documented `opt_out` fields when applicable. +- Existing consent gates control whether Pixel measurement and Pixel user-data updates are allowed. When measurement is allowed but the event should be opted out of future user-level personalization, the Pixel event uses documented `opt_out`. +- Pixel implementation relies on SDK-managed `oppref`, `source_url`, timestamps, and batching; manual plumbing for those fields is only added for CAPI when needed. +- Plausible supported events that are not instrumented are listed with reasons and optional follow-up guidance; if Pixel is installed and `page_viewed` is omitted, the reason is explicit. +- `items_added` covers all confirmed add-to-cart and quantity-increment success paths, or the report explicitly calls out partial coverage. +- Consent/privacy gating matches existing analytics behavior. +- Unsupported frontend surfaces such as mobile/native are skipped or reported as CAPI-only. + +## Suggested Commands + +Run the narrowest relevant commands the repository supports: + +- TypeScript: `npm run typecheck`, `pnpm typecheck`, `yarn typecheck`, or package-specific equivalents. +- Lint: `npm run lint`, `pnpm lint`, `yarn lint`, or package-specific equivalents. +- Tests: targeted unit tests for changed files, checkout/lead/signup handlers, or analytics adapters. +- Static helper: `python3 /scripts/verify_capi_secret_not_exposed.py `. +- Static helper: `python3 /scripts/verify_ads_setup.py --pixel-id --require pixel --require capi`. +- If Pixel+CAPI dedupe is in scope, include `--require dedupe --require shared-pixel-id`. +- If CAPI web attribution is in scope, include `--require oppref --require source-url`. + +`` is the directory containing this installed skill. + +The helper scripts run locally. By default, they report paths, line numbers, rules, and summaries without printing source-line context or transmitting repository contents. + +## Manual Checks + +- Confirm the Pixel script initializes only after consent when consent is required. +- Confirm docs-focused Pixel coverage when applicable: current loader/init syntax, late `user` init placement, Pixel options-level `opt_out`, and no user data on `measure`. +- Confirm docs-focused CAPI coverage when applicable: event-scoped `user`, `opt_out`, valid `timestamp_ms`, documented `action_source`, trusted/sanitized `source_url`, configured canonical origin fallback, raw opaque `oppref` pass-through, and batch failure behavior if batching exists. +- Confirm conversion event fires after a successful test conversion, not before. +- Confirm Pixel or CAPI failures do not fail, block, or materially slow the user flow unless explicitly intended. +- Confirm no secret value appears in browser devtools, source maps, build output, logs, or reports. +- Confirm CAPI uses `Content-Type: application/json` and bearer auth from a server-only secret. +- Confirm CAPI dry-run or test hooks can use `validate_only: true` where practical, but deployed/default config sends real events. +- Confirm any CAPI batching has a clear failure strategy, because one invalid event can fail the whole batch. +- Confirm route/controller tests cover success-only CAPI scheduling when practical. +- Confirm rerun reports separate existing events found, new events added, events already covered, and optional follow-ups. +- Confirm final diff is limited to conversion setup and nearby tests/config. + +## Failure Handling + +If a check fails, either fix the issue or leave the integration unapplied and report the exact blocker. Do not ship a known secret leak, duplicate conversion emission, or undocumented API call. diff --git a/skills/.curated/openai-ads-conversions-setup/scripts/verify_ads_setup.py b/skills/.curated/openai-ads-conversions-setup/scripts/verify_ads_setup.py new file mode 100644 index 0000000..6d2b20a --- /dev/null +++ b/skills/.curated/openai-ads-conversions-setup/scripts/verify_ads_setup.py @@ -0,0 +1,435 @@ +#!/usr/bin/env python3 +"""Static sanity checks for OpenAI Ads Pixel and CAPI integrations.""" + +from __future__ import annotations + +import argparse +import json +import os +import re +import sys +from pathlib import Path +from typing import Iterable + +SKIP_DIRS = { + ".git", + ".hg", + ".svn", + ".next", + ".nuxt", + ".turbo", + ".venv", + "build", + "coverage", + "dist", + "node_modules", + "out", + "target", + "test", + "tests", + "venv", + "__tests__", +} + +SKIP_FILES = { + "verify_capi_secret_not_exposed.py", + "verify_ads_setup.py", +} + +TEXT_EXTENSIONS = { + ".astro", + ".cjs", + ".erb", + ".go", + ".html", + ".java", + ".js", + ".json", + ".jsx", + ".mjs", + ".php", + ".py", + ".rb", + ".rs", + ".svelte", + ".ts", + ".tsx", + ".vue", + ".yaml", + ".yml", +} + +DOC_EXTENSIONS = { + ".md", + ".mdx", + ".rst", +} + + +def is_env_file(filename: str) -> bool: + return filename == ".env" or filename.startswith(".env.") + + +def is_env_template_file(filename: str) -> bool: + return is_env_file(filename) and ( + filename in {".env.example", ".env.sample", ".env.template"} + or filename.endswith((".example", ".sample", ".template")) + ) + + +PIXEL_MARKER_RE = re.compile( + r"(?i)(openai[_\-\s]?ads[_\-\s]?pixel|openai[_\-\s]?pixel|measurement[_\-\s]?pixel|OPENAI_ADS_PIXEL_ID)" +) +CAPI_MARKER_RE = re.compile( + r"(?i)(openai[_\-\s]?ads[_\-\s]?(conversions[_\-\s]?)?(api|capi)|conversions[_\-\s]?api|OPENAI_ADS_CONVERSIONS_API_KEY)" +) +EVENT_ID_RE = re.compile(r"(?i)\b(event[_\-]?id|eventId|dedupe|deduplication|idempotency)\b") +OPPREF_RE = re.compile(r"(?i)\b(__oppref|oppref)\b") +SOURCE_URL_RE = re.compile(r"(?i)\b(source_url|sourceUrl|sourceURL)\b") +CAPI_CONTEXT_RE = re.compile( + r"(?i)(bzr\.openai\.com/v1/events|/v1/events\?pid=|OPENAI_ADS_CONVERSIONS_API_KEY|" + r"\bconversions[_\-\s]?api\b|\bcapi\b)" +) +CAPI_LEGACY_FIELD_RE = re.compile( + r"(?i)(?:[\"']|\b)(event_name|event_time_epoch_ms|event_source_url|event_data)(?:[\"']|\b)\s*:" +) +VALIDATE_ONLY_TRUE_RE = re.compile( + r"(?i)^\s*(?:export\s+)?" + r"OPENAI_ADS(?:_(?:CAPI|CONVERSIONS|CONVERSIONS_API))?_VALIDATE_ONLY" + r"\s*[:=]\s*[\"']?(?:true|1|yes)[\"']?\s*(?:#.*)?$" +) +PUBLIC_CAPI_ENV_RE = re.compile( + r"\b(?:NEXT_PUBLIC|NUXT_PUBLIC|PUBLIC|REACT_APP|VITE)_" + r"(?:[A-Z0-9]+_)*(?:OPENAI_ADS|OPENAI|ADS|CAPI|CONVERSION|CONVERSIONS)" + r"(?:_[A-Z0-9]+)*_(?:API_KEY|KEY|SECRET|TOKEN)[A-Z0-9_]*\b", +) +PUBLIC_PIXEL_ENV_RE = re.compile( + r"\b(?:NEXT_PUBLIC|NUXT_PUBLIC|PUBLIC|REACT_APP|VITE)_OPENAI_ADS_PIXEL_ID\b" +) +SERVER_PIXEL_ENV_RE = re.compile(r"\bOPENAI_ADS_PIXEL_ID\b") +SHARED_PIXEL_GUIDANCE_RE = re.compile( + r"(?i)(same|shared|reuse|match|identical|alias|same value).{0,80}" + r"(pixel id|OPENAI_ADS_PIXEL_ID)" +) +ADS_MEASURE_EVENT_RE = re.compile( + r"(?is)(?:\bwindow\s*\.\s*)?\boaiq\s*(?:\?\.)?\(\s*" + r"[\"']measure[\"']\s*,\s*[\"']([a-z0-9_-]+)[\"']" +) +SUPPORTED_EVENT_NAMES = { + "appointment_scheduled", + "checkout_started", + "contents_viewed", + "custom", + "items_added", + "lead_created", + "order_created", + "page_viewed", + "registration_completed", + "subscription_created", + "trial_started", +} + + +def iter_candidate_files(root: Path, *, include_docs: bool = False) -> Iterable[Path]: + for current_root, dirs, files in os.walk(root): + dirs[:] = [ + directory + for directory in dirs + if directory not in SKIP_DIRS and not (Path(current_root) / directory).is_symlink() + ] + for filename in files: + if filename in SKIP_FILES: + continue + path = Path(current_root) / filename + suffix = path.suffix.lower() + if ( + is_env_file(filename) + or suffix in TEXT_EXTENSIONS + or (include_docs and suffix in DOC_EXTENSIONS) + ): + yield path + + +def read_file(path: Path) -> str: + try: + return path.read_text(encoding="utf-8", errors="ignore") + except OSError: + return "" + + +def first_hits( + root: Path, + pattern: re.Pattern[str], + limit: int = 20, + *, + include_docs: bool = False, +) -> list[dict[str, object]]: + hits: list[dict[str, object]] = [] + for path in iter_candidate_files(root, include_docs=include_docs): + text = read_file(path) + for line_number, line in enumerate(text.splitlines(), start=1): + if pattern.search(line): + hits.append( + { + "path": str(path.relative_to(root)), + "line": line_number, + } + ) + break + if len(hits) >= limit: + break + return hits + + +def literal_hits(root: Path, literal: str, limit: int = 20) -> list[dict[str, object]]: + if not literal: + return [] + + hits: list[dict[str, object]] = [] + for path in iter_candidate_files(root): + text = read_file(path) + for line_number, line in enumerate(text.splitlines(), start=1): + if literal in line: + hits.append( + { + "path": str(path.relative_to(root)), + "line": line_number, + } + ) + break + if len(hits) >= limit: + break + return hits + + +def unsupported_event_hits(root: Path, limit: int = 20) -> list[dict[str, object]]: + hits: list[dict[str, object]] = [] + for path in iter_candidate_files(root): + text = read_file(path) + for match in ADS_MEASURE_EVENT_RE.finditer(text): + event_name = match.group(1) + if event_name in SUPPORTED_EVENT_NAMES: + continue + + hits.append( + { + "path": str(path.relative_to(root)), + "line": text.count("\n", 0, match.start(1)) + 1, + "event_name": event_name, + } + ) + if len(hits) >= limit: + return hits + return hits + + +def legacy_capi_field_hits(root: Path, limit: int = 20) -> list[dict[str, object]]: + hits: list[dict[str, object]] = [] + for path in iter_candidate_files(root): + text = read_file(path) + if not CAPI_CONTEXT_RE.search(text): + continue + + for match in CAPI_LEGACY_FIELD_RE.finditer(text): + hits.append( + { + "path": str(path.relative_to(root)), + "line": text.count("\n", 0, match.start(1)) + 1, + "field_name": match.group(1), + } + ) + if len(hits) >= limit: + return hits + return hits + + +def validate_only_default_hits(root: Path, limit: int = 20) -> list[dict[str, object]]: + hits: list[dict[str, object]] = [] + for path in iter_candidate_files(root): + if not is_env_template_file(path.name): + continue + + text = read_file(path) + for line_number, line in enumerate(text.splitlines(), start=1): + if not VALIDATE_ONLY_TRUE_RE.search(line): + continue + + hits.append( + { + "path": str(path.relative_to(root)), + "line": line_number, + } + ) + if len(hits) >= limit: + return hits + return hits + + +def make_check( + name: str, passed: bool, hits: list[dict[str, object]], message: str +) -> dict[str, object]: + return { + "name": name, + "passed": passed, + "message": message, + "hits": hits, + } + + +def main() -> int: + parser = argparse.ArgumentParser( + description="Run static sanity checks for OpenAI Ads Pixel and CAPI instrumentation." + ) + parser.add_argument("root", nargs="?", default=".", help="Repository root to verify.") + parser.add_argument("--pixel-id", default="", help="Expected Pixel ID literal, if known.") + parser.add_argument( + "--capi-env", + default="OPENAI_ADS_CONVERSIONS_API_KEY", + help="Expected server-only env var or secret config name for CAPI.", + ) + parser.add_argument( + "--require", + action="append", + choices=( + "pixel", + "capi", + "dedupe", + "shared-pixel-id", + "supported-events", + "oppref", + "source-url", + ), + default=[], + help="Require a specific check to pass. Can be passed more than once.", + ) + args = parser.parse_args() + + root = Path(args.root).resolve() + + pixel_hits = ( + literal_hits(root, args.pixel_id) if args.pixel_id else first_hits(root, PIXEL_MARKER_RE) + ) + capi_marker_hits = first_hits(root, CAPI_MARKER_RE) + capi_env_hits = literal_hits(root, args.capi_env) if args.capi_env else [] + event_id_hits = first_hits(root, EVENT_ID_RE) + public_capi_env_hits = first_hits(root, PUBLIC_CAPI_ENV_RE) + public_pixel_env_hits = first_hits(root, PUBLIC_PIXEL_ENV_RE) + server_pixel_env_hits = first_hits(root, SERVER_PIXEL_ENV_RE) + shared_pixel_guidance_hits = first_hits(root, SHARED_PIXEL_GUIDANCE_RE, include_docs=True) + oppref_hits = first_hits(root, OPPREF_RE) + source_url_hits = first_hits(root, SOURCE_URL_RE) + event_name_unsupported_hits = unsupported_event_hits(root) + legacy_capi_fields = legacy_capi_field_hits(root) + validate_only_defaults = validate_only_default_hits(root) + has_public_and_server_pixel_config = bool(public_pixel_env_hits and server_pixel_env_hits) + + checks = [ + make_check( + "pixel_marker", + bool(pixel_hits), + pixel_hits, + "Found expected Pixel ID or OpenAI Ads Pixel marker.", + ), + make_check( + "capi_marker", + bool(capi_marker_hits or capi_env_hits), + capi_marker_hits + capi_env_hits, + "Found CAPI marker or expected server-only secret reference.", + ), + make_check( + "dedupe_marker", + bool(event_id_hits), + event_id_hits, + "Found event_id/deduplication/idempotency marker.", + ), + make_check( + "no_public_capi_env", + not public_capi_env_hits, + public_capi_env_hits, + "No browser-visible CAPI/API secret env references found.", + ), + make_check( + "shared_pixel_id_guidance", + not has_public_and_server_pixel_config or bool(shared_pixel_guidance_hits), + public_pixel_env_hits + server_pixel_env_hits + shared_pixel_guidance_hits, + "If browser and server Pixel ID config names both exist, docs/code explain they are the same logical Pixel ID.", + ), + make_check( + "supported_event_names", + not event_name_unsupported_hits, + event_name_unsupported_hits, + "No unsupported OpenAI Ads Pixel event names found in measure calls.", + ), + make_check( + "oppref_marker", + bool(oppref_hits), + oppref_hits, + "Found oppref or __oppref marker for CAPI attribution context.", + ), + make_check( + "source_url_marker", + bool(source_url_hits), + source_url_hits, + "Found source_url/sourceUrl marker for CAPI web source context.", + ), + make_check( + "no_legacy_capi_fields", + not legacy_capi_fields, + legacy_capi_fields, + "No removed legacy CAPI request field names found in CAPI-like files.", + ), + make_check( + "no_validate_only_default", + not validate_only_defaults, + validate_only_defaults, + "Checked-in env templates do not default OpenAI Ads CAPI validate_only mode to true.", + ), + ] + + required_to_check = { + "pixel": "pixel_marker", + "capi": "capi_marker", + "dedupe": "dedupe_marker", + "shared-pixel-id": "shared_pixel_id_guidance", + "supported-events": "supported_event_names", + "oppref": "oppref_marker", + "source-url": "source_url_marker", + } + check_by_name = {str(check["name"]): check for check in checks} + failed_required = [ + required + for required in args.require + if not check_by_name[required_to_check[required]]["passed"] + ] + failed_security = [ + check["name"] + for check in checks + if check["name"] == "no_public_capi_env" and not check["passed"] + ] + failed_correctness = [ + check["name"] + for check in checks + if check["name"] + in { + "supported_event_names", + "no_legacy_capi_fields", + "no_validate_only_default", + } + and not check["passed"] + ] + + result = { + "root": str(root), + "required": args.require, + "checks": checks, + "passed": not failed_required and not failed_security and not failed_correctness, + "failed_required": failed_required, + "failed_security": failed_security, + "failed_correctness": failed_correctness, + } + print(json.dumps(result, indent=2, sort_keys=True)) + return 0 if result["passed"] else 2 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/skills/.curated/openai-ads-conversions-setup/scripts/verify_capi_secret_not_exposed.py b/skills/.curated/openai-ads-conversions-setup/scripts/verify_capi_secret_not_exposed.py new file mode 100644 index 0000000..6058143 --- /dev/null +++ b/skills/.curated/openai-ads-conversions-setup/scripts/verify_capi_secret_not_exposed.py @@ -0,0 +1,336 @@ +#!/usr/bin/env python3 +"""Verify OpenAI Ads CAPI credentials are not exposed to browser-visible code.""" + +from __future__ import annotations + +import argparse +import json +import os +import re +import sys +from pathlib import Path +from typing import Iterable + +SKIP_DIRS = { + ".git", + ".hg", + ".svn", + ".next", + ".nuxt", + ".turbo", + ".venv", + "build", + "coverage", + "dist", + "node_modules", + "out", + "target", + "venv", +} + +SKIP_FILES = { + "verify_capi_secret_not_exposed.py", + "verify_ads_setup.py", +} + +TEXT_EXTENSIONS = { + ".astro", + ".cjs", + ".css", + ".erb", + ".go", + ".html", + ".java", + ".js", + ".json", + ".jsx", + ".mjs", + ".php", + ".py", + ".rb", + ".rs", + ".scss", + ".svelte", + ".ts", + ".tsx", + ".vue", + ".yaml", + ".yml", +} + + +def is_env_file(filename: str) -> bool: + return filename == ".env" or filename.startswith(".env.") + + +CLIENT_HINTS = { + "app", + "assets", + "browser", + "client", + "components", + "frontend", + "pages", + "public", + "src", + "static", + "ui", + "web", +} + +SERVER_ONLY_HINTS = { + "api", + "backend", + "server", +} + +BROWSER_EXTENSIONS = { + ".astro", + ".html", + ".js", + ".jsx", + ".mjs", + ".svelte", + ".ts", + ".tsx", + ".vue", +} + +STATIC_ASSET_EXTENSIONS = { + ".json", +} + +STATIC_ASSET_HINTS = { + "assets", + "public", + "static", +} + +PUBLIC_ENV_RE = re.compile( + r"\b(?:NEXT_PUBLIC|NUXT_PUBLIC|PUBLIC|REACT_APP|VITE)_" + r"(?:[A-Z0-9]+_)*(?:OPENAI_ADS|OPENAI|ADS|CAPI|CONVERSION|CONVERSIONS)" + r"(?:_[A-Z0-9]+)*_(?:API_KEY|KEY|SECRET|TOKEN)[A-Z0-9_]*\b", +) + +OPENAI_ADS_SECRET_RE = re.compile( + r"\b(" + r"OPENAI_ADS_CONVERSIONS_API_KEY|" + r"OPENAI_ADS_CAPI_KEY|" + r"OPENAI_CAPI_KEY|" + r"OPENAI_CONVERSIONS_API_KEY|" + r"CONVERSIONS_API_KEY|" + r"CAPI_API_KEY|" + r"CAPI_SECRET" + r")\b", + re.IGNORECASE, +) + +OPENAI_ADS_SECRET_ASSIGNMENT_RE = re.compile( + r"['\"]?\b(" + r"OPENAI_ADS_CONVERSIONS_API_KEY|" + r"OPENAI_ADS_CAPI_KEY|" + r"OPENAI_CAPI_KEY|" + r"OPENAI_CONVERSIONS_API_KEY|" + r"CONVERSIONS_API_KEY|" + r"CAPI_API_KEY|" + r"CAPI_SECRET" + r")\b['\"]?\s*[:=]\s*['\"]?([A-Za-z0-9_\-./+=]{12,})['\"]?", + re.IGNORECASE, +) +PLACEHOLDER_VALUE_RE = re.compile( + r"(?i)^(process\.env|os\.environ|env\.|import\.meta\.env|your|replace|placeholder|example|todo)" +) + +LITERAL_SECRET_RE = re.compile( + r"(?i)\b(openai|ads|capi|conversion|conversions)[a-z0-9_\-.\s]{0,50}" + r"(api[_\-\s]?key|secret|token)\b\s*[:=]\s*['\"][A-Za-z0-9_\-./+=]{16,}['\"]" +) + +MASK_VALUE_RE = re.compile(r"(['\"])[A-Za-z0-9_\-./+=]{12,}(['\"])") +UNQUOTED_SECRET_VALUE_RE = re.compile( + r"(?i)(\b[A-Z0-9_.-]*(?:API[_-]?KEY|CAPI|SECRET|TOKEN)[A-Z0-9_.-]*\b\s*[:=]\s*)" + r"([^\s#,'\"]{8,})" +) + + +def iter_candidate_files(root: Path) -> Iterable[Path]: + for current_root, dirs, files in os.walk(root): + dirs[:] = [ + directory + for directory in dirs + if directory not in SKIP_DIRS and not (Path(current_root) / directory).is_symlink() + ] + + for filename in files: + if filename in SKIP_FILES: + continue + path = Path(current_root) / filename + if is_env_file(filename) or path.suffix.lower() in TEXT_EXTENSIONS: + yield path + + +def is_client_visible(path: Path, root: Path) -> bool: + rel = path.relative_to(root) + lower_parts = {part.lower() for part in rel.parts} + if path.suffix.lower() in {".html", ".css", ".scss"}: + return True + if path.suffix.lower() in STATIC_ASSET_EXTENSIONS and bool(lower_parts & STATIC_ASSET_HINTS): + return True + if path.suffix.lower() in BROWSER_EXTENSIONS and bool(lower_parts & STATIC_ASSET_HINTS): + return True + if bool(lower_parts & SERVER_ONLY_HINTS): + return False + return path.suffix.lower() in BROWSER_EXTENSIONS and bool(lower_parts & CLIENT_HINTS) + + +def masked_context(line: str) -> str: + masked = MASK_VALUE_RE.sub(r"\1***\2", line.strip()) + masked = UNQUOTED_SECRET_VALUE_RE.sub(r"\1***", masked) + return masked[:240] + + +def has_literal_openai_ads_secret_assignment(line: str) -> bool: + match = OPENAI_ADS_SECRET_ASSIGNMENT_RE.search(line) + if not match: + return False + return PLACEHOLDER_VALUE_RE.search(match.group(2)) is None + + +def maybe_add_context( + finding: dict[str, object], line: str, include_context: bool +) -> dict[str, object]: + if include_context: + finding["context"] = masked_context(line) + return finding + + +def scan_file(path: Path, root: Path, include_context: bool) -> list[dict[str, object]]: + try: + text = path.read_text(encoding="utf-8", errors="ignore") + except OSError: + return [] + + client_visible = is_client_visible(path, root) + findings: list[dict[str, object]] = [] + + for line_number, line in enumerate(text.splitlines(), start=1): + public_env_match = PUBLIC_ENV_RE.search(line) + if public_env_match: + findings.append( + maybe_add_context( + { + "severity": "high", + "rule": "public_env_secret_name", + "path": str(path.relative_to(root)), + "line": line_number, + "message": "Potential CAPI/API secret uses a browser-visible env var prefix.", + }, + line, + include_context, + ) + ) + + secret_name_match = OPENAI_ADS_SECRET_RE.search(line) + if secret_name_match and client_visible: + findings.append( + maybe_add_context( + { + "severity": "high", + "rule": "capi_secret_name_in_client_file", + "path": str(path.relative_to(root)), + "line": line_number, + "message": "Potential OpenAI Ads CAPI secret reference appears in client-visible code.", + }, + line, + include_context, + ) + ) + + if has_literal_openai_ads_secret_assignment(line): + findings.append( + maybe_add_context( + { + "severity": "high", + "rule": "literal_capi_secret_assignment", + "path": str(path.relative_to(root)), + "line": line_number, + "message": "Potential literal OpenAI Ads CAPI credential assignment found.", + }, + line, + include_context, + ) + ) + + literal_match = LITERAL_SECRET_RE.search(line) + if literal_match: + findings.append( + maybe_add_context( + { + "severity": "high", + "rule": "literal_secret_like_assignment", + "path": str(path.relative_to(root)), + "line": line_number, + "message": "Potential literal conversion API credential assignment found.", + }, + line, + include_context, + ) + ) + + return findings + + +def exit_code(findings: list[dict[str, object]], fail_on: str) -> int: + if fail_on == "none": + return 0 + severities = {str(finding["severity"]) for finding in findings} + if fail_on == "high" and "high" in severities: + return 2 + if fail_on == "medium" and severities & {"high", "medium"}: + return 1 + return 0 + + +def main() -> int: + parser = argparse.ArgumentParser( + description="Verify OpenAI Ads CAPI credentials are not exposed to browser-visible code." + ) + parser.add_argument("root", nargs="?", default=".", help="Repository root to verify.") + parser.add_argument( + "--include-context", + action="store_true", + help="Include masked source-line context in findings. By default, only paths, line numbers, rules, and summaries are reported.", + ) + parser.add_argument( + "--fail-on", + choices=("high", "medium", "none"), + default="high", + help="Minimum severity that should produce a non-zero exit code.", + ) + args = parser.parse_args() + + root = Path(args.root).resolve() + findings: list[dict[str, object]] = [] + for path in iter_candidate_files(root): + findings.extend(scan_file(path, root, args.include_context)) + + result = { + "root": str(root), + "output_policy": { + "runs_locally": True, + "transmits_repository_contents": False, + "source_context_included": args.include_context, + }, + "summary": { + "total": len(findings), + "high": sum(1 for finding in findings if finding["severity"] == "high"), + "medium": sum(1 for finding in findings if finding["severity"] == "medium"), + }, + "findings": findings, + } + print(json.dumps(result, indent=2, sort_keys=True)) + return exit_code(findings, args.fail_on) + + +if __name__ == "__main__": + sys.exit(main())