Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
160 changes: 142 additions & 18 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,43 +5,59 @@ on:
types: [published]

jobs:
publish:
name: Publish
verify:
name: Verify release candidate
runs-on: ubuntu-latest
timeout-minutes: 30
permissions:
contents: write
id-token: write
contents: read

steps:
- uses: actions/checkout@v7
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
fetch-depth: 0
persist-credentials: false

- name: Set up Node.js
uses: actions/setup-node@v7
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7
with:
node-version: "22"
registry-url: "https://registry.npmjs.org"

- name: Upgrade npm (OIDC trusted publishing needs npm >= 11.5.1)
run: npm install -g npm@latest
- name: Install pinned npm for trusted publishing
run: npm install --global npm@11.12.1

- name: Install dependencies
run: npm ci

- name: Verify release tag matches package version
- name: Verify release tag matches package version and protected main
env:
RELEASE_TAG: ${{ github.event.release.tag_name }}
run: |
set -euo pipefail
PACKAGE_VERSION="$(node -p "require('./package.json').version")"
if [ "$RELEASE_TAG" != "v$PACKAGE_VERSION" ]; then
echo "::error::Release tag $RELEASE_TAG does not match package version $PACKAGE_VERSION"
exit 1
fi

git fetch --no-tags origin +refs/heads/main:refs/remotes/origin/main
RELEASE_COMMIT="$(git rev-parse "$RELEASE_TAG^{commit}")"
if [ "$RELEASE_COMMIT" != "$(git rev-parse HEAD)" ]; then
echo "::error::Checked-out commit does not match $RELEASE_TAG"
exit 1
fi
if ! git merge-base --is-ancestor "$RELEASE_COMMIT" origin/main; then
echo "::error::$RELEASE_TAG is not reachable from protected main"
exit 1
fi

- name: Audit dependencies
run: npm audit --audit-level=low

- name: Check for committed credentials
run: npm run check:secrets

- name: TypeScript build check
run: npx tsc --noEmit

Expand All @@ -57,19 +73,127 @@ jobs:
- name: Build
run: npm run build

- name: Validate executable snippets
run: npm run snippets:check

- name: Install and exercise the exact package tarball
run: npm run smoke:package

- name: Build signed snippet manifest
run: npm run snippets:build -- --source-commit "$GITHUB_SHA"
run: npm run snippets:build -- --source-commit "$(git rev-parse HEAD)"

- name: Attach snippet manifest to release
- name: Pack the verified package and publisher
run: |
set -euo pipefail
ARTIFACT_DIR="$RUNNER_TEMP/release-artifact"
mkdir -p "$ARTIFACT_DIR/snippets"

npm pack --ignore-scripts --json --pack-destination "$ARTIFACT_DIR" \
> "$RUNNER_TEMP/npm-pack.json"
jq -e '.[0] | {filename, integrity, shasum}' \
"$RUNNER_TEMP/npm-pack.json" > "$ARTIFACT_DIR/pack-metadata.json"
cp artifacts/snippets/* "$ARTIFACT_DIR/snippets/"

NPM_ROOT="$(npm root --global)"
tar -czf "$ARTIFACT_DIR/npm-cli-11.12.1.tgz" -C "$NPM_ROOT" npm
(
cd "$ARTIFACT_DIR"
find . -type f ! -name artifact.sha256 -print0 \
| sort -z \
| xargs -0 sha256sum > artifact.sha256
)

- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: verified-npm-package
path: ${{ runner.temp }}/release-artifact/
if-no-files-found: error
retention-days: 1

publish:
name: Publish verified package to npm
runs-on: ubuntu-latest
timeout-minutes: 15
needs: verify
permissions:
contents: read
id-token: write

steps:
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
with:
name: verified-npm-package
path: ${{ runner.temp }}/release-artifact

- name: Set up Node.js
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7
with:
node-version: "22"
registry-url: "https://registry.npmjs.org"

# The only package-manager code used with OIDC is the archive prepared by
# the unprivileged verify job and checked before extraction.
- name: Publish the exact verified tarball
working-directory: ${{ runner.temp }}/release-artifact
run: |
set -euo pipefail
sha256sum -c artifact.sha256

mkdir "$RUNNER_TEMP/npm-cli"
tar -xzf npm-cli-11.12.1.tgz -C "$RUNNER_TEMP/npm-cli"
NPM=(node "$RUNNER_TEMP/npm-cli/npm/bin/npm-cli.js")

PACKAGE_FILE="$(jq -er '.filename' pack-metadata.json)"
EXPECTED_INTEGRITY="$(jq -er '.integrity' pack-metadata.json)"
NAME="$(tar -xOf "$PACKAGE_FILE" package/package.json | jq -er '.name')"
VERSION="$(tar -xOf "$PACKAGE_FILE" package/package.json | jq -er '.version')"
EXISTING_INTEGRITY="$(timeout 30s "${NPM[@]}" view "$NAME@$VERSION" dist.integrity --json 2>/dev/null | jq -r '. // empty' || true)"

if [ -n "$EXISTING_INTEGRITY" ]; then
if [ "$EXISTING_INTEGRITY" != "$EXPECTED_INTEGRITY" ]; then
echo "::error::npm $NAME@$VERSION exists with unexpected integrity"
exit 1
fi
echo "npm $NAME@$VERSION already matches the verified tarball; continuing recovery."
else
timeout 10m "${NPM[@]}" publish "$PACKAGE_FILE" \
--ignore-scripts --provenance --access public
fi

for attempt in $(seq 1 12); do
PUBLIC_INTEGRITY="$(timeout 30s "${NPM[@]}" view "$NAME@$VERSION" dist.integrity --json 2>/dev/null | jq -r '. // empty' || true)"
PUBLIC_LATEST="$(timeout 30s "${NPM[@]}" view "$NAME" dist-tags.latest --json 2>/dev/null | jq -r '. // empty' || true)"
if [ "$PUBLIC_INTEGRITY" = "$EXPECTED_INTEGRITY" ] && [ "$PUBLIC_LATEST" = "$VERSION" ]; then
echo "Verified npm $NAME@$VERSION integrity and latest tag."
exit 0
fi
sleep 5
done

echo "::error::npm public readback did not converge to the verified release"
exit 1

release_assets:
name: Attach verified release assets
runs-on: ubuntu-latest
timeout-minutes: 10
needs: publish
permissions:
contents: write

steps:
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
with:
name: verified-npm-package
path: ${{ runner.temp }}/release-artifact

- name: Attach checksummed snippet manifest to release
working-directory: ${{ runner.temp }}/release-artifact
env:
GH_TOKEN: ${{ github.token }}
run: gh release upload "${{ github.event.release.tag_name }}" artifacts/snippets/* --clobber

# Authentication via OIDC Trusted Publishing (configured on npmjs.com).
# No NODE_AUTH_TOKEN needed; OIDC also satisfies the package 2FA requirement
# and produces provenance automatically.
- name: Publish to npm
run: npm publish --provenance --access public
GH_REPO: ${{ github.repository }}
RELEASE_TAG: ${{ github.event.release.tag_name }}
run: |
set -euo pipefail
sha256sum -c artifact.sha256
gh release upload "$RELEASE_TAG" snippets/* --clobber
16 changes: 16 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,22 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [1.2.2] - 2026-08-11

### Added

- Document a coverage-gated permit-to-production flow using live permit search
results and per-well monthly production history.
- Add well-permit, drilling-data, and oil-well-production discovery keywords.

### Fixed

- Route futures contract codes and named helpers through instrument-generic
Brent, WTI, Gasoil, and EU-carbon paths while retaining explicit legacy
slugs as backward-compatible inputs.
- Unwrap the production `{ well_permits, meta }` search envelope through the new
`searchLatest()` method while preserving the existing `search()` return type.

## [1.2.1] - 2026-08-11

### Fixed
Expand Down
29 changes: 29 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,35 @@ The reviewed standalone form is
type-checks and executes it against production-shaped fixtures, then publishes
its exact code and checksum in the release snippet manifest.

## Permit To Production

Well-level production coverage is narrower than permit coverage. Check the live
coverage response before following a permit into monthly production history:

```typescript
const summary = await client.wellProduction.summary();
if (!summary.coverage) {
throw new Error("MALFORMED_RESPONSE: well-production coverage is missing");
}
const coveredStates = new Set(summary.coverage.well_level_states_with_data ?? []);
const permits = await client.ei.wellPermits.searchLatest({
states: "TX",
well_name: "Eagle",
});

for (const permit of permits) {
if (!coveredStates.has(permit.state_code) || !/^\d{14}$/.test(permit.api_number ?? "")) {
continue;
}

const production = await client.wellProduction.wellDetail(permit.api_number!);
console.log(permit.well.name, production.data);
}
```

An empty search or history is a valid data state. Do not infer nationwide
well-level coverage from the presence of permit data or an SDK method.

## CommonJS

```javascript
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 5 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "oilpriceapi",
"version": "1.2.1",
"version": "1.2.2",
"description": "Official Node.js SDK for source-timestamped OilPriceAPI energy data",
"type": "module",
"main": "./dist/cjs/index.js",
Expand Down Expand Up @@ -51,7 +51,10 @@
"energy",
"alerts",
"webhook",
"notifications"
"notifications",
"well-permits",
"drilling-data",
"oil-well-production"
],
"author": "OilPriceAPI <support@oilpriceapi.com>",
"license": "MIT",
Expand Down
40 changes: 33 additions & 7 deletions src/resources/ei/well-permits.ts
Original file line number Diff line number Diff line change
Expand Up @@ -221,7 +221,7 @@ export interface WellPermitSearchQuery {
* states.forEach(s => console.log(`${s.state}: ${s.permit_count} permits`));
*
* // Search permits
* const results = await client.ei.wellPermits.search({
* const results = await client.ei.wellPermits.searchLatest({
* states: 'TX,NM',
* well_name: 'Eagle'
* });
Expand Down Expand Up @@ -320,12 +320,42 @@ export class EIWellPermitsResource {
}

/**
* Search well permits
* Search well permits using the legacy flat record contract.
*
* @param query - Search query parameters
* @returns Array of matching well permit records
*/
async search(query: WellPermitSearchQuery): Promise<WellPermitRecord[]> {
const params = this.searchParams(query);
const response = await this.client["request"]<
WellPermitRecord[] | { data: WellPermitRecord[] }
>("/v1/ei/well-permits/search", params);

return Array.isArray(response) ? response : response.data;
}

/**
* Search the current rich well-permit dataset.
*
* This method matches the production response without changing the public
* return type of {@link search} in a patch release.
*
* @param query - Search query parameters
* @returns Array of rich well-permit records
*/
async searchLatest(query: WellPermitSearchQuery): Promise<LatestWellPermit[]> {
const params = this.searchParams(query);
const response = await this.client["request"]<
| LatestWellPermit[]
| { data: LatestWellPermit[] }
| { well_permits: LatestWellPermit[]; meta?: Record<string, unknown> }
>("/v1/ei/well-permits/search", params);

if (Array.isArray(response)) return response;
return "well_permits" in response ? response.well_permits : response.data;
}

private searchParams(query: WellPermitSearchQuery): Record<string, string> {
const params: Record<string, string> = {};

if (query.states) params.states = query.states;
Expand All @@ -340,10 +370,6 @@ export class EIWellPermitsResource {
if (query.start_date) params.start_date = query.start_date;
if (query.end_date) params.end_date = query.end_date;

const response = await this.client["request"]<
WellPermitRecord[] | { data: WellPermitRecord[] }
>("/v1/ei/well-permits/search", params);

return Array.isArray(response) ? response : response.data;
return params;
}
}
Loading