Leon4gr45 commited on
Commit
048b1e8
·
verified ·
1 Parent(s): 81b285e

Upload folder using huggingface_hub

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .golangci.yaml +68 -0
  2. .goreleaser.yaml +45 -0
  3. .grype/config.yaml +5 -0
  4. .hfignore +40 -0
  5. .mcp.json +9 -0
  6. .nvmrc +1 -0
  7. .semgrepignore +9 -0
  8. .spectral.yaml +9 -0
  9. .syft.yaml +7 -0
  10. .vscode/default.code-workspace +12 -0
  11. .vscode/launch.json +102 -0
  12. .vscode/settings.json +32 -0
  13. AGENTS.md +323 -0
  14. Agent.md +148 -0
  15. CLAUDE.md +3 -0
  16. CODE_OF_CONDUCT.md +68 -0
  17. CONTRIBUTING.md +53 -0
  18. Dockerfile +98 -0
  19. LICENSE +202 -0
  20. Makefile +356 -0
  21. README.md +158 -5
  22. SECURITY.md +29 -0
  23. api/api.gen.go +0 -0
  24. api/api.go +2 -0
  25. api/client/go/README.md +65 -0
  26. api/client/go/client.gen.go +0 -0
  27. api/client/go/client.go +48 -0
  28. api/client/go/client_test.go +327 -0
  29. api/client/go/codegen.yaml +11 -0
  30. api/client/go/error.go +14 -0
  31. api/client/javascript/.gitignore +23 -0
  32. api/client/javascript/.npmignore +27 -0
  33. api/client/javascript/.npmrc +1 -0
  34. api/client/javascript/Makefile +42 -0
  35. api/client/javascript/README.md +267 -0
  36. api/client/javascript/biome.json +67 -0
  37. api/client/javascript/index.ts +1 -0
  38. api/client/javascript/orval.config.ts +38 -0
  39. api/client/javascript/package.json +102 -0
  40. api/client/javascript/patches/openapi-typescript.patch +0 -0
  41. api/client/javascript/pnpm-lock.yaml +0 -0
  42. api/client/javascript/pnpm-workspace.yaml +10 -0
  43. api/client/javascript/scripts/add-as-const.ts +16 -0
  44. api/client/javascript/scripts/generate.ts +51 -0
  45. api/client/javascript/src/client/addons.ts +122 -0
  46. api/client/javascript/src/client/apps.ts +335 -0
  47. api/client/javascript/src/client/billing.ts +525 -0
  48. api/client/javascript/src/client/common.ts +66 -0
  49. api/client/javascript/src/client/customers.ts +672 -0
  50. api/client/javascript/src/client/debug.ts +25 -0
.golangci.yaml ADDED
@@ -0,0 +1,68 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # yaml-language-server: $schema=https://golangci-lint.run/jsonschema/golangci.jsonschema.json
2
+ version: "2"
3
+ linters:
4
+ default: none
5
+ enable:
6
+ - bodyclose
7
+ - errcheck
8
+ - govet
9
+ - ineffassign
10
+ - misspell
11
+ - nolintlint
12
+ - sloglint
13
+ - staticcheck
14
+ - unconvert
15
+ # FIXME: re-enable following linters
16
+ #- unparam
17
+ #- unused
18
+ - whitespace
19
+ #- wsl
20
+ settings:
21
+ misspell:
22
+ locale: US
23
+ nolintlint:
24
+ require-specific: true
25
+ revive:
26
+ confidence: 0
27
+ staticcheck:
28
+ checks:
29
+ - "all"
30
+ - "-QF1008"
31
+ - "-SA1019" # https://staticcheck.dev/docs/checks/#SA1019
32
+ # FIXME: re-enable the following checks
33
+ - "-ST1003"
34
+ - "-ST1016"
35
+ exclusions:
36
+ generated: lax
37
+ presets:
38
+ - comments
39
+ - common-false-positives
40
+ - legacy
41
+ - std-error-handling
42
+ paths:
43
+ - collector/benthos/internal
44
+ - third_party$
45
+ - builtin$
46
+ - examples$
47
+ formatters:
48
+ enable:
49
+ - gci
50
+ - gofmt
51
+ - gofumpt
52
+ - goimports
53
+ settings:
54
+ gci:
55
+ sections:
56
+ - standard
57
+ - default
58
+ - prefix(github.com/openmeterio/openmeter)
59
+ goimports:
60
+ local-prefixes:
61
+ - github.com/openmeterio/openmeter
62
+ exclusions:
63
+ generated: lax
64
+ paths:
65
+ - collector/benthos/internal
66
+ - third_party$
67
+ - builtin$
68
+ - examples$
.goreleaser.yaml ADDED
@@ -0,0 +1,45 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # This is an example .goreleaser.yml file with some sensible defaults.
2
+ # Make sure to check the documentation at https://goreleaser.com
3
+ project_name: openmeter
4
+
5
+ dist: build/dist
6
+
7
+ gomod:
8
+ proxy: true
9
+
10
+ before:
11
+ hooks:
12
+ - go mod tidy
13
+ builds:
14
+ - env:
15
+ - CGO_ENABLED=0
16
+ main: .
17
+ goos:
18
+ - linux
19
+ - windows
20
+ - darwin
21
+
22
+ archives:
23
+ - format: tar.gz
24
+ # this name template makes the OS and Arch compatible with the results of uname.
25
+ name_template: >-
26
+ {{ .ProjectName }}_
27
+ {{- title .Os }}_
28
+ {{- if eq .Arch "amd64" }}x86_64
29
+ {{- else if eq .Arch "386" }}i386
30
+ {{- else }}{{ .Arch }}{{ end }}
31
+ {{- if .Arm }}v{{ .Arm }}{{ end }}
32
+ # use zip for windows archives
33
+ format_overrides:
34
+ - goos: windows
35
+ format: zip
36
+ checksum:
37
+ name_template: "checksums.txt"
38
+ snapshot:
39
+ name_template: "{{ incpatch .Version }}-next"
40
+ changelog:
41
+ sort: asc
42
+ filters:
43
+ exclude:
44
+ - "^docs:"
45
+ - "^test:"
.grype/config.yaml ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+
2
+ ignore:
3
+ # Nats server admin api authorization bypass: https://github.com/advisories/ghsa-fhg8-qxh5-7q3w
4
+ # This is not an issue, as we never start a nats server in our codebase
5
+ - vulnerability: GHSA-fhg8-qxh5-7q3w
.hfignore ADDED
@@ -0,0 +1,40 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ .git/
2
+ .devenv/
3
+ .direnv/
4
+ .env.local
5
+ .pre-commit-config.yaml
6
+ build/
7
+ config.yaml
8
+ tmp/
9
+ .tmp.*
10
+ .DS_Store
11
+ go.work
12
+ go.work.sum
13
+ __debug_bin*
14
+ openmeter.log
15
+ .gocache
16
+ .gomodcache
17
+ .claude/
18
+ CLAUDE.local.md
19
+ api/v3/templates/chi-middleware.tmpl
20
+ .codegraph/
21
+ node_modules/
22
+ dist/
23
+ .agents/
24
+ .vscode/
25
+ .github/
26
+ .spectral.yaml
27
+ .syft.yaml
28
+ .fossa.yaml
29
+ .editorconfig
30
+ .coderabbit.yaml
31
+ .golangci.yaml
32
+ .golangci-fast.yaml
33
+ .goreleaser.yaml
34
+ .mcp.json
35
+ .semgrepignore
36
+ atlas.hcl
37
+ flake.lock
38
+ flake.nix
39
+ justfile
40
+ Makefile
.mcp.json ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "mcpServers": {
3
+ "codegraph": {
4
+ "type": "stdio",
5
+ "command": "codegraph",
6
+ "args": ["serve", "--mcp"]
7
+ }
8
+ }
9
+ }
.nvmrc ADDED
@@ -0,0 +1 @@
 
 
1
+ v26.4.0
.semgrepignore ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ api/client/python/src/openmeter/_serialization.py
2
+ **/*_test.go
3
+
4
+ # These are only included due to bugous //nosemgrep support in current version, please remove them once the semgrep has been upgraded
5
+
6
+ openmeter/billing/service/seq.go
7
+ collector/benthos/input/otel_log.go
8
+ openmeter/productcatalog/feature/connector.go
9
+ openmeter/watermill/driver/kafka/broker.go
.spectral.yaml ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ extends: [[spectral:oas, recommended]]
2
+ rules:
3
+ info-contact: off
4
+
5
+ # Disabled due to TypeSpec conversion
6
+ oas3-valid-schema-example: off
7
+ no-$ref-siblings: off
8
+ path-params: off
9
+ oas3-unused-component: off
.syft.yaml ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ select-catalogers:
2
+ - -go-module-binary-cataloger
3
+ - -github-action-workflow-usage-cataloger
4
+ - -github-actions-usage-cataloger
5
+
6
+ exclude:
7
+ - "./.devenv"
.vscode/default.code-workspace ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "folders": [
3
+ {
4
+ "name": "root",
5
+ "path": "../"
6
+ },
7
+ {
8
+ "name": "typespec",
9
+ "path": "../api/spec"
10
+ }
11
+ ]
12
+ }
.vscode/launch.json ADDED
@@ -0,0 +1,102 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ // Use IntelliSense to learn about possible attributes.
3
+ // Hover to view descriptions of existing attributes.
4
+ // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
5
+ "version": "0.2.0",
6
+ "configurations": [
7
+ {
8
+ "name": "Launch Server",
9
+ "type": "go",
10
+ "request": "launch",
11
+ "mode": "auto",
12
+ "program": "${workspaceFolder}/cmd/server",
13
+ "args": [
14
+ "--config",
15
+ "${workspaceFolder}/config.yaml",
16
+ "--stripe-disable-webhook-registration",
17
+ ]
18
+ },
19
+ {
20
+ "name": "Launch Sink-Worker",
21
+ "type": "go",
22
+ "request": "launch",
23
+ "mode": "auto",
24
+ "program": "${workspaceFolder}/cmd/sink-worker",
25
+ "args": [
26
+ "--config",
27
+ "${workspaceFolder}/config.yaml",
28
+ // Let's prevent port collision with server
29
+ "--telemetry-address",
30
+ ":10001",
31
+ ]
32
+ },
33
+ {
34
+ "name": "Launch Balance Worker",
35
+ "type": "go",
36
+ "request": "launch",
37
+ "mode": "auto",
38
+ "program": "${workspaceFolder}/cmd/balance-worker",
39
+ "args": [
40
+ "--config",
41
+ "${workspaceFolder}/config.yaml",
42
+ "--telemetry-address",
43
+ ":10002"
44
+ ]
45
+ },
46
+ {
47
+ "name": "Launch Notification Service",
48
+ "type": "go",
49
+ "request": "launch",
50
+ "mode": "auto",
51
+ "program": "${workspaceFolder}/cmd/notification-service",
52
+ "args": [
53
+ "--config",
54
+ "${workspaceFolder}/config.yaml",
55
+ "--telemetry-address",
56
+ ":10003"
57
+ ]
58
+ },
59
+ {
60
+ "name": "Launch recalculate entitlement snapshots",
61
+ "type": "go",
62
+ "request": "launch",
63
+ "mode": "auto",
64
+ "program": "${workspaceFolder}/cmd/jobs",
65
+ "args": [
66
+ "--config",
67
+ "${workspaceFolder}/config.yaml",
68
+ "entitlement",
69
+ "recalculate-balance-snapshots"
70
+ ]
71
+ },
72
+ {
73
+ "name": "Launch billing-worker Service",
74
+ "type": "go",
75
+ "request": "launch",
76
+ "mode": "auto",
77
+ "program": "${workspaceFolder}/cmd/billing-worker",
78
+ "args": [
79
+ "--config",
80
+ "${workspaceFolder}/config.yaml",
81
+ "--telemetry-address",
82
+ ":10004",
83
+ // force foreground billing advancement strategy, or billing-worker will not start (this way the config can be set to queued to validate that in local dev mode)
84
+ "--billing-advancement-strategy=foreground",
85
+ ]
86
+ },
87
+ {
88
+ "name": "Launch periodic jobs",
89
+ "type": "go",
90
+ "request": "launch",
91
+ "mode": "auto",
92
+ "program": "${workspaceFolder}/cmd/jobs",
93
+ "args": [
94
+ "--config",
95
+ "${workspaceFolder}/config.yaml",
96
+ "billing",
97
+ "subscriptionsync",
98
+ "list"
99
+ ]
100
+ },
101
+ ]
102
+ }
.vscode/settings.json ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "[helm]": {
3
+ "editor.formatOnSave": false
4
+ },
5
+ "go.testEnvVars": {
6
+ "TZ": "UTC",
7
+ "POSTGRES_HOST": "127.0.0.1",
8
+ "OPENMETER_ADDRESS": "http://127.0.0.1:38888",
9
+ // Useful when the schema is evolving during development (currently only supported
10
+ // by billing)
11
+ // "TEST_DISABLE_ATLAS": "true"
12
+ },
13
+ "gopls": {
14
+ "formatting.gofumpt": true
15
+ },
16
+ "go.lintTool": "golangci-lint",
17
+ "go.lintFlags": [
18
+ "--fast",
19
+ "--fix",
20
+ "-c",
21
+ ".golangci.yaml"
22
+ ],
23
+ "files.exclude": {
24
+ "**/node_modules": false
25
+ },
26
+ "typespec.tsp-server.path": "${workspaceFolder}/api/spec/node_modules/@typespec/compiler",
27
+ // dynamic forces confluent-kafka-go to build against local librdkafka
28
+ // wireinject is kept for gopls so it can analyse wire.go injection files
29
+ "go.buildTags": "wireinject,dynamic",
30
+ // Tests must NOT include wireinject — it causes wire stubs to run instead of wire_gen.go
31
+ "go.testTags": "dynamic"
32
+ }
AGENTS.md ADDED
@@ -0,0 +1,323 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # OpenMeter
2
+
3
+ OpenMeter is a usage metering and billing platform for AI and DevTool companies, built in Go.
4
+
5
+ ## Quick Reference
6
+
7
+ Use the `Makefile` for all common tasks. A `justfile` also exists but is seldom used.
8
+ OpenMeter is a metering and billing platform with usage based pricing and access control.
9
+
10
+ ## Tips for working with the codebase
11
+
12
+ If during your work anything confuses you or something isn't trivial for you, please augment AGENTS.md with your findings so next time it will be easier for you. AGENTS.md files are for you to edit and update as you go so you can interact with the codebase the most effectively.
13
+
14
+ Development commands are run via `Makefile`, it contains all commonly used commands during development. A `justfile` is also present but seldom used. Use the Makefile commands for common tasks like running tests, generating code, linting, etc.
15
+ The committed `.nvmrc` is the GitHub Actions source of truth for Node-based jobs on GitHub-hosted runners. Keep it aligned with the Nix `.#ci` shell's `node -v`; `flake.nix` refreshes it in `enterShell`, and CI validates the file against the Nix shell before running builds.
16
+
17
+ ## AGENTS.md maintenance
18
+
19
+ - Treat this file as long-lived project guidance for all agents and contributors.
20
+ - Treat AGENTS.md as the repo-local source of truth. Do not bypass coding style, workflow, testing, or documentation guidance in this file. If a requested change appears to conflict with AGENTS.md, ask the human developer/reviewer to confirm the exception before proceeding, and make the exception explicit in the handoff.
21
+ - Prefer durable wording over time-based wording (avoid labels like "recent", "latest", "today").
22
+ - Keep entries actionable and specific (what to do, where, and why), not conversational history.
23
+ - Capture universal truths and cross-cutting coding conventions here when they become repeated practice or reviewer expectation. Do not leave them only in chat or pull request comments.
24
+ - Capture subsystem-specific guidance in the closest applicable nested `AGENTS.md` when the guidance should always apply to a subtree, such as `api/spec/AGENTS.md` for TypeSpec and SDK guidance. Use skills for reusable workflows or domain procedures that agents opt into for a task. Skills must stay usable by both Claude and Codex: write them as plain repo guidance, keep `.agents/skills` as the source of truth, and avoid assistant-specific assumptions unless a workflow truly requires them.
25
+ - When adding new guidance, fold it into the most relevant section and remove/merge stale or duplicate notes.
26
+
27
+ ## Testing
28
+
29
+ | Task | Command |
30
+ |------|---------|
31
+ | Start dependencies | `make up` |
32
+ | Stop dependencies | `make down` |
33
+ | Run API server (hot reload) | `make server` |
34
+ | Run all tests | `make test` (root module only; excludes `e2e/`, its own module) |
35
+ | Run e2e tests | `make etoe` |
36
+ | Generate all code | `make generate-all` |
37
+ | Generate Go code only | `make generate` (runs `go generate ./...`) |
38
+ | Generate API + SDKs | `make gen-api` |
39
+ | Lint all | `make lint` |
40
+ | Lint Go only | `make lint-go` |
41
+ | Format code | `make fmt` |
42
+ | Tidy modules | `make mod` (root + `collector` + `e2e`) |
43
+ | Build all binaries | `make build` |
44
+
45
+ ## Architecture
46
+
47
+ **Entry points:** `cmd/server`, `cmd/billing-worker`, `cmd/balance-worker`, `cmd/sink-worker`, `cmd/notification-service`, `cmd/jobs`
48
+
49
+ Core business logic is in `openmeter/`, shared utilities in `pkg/`, API layer in `api/`.
50
+
51
+ **Stack:** Go + PostgreSQL (Ent ORM) + Kafka + ClickHouse. API defined in TypeSpec, generated to OpenAPI.
52
+
53
+ Domain packages under `openmeter/` follow a layered service/adapter pattern. See the `/service` skill for full details.
54
+
55
+ `cmd/server/main.go` now migrates the database before creating the default namespace. Register namespace handlers before `initNamespace(...)` if they must provision the default namespace during startup.
56
+
57
+ **Module layout:** the repo is three separate Go modules. The root module (`github.com/openmeterio/openmeter`) holds all production code (`cmd/`, `openmeter/`, `pkg/`, etc.). `api/v3/client` is the standalone, publishable v3 Go SDK module. `e2e/` is a third, never-published, test-only module that imports both — it pins itself to the working tree of each via `replace github.com/openmeterio/openmeter => ../` and `replace .../api/v3/client => ../api/v3/client`, so e2e always tests local code regardless of what's tagged. The root module must never `require` the SDK module: a `require` on an untagged nested module resolves to an unresolvable `v0.0.0` for anyone outside this repo (the `replace` directive that makes it resolve locally is invisible downstream), so any code that needs the SDK — today, only `e2e/` — has to live in its own module rather than the root one. Because of this, root `go build ./...` / `go test ./...` / `go vet ./...` no longer see `e2e/` at all; use `make etoe` (runs it against a live server) or `go test -C e2e ./...` / `go vet -C e2e ./...` (compiles it standalone, no server needed) instead. `make lint-go` and `make mod` already cover all three modules. For editor/gopls support across all three, run `go work init . ./api/v3/client ./e2e` locally — `go.work`/`go.work.sum` are gitignored and must never be committed.
58
+
59
+ ### Project Layout
60
+
61
+ ```
62
+ cmd/ # Service entrypoints
63
+ openmeter/ # Core business logic (billing, customer, entitlement, meter, etc.)
64
+ openmeter/ent/schema/ # Ent entity definitions (source of truth for DB schema)
65
+ openmeter/ent/db/ # Generated ent code (DO NOT EDIT)
66
+ api/ # API specs, generated code, SDKs
67
+ api/spec/ # TypeSpec API definitions (source of truth for API)
68
+ pkg/ # Shared utility packages
69
+ tools/migrate/ # Migration tooling and SQL migration files
70
+ e2e/ # End-to-end tests
71
+ deploy/ # Helm charts
72
+ docs/ # Documentation and ADRs
73
+ ```
74
+
75
+ ## Code Generation
76
+
77
+ All generated files have `// Code generated by X, DO NOT EDIT.` headers — never edit them manually:
78
+
79
+
80
+ | Generated artifact | Source | Regenerate with |
81
+ |---|---|---|
82
+ | `api/openapi.yaml`, `api/openapi.cloud.yaml` | TypeSpec in `api/spec/` | `make gen-api` |
83
+ | `api/client/javascript/`, `api/client/go/` | OpenAPI spec | `make gen-api` |
84
+ | `api/v3/client/` (v3 Go SDK, standalone module) | TypeSpec in `api/spec/` via `@openmeter/typespec-go` | `make gen-api` |
85
+ | `api/api.gen.go`, `api/v3/api.gen.go` | OpenAPI spec via oapi-codegen | `make gen-api` |
86
+ | `api/client/go/client.gen.go` | OpenAPI spec | `make gen-api` |
87
+ | `**/ent/db/` | Ent schema in `openmeter/ent/schema/` | `make generate` |
88
+ | `**/wire_gen.go` | Wire providers in `**/wire.go` | `make generate` |
89
+ | `**/convert.gen.go` | Goverter converter interfaces (`**/convert.go`) | `make generate` |
90
+ | `billing/derived.gen.go` | Goderive annotations | `make generate` |
91
+ | `tools/migrate/migrations/` | Ent schema diff | `atlas migrate --env local diff <name>` |
92
+
93
+ **Workflow for changing the API:**
94
+
95
+ 1. Edit TypeSpec files in `api/spec/`
96
+ 2. Run `make gen-api` to regenerate OpenAPI spec and SDKs
97
+ 3. Run `make generate` to regenerate Go server/client code
98
+
99
+ The TypeSpec JS client emitted from `api/spec/packages/aip` now lands in `api/spec/packages/aip-client-javascript/`. The emitter regenerates `src/`, `README.md`, and five conformance test files (`tests/client.spec.ts`, `tests/meters.spec.ts`, `tests/errors.spec.ts`, `tests/nesting.spec.ts`, `tests/internal.spec.ts`); every regenerated file carries a `Code generated by @openmeter/typespec-typescript. DO NOT EDIT.` header — treat files without that header as hand-written. `package.json` is **stable, hand-maintained, and committed** (the emitter's `writeOutput` only writes the paths it lists, so the manifest survives regeneration). The test suite is vitest + `@fetch-mock/vitest`; keep hand-written tests and helpers in `tests/` (never `src/`), and put test-runner dependencies/scripts in the `api/spec/package.json` workspace root rather than the client package manifest. Static publish metadata (`name`, `license`, `homepage`, `repository`) lives directly in the client `package.json`; only the per-release `version` is injected at publish time. Operations marked `x-internal` or `x-private` in the TypeSpec source are emitted but quarantined under `client.internal.<group>.<method>` (`src/sdk/internal.ts`, an `Internal` aggregate with `Internal<Group>` facade classes; internal ops share their group's `funcs/` and `models/operations/` modules, a group whose ops are all internal gets no public facade, and the `Internal` class is deliberately not re-exported from the package root — the name belongs to the Internal Server Error model). Removing an operation from the spec leaves stale generated files behind — the emitter never deletes outputs, so `git rm` them (and `rm` the gitignored `*.assert.ts` companions, which `git rm` cannot clean up on collaborators' checkouts).
100
+
101
+ The emitted `api/spec/packages/aip-client-javascript/src/sdk/sdk.ts` exposes aggregated sub-client getters on the `OpenMeter` class (e.g. `events`, `meters`, `customers`, `entitlements`, `subscriptions`, `billing`, `features`, `plans`, `addons`, `planAddons`, `tax`, `defaults`). Access operations through those getters, e.g. `sdk.meters.list()`, `sdk.customers.create(...)`, `sdk.plans.create(...)`. These grouped-client methods throw `HTTPError` on failure; the same operations are also available as tree-shakeable standalone functions under `src/funcs/` that return a `Result` instead of throwing.
102
+
103
+ **Workflow for changing Go types/DI:**
104
+
105
+ 1. Edit the source files (ent schema, wire.go, converter interfaces)
106
+ 2. Run `make generate` (or `go generate ./...`)
107
+
108
+ ## Database Migrations
109
+
110
+ Uses [ent](https://entgo.io) for schema definition and [Atlas](https://atlasgo.io/) for migration generation. Migrations are in `tools/migrate/migrations/` using golang-migrate format.
111
+
112
+ **Schema files:** `openmeter/ent/schema/*.go`
113
+
114
+ **Workflow for schema changes:**
115
+
116
+ 1. Edit the ent schema in `openmeter/ent/schema/`
117
+ 2. Run `make generate` to regenerate ent code in `openmeter/ent/db/`
118
+ 3. Generate migration: `atlas migrate --env local diff <migration-name>`
119
+ - This creates timestamped `.up.sql` / `.down.sql` files in `tools/migrate/migrations/`
120
+ - Also updates `tools/migrate/migrations/atlas.sum`
121
+ 4. Versioned migrations run automatically on startup when `postgres.autoMigrate` is set to `migration`; runtime Ent migration is not supported
122
+
123
+ **Ent view caveat:** in this repo's current Ent/Atlas setup, schemas declared with `ent.View` can generate query code under `openmeter/ent/db/`, but they do not appear in `openmeter/ent/db/migrate/schema.go` or the generated `migrate.Tables` list. If `atlas migrate --env local diff ...` reports no changes for a new view, verify whether the view exists in generated migration metadata before debugging Atlas; view DDL may need an explicit SQL migration until generator support is added.
124
+
125
+ **Atlas config:** `atlas.hcl` — schema source is `ent://openmeter/ent/schema`, migrations dir is `file://tools/migrate/migrations`.
126
+
127
+ **Local Postgres:** `postgres://postgres:postgres@localhost:5432/postgres?sslmode=disable`
128
+
129
+ Runtime Ent schema migration is not supported. `postgres.autoMigrate: ent` must fail validation; use `migration`, `migration-job`, or `false`. `openmeter-jobs migrate adopt-ent` contains a frozen compatibility bridge for databases created by Ent before migration baseline `20260709134422`; it stops at that baseline, after which `openmeter-jobs migrate` performs the normal target-version upgrade. Keep the frozen descriptors and reconciliation scripts under `tools/migrate/legacyent` independent from current Ent generation so later schema changes cannot alter the adoption baseline. Ent and Atlas can derive different names for equivalent indexes and constraints, so reconciliation must preserve the canonical Atlas names and `TestLegacyEntAdoptionSchemaParity` must continue comparing the adopted database with a from-scratch Atlas database.
130
+
131
+ ## Testing
132
+
133
+ Tests require PostgreSQL running locally. Start it with `docker compose up -d postgres`.
134
+
135
+ Keep domain test helpers under `openmeter/.../testutils` independent from `app/common`. Build test dependencies from the underlying package constructors (repos, adapters, services, `lockr`) instead of importing the application wiring layer, or unrelated wiring additions can create test-only import cycles.
136
+
137
+ For usage-based billing lifecycle tests, prefer driving behavior through `charges.Service.Create`, `AdvanceCharges`, and `ApplyPatches` rather than calling lower-level charge adapters directly. To model late-arriving or newly visible usage, use `MockStreamingConnector` events with explicit `StoredAt` values (or `SetSimpleEvents`) so the test exercises the real stored-at cutoff logic in finalization.
138
+
139
+ For OpenMeter Go tests that touch the database, explicitly set `POSTGRES_HOST=127.0.0.1`. Without it, many suites will skip during setup even if PostgreSQL is running and the repo environment is otherwise loaded correctly.
140
+
141
+ Use the repo's Nix CI dev shell when `go`, `gofmt`, or other toolchain binaries are missing from the ambient shell. The CI and local-compatible invocation pattern is:
142
+
143
+ ```bash
144
+ nix develop --impure .#ci -c <command>
145
+ ```
146
+
147
+ Always invoke `nix develop` with the repo root as the working directory (use absolute paths in `<command>` instead of `cd`-ing first). The devenv `enterShell` writes CWD-relative state: run from a subdirectory it drops `.devenv/`, `.nvmrc`, and `.pre-commit-config.yaml` there (which then fail `prettier --check` in lint) and reinstalls the git pre-commit hook with that subdirectory's config path baked in, breaking later commits until a root-CWD `nix develop` run repairs it.
148
+
149
+ Codex's default shell may not auto-load `.envrc`, so `direnv`-managed tools like `go` can be missing even when the repo is configured correctly. In that case, run commands through `nix develop --impure .#ci -c ...` explicitly instead of assuming the ambient shell reflects the flake environment. `direnv exec . <command>` is also a valid one-off fallback when `direnv` is installed and the repo has already been allowed.
150
+
151
+ When invoking commands through Codex tools, prefer direct command execution. Do not wrap commands in `sh -lc`, `bash -lc`, or other helper shells when the command can be run directly. For environment variables, prefer `env KEY=value <command>` or `KEY=value <command>` over shell-wrapped forms. This keeps failures attributable to the actual toolchain/runtime being tested.
152
+
153
+ In tests, prefer `t.Context()` when a `testing.T` or `testing.TB` is available instead of introducing `context.Background()`. This keeps cancellation and test-scoped lifecycle tied to the test harness.
154
+
155
+ Prefer one consistent test harness style over mixed ad hoc structures. Use production-backed paths, such as rating-backed or service-backed fixtures, when the real path can express the scenario; keep hand-assembled fixtures for cases that cannot be produced realistically. If a behavior is a suite-wide rule, hardcode it into the shared harness instead of exposing it as per-test knobs.
156
+
157
+ Avoid redundant test helpers and duplicate setup paths. Prefer parameterizing one helper over maintaining near-identical helpers, use literal helper names that state exactly what they do, and inline single-use helpers that only wrap setup, conversion, or assertions even when the test becomes longer. Add a test helper only when it is used by at least two tests in the same package or when the helper name captures non-obvious domain semantics that would otherwise be easy to miss. Clean up dead test helpers immediately after refactors.
158
+
159
+ For service and lifecycle subtests, start each subtest body with concise intent comments when the scenario is non-trivial:
160
+
161
+ ```go
162
+ // given:
163
+ // - ...
164
+ // when:
165
+ // - ...
166
+ // then:
167
+ // - ...
168
+ ```
169
+
170
+ When using `clock.FreezeTime(...)` in tests, immediately pair it with `defer clock.UnFreeze()` in the same scope so later assertions or subtests do not inherit frozen time accidentally.
171
+
172
+ When asserting `alpacadecimal.Decimal` equality in tests, prefer `require.Equal(t, expectedFloat64, actual.InexactFloat64())` over boolean assertions like `require.True(t, expected.Equal(actual))` when precision requirements allow it. Prefer simple `float64(5)`-style literals over verbose decimal construction for expected values. Inline one-off expected balance structs at the assertion site; name expected balances only when reused or when the name carries useful phase semantics across subtests.
173
+
174
+ After each meaningful test-related change, run focused `go vet` and focused `go test` for the touched package.
175
+
176
+ Examples:
177
+
178
+ ```bash
179
+ nix develop --impure .#ci -c gofmt -w openmeter/ledger/historical/entry.go
180
+ nix develop --impure .#ci -c make lint-go
181
+ nix develop --impure .#ci -c env POSTGRES_HOST=127.0.0.1 go test -tags=dynamic ./openmeter/ledger/historical/...
182
+ ```
183
+
184
+ | Command | Description |
185
+ |---------|-------------|
186
+ | `make test` | Run all tests (parallel: `-p 128 -parallel 16`) |
187
+ | `make test-nocache` | Run tests bypassing cache |
188
+ | `make test-all` | Run tests including Svix/Redis dependencies |
189
+ | `make test-go-sdk` | Build, vet, and test the v3 Go SDK module (`api/v3/client`) |
190
+ | `make etoe` | Run e2e tests (requires docker compose dependencies) |
191
+
192
+ **Running a single package directly:**
193
+
194
+ ```bash
195
+ POSTGRES_HOST=127.0.0.1 go test -tags=dynamic -v ./openmeter/billing/...
196
+ ```
197
+
198
+ Key flags: `-tags=dynamic` (required for confluent-kafka-go), `-p 128 -parallel 16` (used by Make). Set `POSTGRES_HOST=127.0.0.1` or tests requiring Postgres will be skipped. `e2e/` is its own module and its import graph never reaches confluent-kafka-go, so `-tags=dynamic` is not needed there — `go test -C e2e ./...` (or `TZ=UTC OPENMETER_ADDRESS=... go test -C e2e ./...`) is enough.
199
+
200
+ See the `/test` skill for testing patterns, TestEnv setup, and examples.
201
+
202
+ ## Building
203
+
204
+ ```bash
205
+ make build # All binaries → build/
206
+ make build-server # Just the server
207
+ ```
208
+
209
+ All builds use `GO_BUILD_FLAGS=-tags=dynamic`.
210
+
211
+ ## Configuration
212
+
213
+ - Copy `config.example.yaml` to `config.yaml` (done automatically by Make targets)
214
+ - Load the repository environment with `direnv`, or run commands with `direnv exec . <command>`, so project-specific environment variables and tool configuration are applied consistently
215
+ - Key settings: `postgres.url`, `postgres.autoMigrate`, `billing`, `notification`, meter definitions
216
+ - `credits.enabled` needs explicit guarding at multiple layers: ledger-backed customer credit handlers in `api/v3/server`, customer ledger hooks, and namespace/default-account provisioning are wired separately and must each stay disabled when credits are off.
217
+ - When `credits.enabled` is `false`, `app/common` wires ledger account services/resolvers to noop implementations. Any ledger account backfill that must write real `ledger_accounts` / `ledger_customer_accounts` rows needs to construct concrete ledger account + resolver adapters directly instead of relying on the default DI outputs.
218
+ - Make targets for running services will warn if `config.yaml` is outdated vs `config.example.yaml`
219
+
220
+ ## Coding Conventions
221
+
222
+ See the `/service` skill for service/adapter patterns, constructors, input types, errors, transactions, hooks, logging, multi-tenancy, and DI wiring. See the `/api` skill for HTTP handler patterns and ValidationIssue. See the `/ent` skill for Ent ORM patterns and Postgres type gotchas. See the `/ledger` skill for ledger package architecture, wiring, and testing. See the `/subscription` skill for subscription domain model, sync algorithm, patch system, workflow layer, and addon sub-system. See the `/notification` skill for notification event pipeline, Kafka consumers, Svix webhook delivery, reconciliation loop, and payload versioning.
223
+
224
+ For TypeSpec-specific coding constraints, update `api/spec/AGENTS.md` instead of adding them here.
225
+
226
+ ### Documentation Constraints
227
+
228
+ - When adding comments or docstrings, document intent and domain constraints that are only available from human author context, not facts a reader can infer by reading the codebase. Avoid comments that merely translate obvious conditions, such as saying that a branch runs when `servicePeriod > 0`. A good comment should be understandable without the author's chat context and should explain why the code deliberately includes or excludes a case. For fallback or guard comments, name the concrete input shape or lifecycle state, the invariant being protected, the chosen behavior, and what would go wrong if the guard or fallback were removed. Avoid vague phrases like "can still arrive" or "as needed".
229
+ - Add a docstring to domain helpers when the name compresses important business semantics that are easy to misread at call sites. Explain the observable business contract and why excluded cases are excluded, not the implementation mechanics.
230
+ - When refactoring or reverting code, preserve existing explanatory comments by default. Remove or rewrite a comment only when the code change makes it false, stale, or misleading.
231
+
232
+ ### Go Style Constraints
233
+
234
+ - For Go string enum constants, name values as `<Type><Value>` so the constant carries its enum type at the use site, for example `InvoiceStatusDraft` instead of `Draft`.
235
+ - Do not extract helper functions only to hide a couple of simple operations or short guard checks. If the helper would only wrap 2-4 lines and its name does not add meaningful domain or business intent, keep the code inline even when there is some duplication. Readers can inspect the function body to see what the code does; prefer function names that explain the domain reason for the call over names that merely restate the implementation steps. When you encounter a leftover pass-through wrapper that only calls another function without adding behavior, remove it and call the underlying function directly, even if it is outside the immediate change area.
236
+ - Do not hide non-trivial branching or domain translation inside local inline functions. If a closure performs type switching, validation, persistence mapping, or meaningful domain conversion, make it a named helper near the code that uses it so it is discoverable, testable, and grep-friendly. Reserve inline closures for tiny callbacks where the surrounding API requires a function literal and the logic is obvious at the call site.
237
+ - For `Validate() error` methods, prefer collecting all validation issues into `var errs []error` and returning `models.NewNillableGenericValidationError(errors.Join(errs...))` instead of returning on the first invalid field. Preserve field context with wrapped errors like `fmt.Errorf("field: %w", err)` and use plain `errors.New(...)` for simple local checks.
238
+ - Do not introduce `context.Background()` or `context.TODO()` to sidestep missing context propagation in application code. Either propagate the caller's context through the full call path, or remove the unused `context.Context` parameter from the API if the operation is purely local and does not need cancellation, deadlines, or request-scoped values.
239
+ - Never use `panic` in non-test code paths. If a new failure mode is possible, change the function signature to return an error and propagate it explicitly.
240
+ - In production constructors and initialization, do not use `slog.Default()` as a fallback dependency. Require a `*slog.Logger` in config/provider inputs and inject it explicitly.
241
+ - Prefer standard library `slices` and `maps` helpers for common collection operations, and use `github.com/samber/lo` when it makes pointer literals or collection transformations clearer than local wrappers or hand-written loops. See the `/samber-lo` skill for common OpenMeter use cases and caveats. Do not add local wrappers such as `ptr`, `loPtr`, `must`, or `loMust` when standard helpers or `lo` already cover the need.
242
+ - Use repo helper packages when they capture a common pattern better than ad hoc closures. For example, use `pkg/slicesx` for existing slice helpers (but prefer `samber/lo` and `slices` system packages if they fit), and use `pkg/syncx.OnceValues` for lazy context-aware database lookups that may be needed by multiple callbacks but should execute at most once.
243
+ - Keep helper functions honest and narrow. If a production helper is only called once and is just a short guard or a few straightforward lines, inline it unless the name carries meaningful domain semantics. Do not add helpers for trivial single-use struct literals, do not hide aggregate mutation inside construction helpers, and return the domain value a helper actually builds rather than a broader wrapper needed by one caller.
244
+ - For files and functions that convert between domain, API, and DB representations, use the `/go-types-conversion` skill. In prose, prefer `map` / `mapped` terminology for domain representation translation and avoid `project` / `projected` for that meaning; function names must still follow the skill's `FromAPI...`, `ToAPI...`, `FromDB...`, and `ToDB...` conventions.
245
+
246
+ ### Generation And Dependency Constraints
247
+
248
+ - When `make generate` or `atlas migrate --env local diff ...` adds incidental `go.sum` entries, such as `tablewriter`, drop those `go.sum` changes unless the task explicitly requires a dependency change.
249
+
250
+ ## Key Dependencies
251
+
252
+ | Category | Libraries |
253
+ |----------|-----------|
254
+ | DB | PostgreSQL (Ent ORM, Atlas migrations, pgx driver) |
255
+ | Analytics | ClickHouse |
256
+ | Events | Kafka (confluent-kafka-go) + Watermill |
257
+ | HTTP | Chi router + oapi-codegen |
258
+ | Invoicing | GOBL (invoice format) |
259
+ | Webhooks | Svix |
260
+ | Observability | OpenTelemetry |
261
+ | Config | Viper + Cobra |
262
+ | Utilities | samber/lo |
263
+
264
+ ## CodeGraph
265
+
266
+ CodeGraph builds a semantic knowledge graph of the codebase (~1,800 Go files, ~36k symbols) for faster, smarter code exploration. The index lives in `.codegraph/codegraph.db` (gitignored). Generated files (`ent/db/`, `*_gen.go`, `wire_gen.go`, `*.gen.go`) are excluded.
267
+
268
+ ### If `.codegraph/` exists
269
+
270
+ **Default to CodeGraph, not Grep/Glob/find.** CodeGraph understands symbols, call relationships, and file structure — those tools return string matches. On a ~1,800-file Go codebase the symbol-aware answer is almost always what you wanted. Fall back to Grep/Glob **only** when CodeGraph returns no results or the query is inherently textual (string literals, comments, log messages, SQL, YAML keys).
271
+
272
+ **Never call `codegraph_explore` or `codegraph_context` in the main session.** These tools return large source code blocks that fill up main-session context fast. Instead, spawn an Explore agent for any exploration question (e.g., "how does billing sync work?", "where is entitlement reset implemented?").
273
+
274
+ When spawning Explore agents, include this instruction in the prompt:
275
+
276
+ > This project has CodeGraph initialized (.codegraph/ exists). Use `codegraph_explore` as your PRIMARY exploration tool — it returns full source code sections from all relevant files in one call.
277
+ >
278
+ > **Rules:**
279
+ > 1. Follow the explore call budget in the `codegraph_explore` tool description — it scales automatically based on project size.
280
+ > 2. Do NOT re-read files that `codegraph_explore` already returned source code for. The source sections are complete and authoritative.
281
+ > 3. Only fall back to Grep/Glob/Read for files listed under "Additional relevant files" if you need more detail, or if CodeGraph returned no results.
282
+
283
+ **The main session should use these lightweight tools directly** for targeted lookups before making edits:
284
+
285
+ | Tool | Use for | Example |
286
+ |------|---------|---------|
287
+ | `codegraph_search` | Find symbols by name | `query: "BillingService"` |
288
+ | `codegraph_callers` | Who calls this function? | Before renaming or changing a signature |
289
+ | `codegraph_callees` | What does this function call? | Understanding a function's dependencies |
290
+ | `codegraph_impact` | Blast radius of a change | Before refactoring a shared type |
291
+ | `codegraph_node` | Single symbol details | Quick check on a struct or interface |
292
+ | `codegraph_files` | Project file tree | Faster than Glob for directory overviews |
293
+ | `codegraph_status` | Index health check | Verify the index is up to date |
294
+
295
+ ### Choosing CodeGraph vs Grep/Glob
296
+
297
+ | Task | Prefer | Reason |
298
+ |------|--------|--------|
299
+ | "Where is `BillingService` defined?" | `codegraph_search` | Symbol lookup with location + signature |
300
+ | "Who calls `ListCustomers`?" | `codegraph_callers` | Call-graph edges, not text matches |
301
+ | "What does `Reconcile` call?" | `codegraph_callees` | Dependencies of a function |
302
+ | "Blast radius of changing this type?" | `codegraph_impact` | Transitive reverse-deps |
303
+ | "Show the `Filter` interface fields" | `codegraph_node` | Single-symbol detail without reading whole file |
304
+ | "List files under `api/v3/filters/`" | `codegraph_files` | Indexed tree; no disk walk |
305
+ | Find a string literal / log message / SQL fragment | `Grep` | Not a symbol |
306
+ | Find files by glob pattern (`**/*.tsp`) | `Glob` | CodeGraph indexes Go; non-Go globs go through Glob |
307
+ | Navigate a specific known path | `Read` | Direct reads are always fine |
308
+ | Running `find` on the shell | Don't | Use `codegraph_files` or `Glob` |
309
+ | Running `grep`/`rg` on the shell | Don't | Use `Grep` (or `codegraph_search` for symbols) |
310
+
311
+ Rule of thumb: **if the target is a Go identifier, start with CodeGraph. If it's a string, start with Grep.** Never shell out to `grep`, `rg`, or `find` — the dedicated tools (`Grep`, `Glob`, `codegraph_*`) give better output and permission handling.
312
+
313
+ ### Keeping the index fresh
314
+
315
+ At the start of work, refresh CodeGraph before exploring Go code. If `.codegraph/` exists, run `codegraph sync`; if it does not exist, run `codegraph init -i` without asking first. Run `codegraph index` for a full rebuild if the index seems stale or after branch switches.
316
+
317
+ ### If `.codegraph/` does NOT exist
318
+
319
+ Initialize it with `codegraph init -i` before doing code exploration. It indexes the Go codebase quickly and keeps symbol-aware lookup available.
320
+
321
+ ## Skills
322
+
323
+ Skills are created inside [.agents/skills](.agents/skills/) by default and then symlinked to [.claude/skills](.claude/skills). Make sure you always treat `.agents/skills` as the source of truth. Keep skill guidance compatible with both Claude and Codex; avoid instructions that assume only one agent runtime unless the skill is explicitly about that runtime.
Agent.md ADDED
@@ -0,0 +1,148 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Hugging Face Space Deployment Agent Guide
2
+
3
+ This file outlines the deployment configuration, API exposure, and deployment workflow for OpenMeter on Hugging Face Spaces.
4
+
5
+ ## 1. Deployment Configuration
6
+
7
+ ### Target Space
8
+ - **Profile:** `Leon4gr45`
9
+ - **Space:** `openmeter`
10
+ - **Full Identifier:** `Leon4gr45/openmeter`
11
+ - **Frontend Port:** `7860` (mandatory for all Hugging Face Spaces)
12
+
13
+ ### Deployment Method
14
+ Choose the correct SDK based on the app type based on the codebase language:
15
+ - **Docker SDK** is used for OpenMeter (as a Go application).
16
+
17
+ ### HF Token
18
+ - The environment variable `HF_TOKEN` must be set to your Hugging Face Space token at execution time (never hardcode the token).
19
+ - All monitoring, upload, and log-streaming commands rely on this token.
20
+
21
+ ### Required Files
22
+ - `Dockerfile` (packaged to run with the mock server enabled for space readiness)
23
+ - `README.md` with Hugging Face YAML frontmatter specifying:
24
+ ```yaml
25
+ ---
26
+ title: Openmeter
27
+ sdk: docker
28
+ app_port: 7860
29
+ ---
30
+ ```
31
+ - `.hfignore` to exclude unnecessary files.
32
+ - `Agent.md` (this file, committed before deployment).
33
+
34
+ ---
35
+
36
+ ## 2. API Exposure and Documentation
37
+
38
+ ### Mandatory Endpoints
39
+ Every deployment **must** expose:
40
+
41
+ - **`/health`**
42
+ - Returns HTTP 200 `OK` (plain text or JSON) when the app is ready.
43
+ - Required for Hugging Face to transition the Space from *starting* → *running*.
44
+
45
+ - **`/api-docs`**
46
+ - Documents **all** available API endpoints.
47
+ - Must be reachable at:
48
+ `https://Leon4gr45-openmeter.hf.space/api-docs`
49
+
50
+ ---
51
+
52
+ ### Functional Endpoints
53
+
54
+ The mock server exposed in this space implements the following critical OpenMeter functional endpoints (all of which are documented in `/api-docs` via `/api/swagger.json`):
55
+
56
+ #### 1. Ingest Events
57
+ - **Method:** POST
58
+ - **Path:** `/api/v1/events`
59
+ - **Purpose:** Ingest event data in CloudEvents format.
60
+ - **Request Example:**
61
+ ```json
62
+ {
63
+ "specversion": "1.0",
64
+ "type": "request",
65
+ "id": "00001",
66
+ "time": "2026-07-07T00:00:00.001Z",
67
+ "source": "my-service",
68
+ "subject": "customer-1",
69
+ "data": { "method": "GET", "route": "/api/hello" }
70
+ }
71
+ ```
72
+ - **Response Example:**
73
+ ```json
74
+ {
75
+ "status": "accepted"
76
+ }
77
+ ```
78
+
79
+ #### 2. List Meters
80
+ - **Method:** GET
81
+ - **Path:** `/api/v1/meters`
82
+ - **Purpose:** List all configured usage meters.
83
+ - **Request:** (None)
84
+ - **Response Example:**
85
+ ```json
86
+ [
87
+ {
88
+ "id": "api_requests_total",
89
+ "slug": "api_requests_total",
90
+ "description": "API Requests",
91
+ "eventType": "request",
92
+ "aggregation": "COUNT"
93
+ },
94
+ {
95
+ "id": "tokens_total",
96
+ "slug": "tokens_total",
97
+ "description": "AI Token Usage",
98
+ "eventType": "prompt",
99
+ "aggregation": "SUM"
100
+ }
101
+ ]
102
+ ```
103
+
104
+ #### 3. Query Meter Usage
105
+ - **Method:** GET
106
+ - **Path:** `/api/v1/meters/{meterIdOrSlug}/query`
107
+ - **Purpose:** Query usage data for a specific meter.
108
+ - **Request Parameters:** `windowSize=HOUR`
109
+ - **Response Example:**
110
+ ```json
111
+ {
112
+ "data": [
113
+ {
114
+ "value": 150,
115
+ "windowStart": "2026-07-07T00:00:00Z",
116
+ "windowEnd": "2026-07-07T01:00:00Z"
117
+ }
118
+ ]
119
+ }
120
+ ```
121
+
122
+ ---
123
+
124
+ ## 3. Deployment Workflow
125
+
126
+ Precondition: Check that the space is empty of files and delete any which are still in there and not belonging to the project to be uploaded.
127
+
128
+ ### Standard Deployment Command
129
+ After any code change, run:
130
+
131
+ ```bash
132
+ HF_TOKEN=<HF_TOKEN_VALUE> hf upload Leon4gr45/openmeter . . --repo-type=space
133
+ ```
134
+
135
+ ### Scan build and run logs
136
+ Get build logs (SSE):
137
+ ```bash
138
+ curl -N -H "Authorization: Bearer <HF_TOKEN_VALUE>" "https://huggingface.co/api/spaces/Leon4gr45/openmeter/logs/build"
139
+ ```
140
+
141
+ Get run logs (SSE) once the build logs succeed:
142
+ ```bash
143
+ curl -N -H "Authorization: Bearer <HF_TOKEN_VALUE>" "https://huggingface.co/api/spaces/Leon4gr45/openmeter/logs/run"
144
+ ```
145
+
146
+ Cycle monitoring and deployment:
147
+ - Wait up to 300 seconds to see if the deployment has been successful.
148
+ - If not, check logs for errors, fix the issues in the codebase, redeploy, and monitor again in a cycle until the space is running and reacts successfully to API endpoints.
CLAUDE.md ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ # OpenMeter Development Guide
2
+
3
+ See consolidated agents instructions in @AGENTS.md
CODE_OF_CONDUCT.md ADDED
@@ -0,0 +1,68 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Contributor Covenant Code of Conduct
2
+
3
+ ## Our Pledge
4
+
5
+ In the interest of fostering an open and welcoming environment,
6
+ we as contributors and maintainers pledge to make participation in our project
7
+ and our community a harassment-free experience for everyone, regardless of age,
8
+ body size, disability, ethnicity, sex characteristics, gender identity and expression,
9
+ level of experience, education, socio-economic status, nationality, personal appearance,
10
+ race, religion, or sexual identity and orientation.
11
+
12
+ ## Our Standards
13
+
14
+ Examples of behavior that contributes to creating a positive environment include:
15
+
16
+ - Using welcoming and inclusive language
17
+ - Being respectful of differing viewpoints and experiences
18
+ - Gracefully accepting constructive criticism
19
+ - Focusing on what is best for the community
20
+ - Showing empathy towards other community members
21
+
22
+ Examples of unacceptable behavior by participants include:
23
+
24
+ - The use of sexualized language or imagery and unwelcome sexual attention or advances
25
+ - Trolling, insulting/derogatory comments, and personal or political attacks
26
+ - Public or private harassment
27
+ - Publishing others' private information, such as a physical or electronic address, without explicit permission
28
+ - Other conduct which could reasonably be considered inappropriate in a professional setting
29
+
30
+ ## Our Responsibilities
31
+
32
+ Project maintainers are responsible for clarifying the standards of acceptable behavior
33
+ and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior.
34
+
35
+ Project maintainers have the right and responsibility to remove, edit,
36
+ or reject comments, commits, code, wiki edits, issues,
37
+ and other contributions that are not aligned to this Code of Conduct,
38
+ or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate,
39
+ threatening, offensive, or harmful.
40
+
41
+ ## Scope
42
+
43
+ This Code of Conduct applies within all project spaces,
44
+ and it also applies when an individual is representing the project or its community in public spaces.
45
+ Examples of representing a project or community include using an official project e-mail address,
46
+ posting via an official social media account, or acting as an appointed representative at an online or offline event.
47
+ Representation of a project may be further defined and clarified by project maintainers.
48
+
49
+ ## Enforcement
50
+
51
+ Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at [conduct@openmeter.io][conduct-email].
52
+ All complaints will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances.
53
+ The project team is obligated to maintain confidentiality with regard to the reporter of an incident.
54
+ Further details of specific enforcement policies may be posted separately.
55
+
56
+ Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary
57
+ or permanent repercussions as determined by other members of the project's leadership.
58
+
59
+ [conduct-email]: mailto:conduct@openmeter.io
60
+
61
+ ## Attribution
62
+
63
+ This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
64
+ available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
65
+
66
+ [homepage]: https://www.contributor-covenant.org
67
+
68
+ For answers to common questions about this code of conduct, see https://www.contributor-covenant.org/faq
CONTRIBUTING.md ADDED
@@ -0,0 +1,53 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Contributing
2
+
3
+ Thanks for your interest in contributing to OpenMeter!
4
+
5
+ Here are a few general guidelines on contributing and reporting bugs that we ask you to review.
6
+ Following these guidelines helps to communicate that you respect the time of the contributors managing and developing this open source project.
7
+ In return, they should reciprocate that respect in addressing your issue, assessing changes, and helping you finalize your pull requests.
8
+ In that spirit of mutual respect, we endeavor to review incoming issues and pull requests within 10 days,
9
+ and will close any lingering issues or pull requests after 60 days of inactivity.
10
+
11
+ Please note that all of your interactions in the project are subject to our [Code of Conduct](/CODE_OF_CONDUCT.md).
12
+ This includes creation of issues or pull requests, commenting on issues or pull requests,
13
+ and extends to all interactions in any real-time space e.g., Slack, Discord, etc.
14
+
15
+ ## Reporting issues
16
+
17
+ Before reporting a new issue, please ensure that the issue was not already reported or fixed by searching through our issue tracker.
18
+
19
+ When creating a new issue, please be sure to include a **title and clear description**, as much relevant information as possible, and, if possible, a test case.
20
+
21
+ **If you discover a security bug, please do not report it through GitHub issues. Instead, please see security procedures in [SECURITY.md](/SECURITY.md).**
22
+
23
+ ## Sending pull requests
24
+
25
+ Before sending a new pull request, take a look at existing pull requests and issues to see if the proposed change or fix has been discussed in the past,
26
+ or if the change was already implemented but not yet released.
27
+
28
+ We expect new pull requests to include tests for any affected behavior, and, as we follow semantic versioning,
29
+ we may reserve breaking changes until the next major version release.
30
+
31
+ ### Ensuring All Requested Reviewers Approve
32
+
33
+ By default, pull requests can often be merged once the minimum number of required approvals (e.g., from CODEOWNERS or branch protection rules) is met. However, sometimes you might explicitly request reviews from specific individuals because their input is crucial for that particular PR.
34
+
35
+ To ensure that *all* individuals you've specifically requested using the GitHub "Reviewers" UI must approve before merging, follow these steps:
36
+
37
+ 1. **Request Reviews:** Use the standard GitHub interface on the pull request page to request reviews from the necessary individuals.
38
+ 2. **Add Label:** Add the label `require-all-reviewers` to the pull request.
39
+
40
+ When this label is present, an automated check named "Review Gatekeeper" will run. This check will only pass if **every single user** listed under the "Reviewers" section has submitted an **approving** review. This check is required for merging, preventing merges until all explicitly requested reviewers are satisfied.
41
+
42
+ If the label is removed, the "Review Gatekeeper" check will be skipped.
43
+
44
+ ## Other ways to contribute
45
+
46
+ We welcome anyone that wants to contribute to triage and reply to open issues to help troubleshoot and fix existing bugs.
47
+ Here is what you can do:
48
+
49
+ - Help ensure that existing issues follows the recommendations from the _[Reporting Issues](#reporting-issues)_ section,
50
+ providing feedback to the issue's author on what might be missing.
51
+ - Review and update the existing content of our [documentation](https://openmeter.io) with up-to-date instructions and code samples.
52
+ - Review existing pull requests, and testing patches against real existing applications.
53
+ - Write a test, or add a missing test case to an existing test.
Dockerfile ADDED
@@ -0,0 +1,98 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM --platform=$BUILDPLATFORM tonistiigi/xx:1.9.0@sha256:c64defb9ed5a91eacb37f96ccc3d4cd72521c4bd18d5442905b95e2226b0e707 AS xx
2
+
3
+ FROM --platform=$BUILDPLATFORM golang:1.26.4-alpine3.23@sha256:18b460dd17542c2ba43299a633cf6ebfc1115101509531471d7cfce1019af083 AS builder
4
+
5
+ COPY --link --from=xx / /
6
+
7
+ RUN xx-apk add --update --no-cache ca-certificates make git curl clang lld
8
+
9
+ ARG TARGETPLATFORM
10
+
11
+ RUN xx-apk --update --no-cache add musl-dev gcc
12
+
13
+ WORKDIR /src
14
+
15
+ ARG GOPROXY
16
+
17
+ ENV CGO_ENABLED=1
18
+
19
+ ENV GOCACHE=/go/cache
20
+ ENV GOMODCACHE=/go/pkg/mod
21
+
22
+ COPY --link go.mod go.sum ./
23
+
24
+ RUN --mount=type=cache,target=/go/pkg/mod \
25
+ --mount=type=cache,target=/go/cache \
26
+ xx-go mod download -x
27
+
28
+ ARG VERSION
29
+
30
+ COPY --link . .
31
+
32
+ RUN chmod +x entrypoint.sh
33
+
34
+ # See https://github.com/confluentinc/confluent-kafka-go#librdkafka
35
+ # See https://github.com/confluentinc/confluent-kafka-go#static-builds-on-linux
36
+ # Build server binary (default)
37
+ RUN --mount=type=cache,target=/go/pkg/mod \
38
+ --mount=type=cache,target=/go/cache \
39
+ xx-go build -ldflags "-linkmode external -extldflags \"-static\" -X main.version=${VERSION}" -tags musl -o /usr/local/bin/openmeter ./cmd/server
40
+
41
+ RUN xx-verify /usr/local/bin/openmeter
42
+
43
+ # Build sink-worker binary
44
+ RUN --mount=type=cache,target=/go/pkg/mod \
45
+ --mount=type=cache,target=/go/cache \
46
+ xx-go build -ldflags "-linkmode external -extldflags \"-static\" -X main.version=${VERSION}" -tags musl -o /usr/local/bin/openmeter-sink-worker ./cmd/sink-worker
47
+
48
+ RUN xx-verify /usr/local/bin/openmeter-sink-worker
49
+
50
+ # Build balance-worker binary
51
+ RUN --mount=type=cache,target=/go/pkg/mod \
52
+ --mount=type=cache,target=/go/cache \
53
+ xx-go build -ldflags "-linkmode external -extldflags \"-static\" -X main.version=${VERSION}" -tags musl -o /usr/local/bin/openmeter-balance-worker ./cmd/balance-worker
54
+
55
+ RUN xx-verify /usr/local/bin/openmeter-balance-worker
56
+
57
+ # Build notification-service binary
58
+ RUN --mount=type=cache,target=/go/pkg/mod \
59
+ --mount=type=cache,target=/go/cache \
60
+ xx-go build -ldflags "-linkmode external -extldflags \"-static\" -X main.version=${VERSION}" -tags musl -o /usr/local/bin/openmeter-notification-service ./cmd/notification-service
61
+
62
+ RUN xx-verify /usr/local/bin/openmeter-notification-service
63
+
64
+ # Build billing-worker binary
65
+ RUN --mount=type=cache,target=/go/pkg/mod \
66
+ --mount=type=cache,target=/go/cache \
67
+ xx-go build -ldflags "-linkmode external -extldflags \"-static\" -X main.version=${VERSION}" -tags musl -o /usr/local/bin/openmeter-billing-worker ./cmd/billing-worker
68
+
69
+ RUN xx-verify /usr/local/bin/openmeter-billing-worker
70
+
71
+ # Build periodic jobs binary
72
+ RUN --mount=type=cache,target=/go/pkg/mod \
73
+ --mount=type=cache,target=/go/cache \
74
+ xx-go build -ldflags "-linkmode external -extldflags \"-static\" -X main.version=${VERSION}" -tags musl -o /usr/local/bin/openmeter-jobs ./cmd/jobs
75
+
76
+ RUN xx-verify /usr/local/bin/openmeter-jobs
77
+
78
+ FROM alpine:3.24.1@sha256:28bd5fe8b56d1bd048e5babf5b10710ebe0bae67db86916198a6eec434943f8b
79
+
80
+ ENV MOCK_SERVER=true
81
+ ENV PORT=7860
82
+
83
+ RUN apk add --update --no-cache ca-certificates tzdata bash
84
+
85
+ SHELL ["/bin/bash", "-c"]
86
+
87
+ COPY --link --from=builder /usr/local/bin/openmeter /usr/local/bin/
88
+ COPY --link --from=builder /usr/local/bin/openmeter-sink-worker /usr/local/bin/
89
+ COPY --link --from=builder /usr/local/bin/openmeter-balance-worker /usr/local/bin/
90
+ COPY --link --from=builder /usr/local/bin/openmeter-notification-service /usr/local/bin/
91
+ COPY --link --from=builder /usr/local/bin/openmeter-billing-worker /usr/local/bin/
92
+ COPY --link --from=builder /usr/local/bin/openmeter-jobs /usr/local/bin/
93
+ COPY --link --from=builder /src/go.* /usr/local/src/openmeter/
94
+ COPY --link --from=builder /src/entrypoint.sh /entrypoint.sh
95
+
96
+ ENTRYPOINT ["/entrypoint.sh"]
97
+
98
+ CMD openmeter
LICENSE ADDED
@@ -0,0 +1,202 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ Apache License
3
+ Version 2.0, January 2004
4
+ http://www.apache.org/licenses/
5
+
6
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
7
+
8
+ 1. Definitions.
9
+
10
+ "License" shall mean the terms and conditions for use, reproduction,
11
+ and distribution as defined by Sections 1 through 9 of this document.
12
+
13
+ "Licensor" shall mean the copyright owner or entity authorized by
14
+ the copyright owner that is granting the License.
15
+
16
+ "Legal Entity" shall mean the union of the acting entity and all
17
+ other entities that control, are controlled by, or are under common
18
+ control with that entity. For the purposes of this definition,
19
+ "control" means (i) the power, direct or indirect, to cause the
20
+ direction or management of such entity, whether by contract or
21
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
22
+ outstanding shares, or (iii) beneficial ownership of such entity.
23
+
24
+ "You" (or "Your") shall mean an individual or Legal Entity
25
+ exercising permissions granted by this License.
26
+
27
+ "Source" form shall mean the preferred form for making modifications,
28
+ including but not limited to software source code, documentation
29
+ source, and configuration files.
30
+
31
+ "Object" form shall mean any form resulting from mechanical
32
+ transformation or translation of a Source form, including but
33
+ not limited to compiled object code, generated documentation,
34
+ and conversions to other media types.
35
+
36
+ "Work" shall mean the work of authorship, whether in Source or
37
+ Object form, made available under the License, as indicated by a
38
+ copyright notice that is included in or attached to the work
39
+ (an example is provided in the Appendix below).
40
+
41
+ "Derivative Works" shall mean any work, whether in Source or Object
42
+ form, that is based on (or derived from) the Work and for which the
43
+ editorial revisions, annotations, elaborations, or other modifications
44
+ represent, as a whole, an original work of authorship. For the purposes
45
+ of this License, Derivative Works shall not include works that remain
46
+ separable from, or merely link (or bind by name) to the interfaces of,
47
+ the Work and Derivative Works thereof.
48
+
49
+ "Contribution" shall mean any work of authorship, including
50
+ the original version of the Work and any modifications or additions
51
+ to that Work or Derivative Works thereof, that is intentionally
52
+ submitted to Licensor for inclusion in the Work by the copyright owner
53
+ or by an individual or Legal Entity authorized to submit on behalf of
54
+ the copyright owner. For the purposes of this definition, "submitted"
55
+ means any form of electronic, verbal, or written communication sent
56
+ to the Licensor or its representatives, including but not limited to
57
+ communication on electronic mailing lists, source code control systems,
58
+ and issue tracking systems that are managed by, or on behalf of, the
59
+ Licensor for the purpose of discussing and improving the Work, but
60
+ excluding communication that is conspicuously marked or otherwise
61
+ designated in writing by the copyright owner as "Not a Contribution."
62
+
63
+ "Contributor" shall mean Licensor and any individual or Legal Entity
64
+ on behalf of whom a Contribution has been received by Licensor and
65
+ subsequently incorporated within the Work.
66
+
67
+ 2. Grant of Copyright License. Subject to the terms and conditions of
68
+ this License, each Contributor hereby grants to You a perpetual,
69
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
70
+ copyright license to reproduce, prepare Derivative Works of,
71
+ publicly display, publicly perform, sublicense, and distribute the
72
+ Work and such Derivative Works in Source or Object form.
73
+
74
+ 3. Grant of Patent License. Subject to the terms and conditions of
75
+ this License, each Contributor hereby grants to You a perpetual,
76
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
77
+ (except as stated in this section) patent license to make, have made,
78
+ use, offer to sell, sell, import, and otherwise transfer the Work,
79
+ where such license applies only to those patent claims licensable
80
+ by such Contributor that are necessarily infringed by their
81
+ Contribution(s) alone or by combination of their Contribution(s)
82
+ with the Work to which such Contribution(s) was submitted. If You
83
+ institute patent litigation against any entity (including a
84
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
85
+ or a Contribution incorporated within the Work constitutes direct
86
+ or contributory patent infringement, then any patent licenses
87
+ granted to You under this License for that Work shall terminate
88
+ as of the date such litigation is filed.
89
+
90
+ 4. Redistribution. You may reproduce and distribute copies of the
91
+ Work or Derivative Works thereof in any medium, with or without
92
+ modifications, and in Source or Object form, provided that You
93
+ meet the following conditions:
94
+
95
+ (a) You must give any other recipients of the Work or
96
+ Derivative Works a copy of this License; and
97
+
98
+ (b) You must cause any modified files to carry prominent notices
99
+ stating that You changed the files; and
100
+
101
+ (c) You must retain, in the Source form of any Derivative Works
102
+ that You distribute, all copyright, patent, trademark, and
103
+ attribution notices from the Source form of the Work,
104
+ excluding those notices that do not pertain to any part of
105
+ the Derivative Works; and
106
+
107
+ (d) If the Work includes a "NOTICE" text file as part of its
108
+ distribution, then any Derivative Works that You distribute must
109
+ include a readable copy of the attribution notices contained
110
+ within such NOTICE file, excluding those notices that do not
111
+ pertain to any part of the Derivative Works, in at least one
112
+ of the following places: within a NOTICE text file distributed
113
+ as part of the Derivative Works; within the Source form or
114
+ documentation, if provided along with the Derivative Works; or,
115
+ within a display generated by the Derivative Works, if and
116
+ wherever such third-party notices normally appear. The contents
117
+ of the NOTICE file are for informational purposes only and
118
+ do not modify the License. You may add Your own attribution
119
+ notices within Derivative Works that You distribute, alongside
120
+ or as an addendum to the NOTICE text from the Work, provided
121
+ that such additional attribution notices cannot be construed
122
+ as modifying the License.
123
+
124
+ You may add Your own copyright statement to Your modifications and
125
+ may provide additional or different license terms and conditions
126
+ for use, reproduction, or distribution of Your modifications, or
127
+ for any such Derivative Works as a whole, provided Your use,
128
+ reproduction, and distribution of the Work otherwise complies with
129
+ the conditions stated in this License.
130
+
131
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
132
+ any Contribution intentionally submitted for inclusion in the Work
133
+ by You to the Licensor shall be under the terms and conditions of
134
+ this License, without any additional terms or conditions.
135
+ Notwithstanding the above, nothing herein shall supersede or modify
136
+ the terms of any separate license agreement you may have executed
137
+ with Licensor regarding such Contributions.
138
+
139
+ 6. Trademarks. This License does not grant permission to use the trade
140
+ names, trademarks, service marks, or product names of the Licensor,
141
+ except as required for reasonable and customary use in describing the
142
+ origin of the Work and reproducing the content of the NOTICE file.
143
+
144
+ 7. Disclaimer of Warranty. Unless required by applicable law or
145
+ agreed to in writing, Licensor provides the Work (and each
146
+ Contributor provides its Contributions) on an "AS IS" BASIS,
147
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
148
+ implied, including, without limitation, any warranties or conditions
149
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
150
+ PARTICULAR PURPOSE. You are solely responsible for determining the
151
+ appropriateness of using or redistributing the Work and assume any
152
+ risks associated with Your exercise of permissions under this License.
153
+
154
+ 8. Limitation of Liability. In no event and under no legal theory,
155
+ whether in tort (including negligence), contract, or otherwise,
156
+ unless required by applicable law (such as deliberate and grossly
157
+ negligent acts) or agreed to in writing, shall any Contributor be
158
+ liable to You for damages, including any direct, indirect, special,
159
+ incidental, or consequential damages of any character arising as a
160
+ result of this License or out of the use or inability to use the
161
+ Work (including but not limited to damages for loss of goodwill,
162
+ work stoppage, computer failure or malfunction, or any and all
163
+ other commercial damages or losses), even if such Contributor
164
+ has been advised of the possibility of such damages.
165
+
166
+ 9. Accepting Warranty or Additional Liability. While redistributing
167
+ the Work or Derivative Works thereof, You may choose to offer,
168
+ and charge a fee for, acceptance of support, warranty, indemnity,
169
+ or other liability obligations and/or rights consistent with this
170
+ License. However, in accepting such obligations, You may act only
171
+ on Your own behalf and on Your sole responsibility, not on behalf
172
+ of any other Contributor, and only if You agree to indemnify,
173
+ defend, and hold each Contributor harmless for any liability
174
+ incurred by, or claims asserted against, such Contributor by reason
175
+ of your accepting any such warranty or additional liability.
176
+
177
+ END OF TERMS AND CONDITIONS
178
+
179
+ APPENDIX: How to apply the Apache License to your work.
180
+
181
+ To apply the Apache License to your work, attach the following
182
+ boilerplate notice, with the fields enclosed by brackets "[]"
183
+ replaced with your own identifying information. (Don't include
184
+ the brackets!) The text should be enclosed in the appropriate
185
+ comment syntax for the file format. We also recommend that a
186
+ file or class name and description of purpose be included on the
187
+ same "printed page" as the copyright notice for easier
188
+ identification within third-party archives.
189
+
190
+ Copyright [yyyy] [name of copyright owner]
191
+
192
+ Licensed under the Apache License, Version 2.0 (the "License");
193
+ you may not use this file except in compliance with the License.
194
+ You may obtain a copy of the License at
195
+
196
+ http://www.apache.org/licenses/LICENSE-2.0
197
+
198
+ Unless required by applicable law or agreed to in writing, software
199
+ distributed under the License is distributed on an "AS IS" BASIS,
200
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
201
+ See the License for the specific language governing permissions and
202
+ limitations under the License.
Makefile ADDED
@@ -0,0 +1,356 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # A Self-Documenting Makefile: http://marmelab.com/blog/2016/02/29/auto-documented-makefile.html
2
+
3
+ ROOT_DIR := $(dir $(abspath $(lastword $(MAKEFILE_LIST))))
4
+
5
+ # Docker-local svix secret, only used for testing
6
+ SVIX_JWT_SECRET = DUMMY_JWT_SECRET
7
+
8
+ # dynamic forces confluent-kafka-go to build against local librdkafka
9
+ GO_BUILD_FLAGS = -tags=dynamic
10
+ GO_TEST_PACKAGE_PARALLELISM ?= 128
11
+ GO_TEST_FLAGS = -p ${GO_TEST_PACKAGE_PARALLELISM} -parallel 16 ${GO_BUILD_FLAGS}
12
+ GOTESTSUM_FLAGS ?= --format pkgname-and-test-fails --hide-summary=skipped
13
+ GO_LINT_PATH ?= ./...
14
+
15
+ .PHONY: up
16
+ up: ## Start the dependencies via docker compose. `export COMPOSE_PROFILES=dev,redis,...`
17
+ $(call print-target)
18
+ docker compose up -d
19
+
20
+ .PHONY: down
21
+ down: ## Stop the dependencies via docker compose
22
+ $(call print-target)
23
+ docker compose down --remove-orphans --volumes
24
+
25
+ .PHONY: patch-oapi-templates
26
+ patch-oapi-templates: ## Patch oapi-codegen chi-middleware template with custom filter parsing
27
+ $(call print-target)
28
+ @go mod download github.com/oapi-codegen/oapi-codegen/v2
29
+ @OAPI_MOD_DIR=$$(go list -m -f '{{.Dir}}' github.com/oapi-codegen/oapi-codegen/v2) && \
30
+ if [ -z "$$OAPI_MOD_DIR" ]; then echo "error: could not locate oapi-codegen/v2 module dir"; exit 1; fi && \
31
+ cp "$$OAPI_MOD_DIR/pkg/codegen/templates/chi/chi-middleware.tmpl" api/v3/templates/chi-middleware.tmpl && \
32
+ chmod u+w api/v3/templates/chi-middleware.tmpl && \
33
+ patch -p1 -d api/v3/templates < api/v3/templates/chi-middleware.tmpl.patch
34
+
35
+ .PHONY: update-openapi
36
+ update-openapi: patch-oapi-templates ## Update OpenAPI spec
37
+ $(call print-target)
38
+ $(MAKE) -C api/spec generate
39
+ go generate ./api/...
40
+
41
+ .PHONY: generate-javascript-sdk
42
+ generate-javascript-sdk: ## Generate JavaScript SDK
43
+ $(call print-target)
44
+ $(MAKE) -C api/client/javascript generate
45
+
46
+ .PHONY: gen-api
47
+ gen-api: update-openapi generate-javascript-sdk ## Generate API and SDKs
48
+ $(call print-target)
49
+
50
+ .PHONY: generate-all
51
+ generate-all: update-openapi generate-javascript-sdk ## Execute all code generators
52
+ $(call print-target)
53
+ go generate ./...
54
+
55
+ .PHONY: migrate-check
56
+ migrate-check: migrate-check-schema migrate-check-diff migrate-check-lint migrate-check-validate ## Validate migrations
57
+
58
+ .PHONY: migrate-check-schema
59
+ migrate-check-schema: ## Ensure ent schema is in sync with generated code
60
+ $(call print-target)
61
+ go generate -x ./openmeter/ent/...
62
+ @if ! git diff --quiet -- openmeter/ent || [ -n "$$(git ls-files --others --exclude-standard -- openmeter/ent)" ]; then \
63
+ git --no-pager diff -- openmeter/ent; \
64
+ git ls-files --others --exclude-standard -- openmeter/ent; \
65
+ echo "!!! schema is not in sync with generated code — run 'go generate ./openmeter/ent/...' and commit the changes !!!"; \
66
+ exit 1; \
67
+ fi
68
+
69
+ .PHONY: migrate-check-diff
70
+ migrate-check-diff: ## Ensure migrations are in sync with schema (runs atlas migrate diff against a clean target)
71
+ $(call print-target)
72
+ atlas migrate --env local diff migrate-check >/dev/null
73
+ @if ! git diff --quiet -- tools/migrate/migrations || [ -n "$$(git ls-files --others --exclude-standard -- tools/migrate/migrations)" ]; then \
74
+ git --no-pager diff -- tools/migrate/migrations; \
75
+ git ls-files --others --exclude-standard -- tools/migrate/migrations; \
76
+ echo "!!! migrations are not in sync with schema — run 'atlas migrate --env local diff <name>' and commit the generated files !!!"; \
77
+ exit 1; \
78
+ fi
79
+
80
+ .PHONY: migrate-check-lint
81
+ migrate-check-lint: ## Lint the last 10 migrations
82
+ $(call print-target)
83
+ atlas migrate --env local lint --latest 10
84
+
85
+ .PHONY: migrate-check-validate
86
+ migrate-check-validate: ## Validate migration checksums
87
+ $(call print-target)
88
+ atlas migrate --env local validate
89
+
90
+ .PHONY: generate-sqlc-testdata
91
+ generate-sqlc-testdata: ## Generate SQLC testdata for a specific version (make generate-sqlc-testdata VERSION=20240826120919)
92
+ $(call print-target)
93
+ @if [ -z "$(VERSION)" ]; then echo "Usage: make generate-sqlc-testdata VERSION=<migration_version>"; exit 1; fi
94
+ VERSION=$(VERSION) ./tools/migrate/generate-sqlc-testdata.sh
95
+
96
+ .PHONY: generate
97
+ generate: patch-oapi-templates ## Generate code
98
+ $(call print-target)
99
+ go generate ./...
100
+
101
+ .PHONY: generate-view-sql
102
+ generate-view-sql: ## Generate SQL for ent.View schemas
103
+ $(call print-target)
104
+ go run ./tools/migrate/cmd/viewgen
105
+
106
+ .PHONY: build-dir
107
+ build-dir:
108
+ @mkdir -p build
109
+
110
+ .PHONY: build
111
+ build: build-server build-sink-worker build-benthos-collector build-balance-worker build-billing-worker build-notification-service build-jobs ## Build all binaries
112
+
113
+ COLLECTOR_DIR := $(ROOT_DIR)/collector
114
+ COLLECTOR_RELEASE_OUTPUT_DIR := $(ROOT_DIR)/build/release/benthos-collector_$(GOOS)_$(GOARCH)
115
+
116
+ collector-release-output-dir:
117
+ $(if $(GOOS),,$(error GOOS is not set))
118
+ $(if $(GOARCH),,$(error GOARCH is not set))
119
+ @mkdir -p $(COLLECTOR_RELEASE_OUTPUT_DIR)
120
+
121
+ # Cross-compile the benthos-collector binary for release archives.
122
+ # Usage: make build-benthos-collector-release GOOS=linux GOARCH=amd64 VERSION=v1.2.3
123
+ # Produces build/release/benthos-collector_<GOOS>_<GOARCH>/benthos (+ README.md, LICENSE)
124
+ .PHONY: build-benthos-collector-release
125
+ build-benthos-collector-release: | collector-release-output-dir ## Cross-compile benthos-collector for release (set GOOS/GOARCH/VERSION)
126
+ $(call print-target)
127
+ $(if $(GOOS),,$(error GOOS is not set))
128
+ $(if $(GOARCH),,$(error GOARCH is not set))
129
+ @rm -rf "$(COLLECTOR_RELEASE_OUTPUT_DIR)"/* && \
130
+ CGO_ENABLED=0 GOOS=$(GOOS) GOARCH=$(GOARCH) \
131
+ go build -C $(COLLECTOR_DIR) -trimpath \
132
+ -ldflags "-s -w -X main.version=$(or $(VERSION),unknown)" \
133
+ -o "$(COLLECTOR_RELEASE_OUTPUT_DIR)/benthos" ./cmd && \
134
+ cp README.md LICENSE "$(COLLECTOR_RELEASE_OUTPUT_DIR)/"
135
+
136
+ COLLECTOR_RELEASE_NAME := benthos-collector_$(GOOS)_$(GOARCH)
137
+
138
+ # Produces build/release/benthos-collector_<GOOS>_<GOARCH>.tar.gz from the directory above.
139
+ .PHONY: archive-benthos-collector-release
140
+ archive-benthos-collector-release: ## Archive the cross-compiled benthos-collector (set GOOS/GOARCH)
141
+ $(call print-target)
142
+ $(if $(GOOS),,$(error GOOS is not set))
143
+ $(if $(GOARCH),,$(error GOARCH is not set))
144
+ @tar -C build/release -czf "build/release/$(COLLECTOR_RELEASE_NAME).tar.gz" "$(COLLECTOR_RELEASE_NAME)"
145
+
146
+ .PHONY: build-server
147
+ build-server: | build-dir ## Build server binary
148
+ $(call print-target)
149
+ go build -o build/server ${GO_BUILD_FLAGS} ./cmd/server
150
+
151
+ .PHONY: build-sink-worker
152
+ build-sink-worker: | build-dir ## Build sink-worker binary
153
+ $(call print-target)
154
+ go build -o build/sink-worker ${GO_BUILD_FLAGS} ./cmd/sink-worker
155
+
156
+ .PHONY: build-benthos-collector
157
+ build-benthos-collector: | build-dir ## Build benthos collector binary
158
+ $(call print-target)
159
+ go build -C $(COLLECTOR_DIR) -o ../build/benthos-collector ${GO_BUILD_FLAGS} ./cmd
160
+
161
+ .PHONY: build-balance-worker
162
+ build-balance-worker: | build-dir ## Build balance-worker binary
163
+ $(call print-target)
164
+ go build -o build/balance-worker ${GO_BUILD_FLAGS} ./cmd/balance-worker
165
+
166
+ .PHONY: build-billing-worker
167
+ build-billing-worker: | build-dir ## Build billing-worker binary
168
+ $(call print-target)
169
+ go build -o build/billing-worker ${GO_BUILD_FLAGS} ./cmd/billing-worker
170
+
171
+ .PHONY: build-notification-service
172
+ build-notification-service: | build-dir ## Build notification-service binary
173
+ $(call print-target)
174
+ go build -o build/notification-service ${GO_BUILD_FLAGS} ./cmd/notification-service
175
+
176
+ .PHONY: build-jobs
177
+ build-jobs: | build-dir ## Build jobs binary
178
+ $(call print-target)
179
+ go build -o build/jobs ${GO_BUILD_FLAGS} ./cmd/jobs
180
+
181
+ config.yaml:
182
+ cp config.example.yaml config.yaml
183
+
184
+ .PHONY: server
185
+ server: ## Run sink-worker
186
+ @ if [ config.yaml -ot config.example.yaml ]; then diff -u config.yaml config.example.yaml || (echo "!!! The configuration example changed. Please update your config.yaml file accordingly (or at least touch it). !!!" && false); fi
187
+ $(call print-target)
188
+ air -c ./cmd/server/.air.toml
189
+
190
+ .PHONY: sink-worker
191
+ sink-worker: ## Run sink-worker
192
+ @ if [ config.yaml -ot config.example.yaml ]; then diff -u config.yaml config.example.yaml || (echo "!!! The configuration example changed. Please update your config.yaml file accordingly (or at least touch it). !!!" && false); fi
193
+ $(call print-target)
194
+ air -c ./cmd/sink-worker/.air.toml
195
+
196
+ .PHONY: balance-worker
197
+ balance-worker: ## Run balance-worker
198
+ @ if [ config.yaml -ot config.example.yaml ]; then diff -u config.yaml config.example.yaml || (echo "!!! The configuration example changed. Please update your config.yaml file accordingly (or at least touch it). !!!" && false); fi
199
+ $(call print-target)
200
+ air -c ./cmd/balance-worker/.air.toml
201
+
202
+ .PHONY: billing-worker
203
+ billing-worker: ## Run billing-worker
204
+ @ if [ config.yaml -ot config.example.yaml ]; then diff -u config.yaml config.example.yaml || (echo "!!! The configuration example changed. Please update your config.yaml file accordingly (or at least touch it). !!!" && false); fi
205
+ $(call print-target)
206
+ air -c ./cmd/billing-worker/.air.toml
207
+
208
+ .PHONY: notification-service
209
+ notification-service: ## Run notification-service
210
+ @ if [ config.yaml -ot config.example.yaml ]; then diff -u config.yaml config.example.yaml || (echo "!!! The configuration example changed. Please update your config.yaml file accordingly (or at least touch it). !!!" && false); fi
211
+ $(call print-target)
212
+ air -c ./cmd/notification-service/.air.toml
213
+
214
+ .PHONY: llm-cost-sync
215
+ llm-cost-sync: ## Sync LLM cost prices from external sources
216
+ $(call print-target)
217
+ go run ./cmd/jobs llm-cost sync
218
+
219
+ .PHONY: etoe
220
+ etoe: ## Run e2e tests
221
+ $(call print-target)
222
+ $(MAKE) -C e2e test-local
223
+
224
+ .PHONY: etoe-slow
225
+ etoe-slow: ## Run e2e tests with slow tests enabled
226
+ $(call print-target)
227
+ export RUN_SLOW_TESTS=1
228
+ $(MAKE) -C e2e test-local
229
+
230
+
231
+ .PHONY: test
232
+ test: ## Run tests
233
+ $(call print-target)
234
+ PGPASSWORD=postgres psql -h 127.0.0.1 -U postgres postgres -c "SELECT version();" || (echo "!!! Postgres is not running. Please start it with 'docker compose up -d postgres' !!!" && false)
235
+ POSTGRES_HOST=127.0.0.1 gotestsum $(GOTESTSUM_FLAGS) -- $(GO_TEST_FLAGS) ./...
236
+
237
+ .PHONY: test-nocache
238
+ test-nocache: ## Run tests without cache
239
+ $(call print-target)
240
+ PGPASSWORD=postgres psql -h 127.0.0.1 -U postgres postgres -c "SELECT version();" || (echo "!!! Postgres is not running. Please start it with 'docker compose up -d postgres' !!!" && false)
241
+ POSTGRES_HOST=127.0.0.1 gotestsum $(GOTESTSUM_FLAGS) -- $(GO_TEST_FLAGS) -count=1 ./...
242
+
243
+ .PHONY: test-all
244
+ test-all: ## Run tests with svix dependencies, bypassing the test cache
245
+ $(call print-target)
246
+ docker compose up -d postgres svix redis
247
+ ./tools/wait-for-compose.sh postgres svix redis
248
+ SVIX_HOST="localhost" SVIX_JWT_SECRET="$(SVIX_JWT_SECRET)" gotestsum $(GOTESTSUM_FLAGS) -- $(GO_TEST_FLAGS) -count=1 ./...
249
+
250
+ .PHONY: test-go-sdk
251
+ test-go-sdk: ## Build, vet, and test the v3 Go SDK module (api/v3/client)
252
+ $(call print-target)
253
+ cd api/v3/client && go build ./... && go vet ./... && gotestsum $(GOTESTSUM_FLAGS) -- ./...
254
+
255
+ .PHONY: lint
256
+ lint: lint-go lint-api-spec lint-openapi lint-helm ## Run linters
257
+ $(call print-target)
258
+
259
+ .PHONY: lint-api-spec
260
+ lint-api-spec: ## Lint OpenAPI spec
261
+ $(call print-target)
262
+ $(MAKE) -C api/spec lint
263
+
264
+ .PHONY: test-api-spec
265
+ test-api-spec: ## Run AIP TypeScript SDK and emitter tests
266
+ $(call print-target)
267
+ $(MAKE) -C api/spec test
268
+
269
+ .PHONY: lint-openapi
270
+ lint-openapi: ## Lint OpenAPI spec
271
+ $(call print-target)
272
+ spectral lint api/openapi.yaml api/openapi.cloud.yaml api/v3/openapi.yaml
273
+
274
+ .PHONY: lint-helm
275
+ lint-helm: ## Lint Helm charts
276
+ $(call print-target)
277
+ helm lint deploy/charts/openmeter
278
+ helm lint deploy/charts/benthos-collector
279
+
280
+ # Package a helm chart for release.
281
+ # Usage: make package-helm-chart CHART=openmeter VERSION=v1.2.3
282
+ # Produces build/helm/<CHART>-<version-without-v>.tgz
283
+ .PHONY: package-helm-chart
284
+ package-helm-chart: ## Package a helm chart for release (set CHART and VERSION)
285
+ $(call print-target)
286
+ @if [ -z "$(CHART)" ] || [ -z "$(VERSION)" ]; then echo "ERROR: CHART and VERSION are required"; exit 1; fi
287
+ @chart_dir="deploy/charts/$(CHART)" && \
288
+ version_no_v="$(VERSION:v%=%)" && \
289
+ mkdir -p build/helm && \
290
+ helm-docs --log-level info -s file -c "$$chart_dir" \
291
+ -t "deploy/charts/template.md" -t "$$chart_dir/README.tmpl.md" && \
292
+ helm dependency update "$$chart_dir" && \
293
+ helm package "$$chart_dir" \
294
+ --version "$$version_no_v" \
295
+ --app-version "$(VERSION)" \
296
+ --destination build/helm
297
+
298
+ .PHONY: lint-go
299
+ lint-go: ## Lint Go code
300
+ $(call print-target)
301
+ golangci-lint run -v $(GO_LINT_PATH)
302
+ cd api/v3/client && golangci-lint run -v ./...
303
+ go vet -C e2e ./...
304
+ cd e2e && golangci-lint run -v ./...
305
+
306
+ .PHONY: lint-go-fast
307
+ lint-go-fast: ## Lint Go bug-finding checks (set GO_LINT_PATH=./openmeter/ledger/...)
308
+ $(call print-target)
309
+ golangci-lint run -v --config .golangci-fast.yaml $(GO_LINT_PATH)
310
+
311
+ .PHONY: lint-go-style
312
+ lint-go-style: ## Lint Go formatting and import order
313
+ $(call print-target)
314
+ golangci-lint fmt -v -d $(GO_LINT_PATH)
315
+
316
+ .PHONY: lint-go-head
317
+ lint-go-head: ## Lint Go code since last commit
318
+ $(call print-target)
319
+ golangci-lint run --new-from-rev=HEAD~1
320
+
321
+ .PHONY: ci
322
+ ci: ## Run CI checks
323
+ $(call print-target)
324
+ $(MAKE) generate-all
325
+ $(MAKE) -j 10 lint test etoe
326
+
327
+ .PHONY: fmt
328
+ fmt: ## Format code
329
+ $(call print-target)
330
+ golangci-lint run --fix
331
+
332
+ .PHONY: mod
333
+ mod: ## go mod tidy
334
+ $(call print-target)
335
+ go mod tidy
336
+ go mod tidy -C collector
337
+ go mod tidy -C api/v3/client
338
+ go mod tidy -C e2e
339
+
340
+ .PHONY: seed
341
+ seed: ## Seed OpenMeter with test data
342
+ $(call print-target)
343
+ benthos -c etc/seed/seed.yaml
344
+
345
+ .PHONY: help
346
+ .DEFAULT_GOAL := help
347
+ help:
348
+ @grep -h -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}'
349
+
350
+ # Variable outputting/exporting rules
351
+ var-%: ; @echo $($*)
352
+ varexport-%: ; @echo $*=$($*)
353
+
354
+ define print-target
355
+ @printf "Executing target: \033[36m$@\033[0m\n"
356
+ endef
README.md CHANGED
@@ -1,10 +1,163 @@
1
  ---
2
  title: Openmeter
3
- emoji: 🏃
4
- colorFrom: blue
5
- colorTo: red
6
  sdk: docker
7
- pinned: false
8
  ---
9
 
10
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
  title: Openmeter
 
 
 
3
  sdk: docker
4
+ app_port: 7860
5
  ---
6
 
7
+ <div align="center">
8
+
9
+ ![OpenMeter logo](assets/logo.png)
10
+
11
+ # OpenMeter
12
+
13
+ The open-source metering and billing platform
14
+ for AI, agentic and DevTool monetization.
15
+
16
+ [Docs](https://openmeter.io/docs) |
17
+ [Hosted](https://cloud.konghq.com/register?utm_campaign=metering_and_billing) |
18
+ [Blog](https://openmeter.io/blog) |
19
+ [Contributing](CONTRIBUTING.md)
20
+
21
+ [![GitHub Release](https://img.shields.io/github/v/release/openmeterio/openmeter?style=flat-square)](https://github.com/openmeterio/openmeter/releases/latest)
22
+ [![CI Status](https://img.shields.io/github/actions/workflow/status/openmeterio/openmeter/ci.yaml?style=flat-square)](https://github.com/openmeterio/openmeter/actions/workflows/ci.yaml)
23
+ [![Go Report Card](https://goreportcard.com/badge/github.com/openmeterio/openmeter?style=flat-square)](https://goreportcard.com/report/github.com/openmeterio/openmeter)
24
+ ![GitHub Stars](https://img.shields.io/github/stars/openmeterio/openmeter?style=flat-square)
25
+
26
+ </div>
27
+
28
+ ---
29
+
30
+ OpenMeter is a real-time metering and billing engine that
31
+ helps you track usage, enforce limits, manage subscriptions,
32
+ and automate invoicing — all in one platform. Ingest events
33
+ via a simple API, define meters with flexible aggregations,
34
+ and connect usage data to billing, entitlements, and
35
+ customer-facing dashboards.
36
+
37
+ ## Features
38
+
39
+ - **Usage Metering** — Ingest events in
40
+ [CloudEvents](https://cloudevents.io) format, define meters
41
+ with flexible aggregations (SUM, COUNT, AVG, MIN, MAX),
42
+ and query usage in real time.
43
+ - **Usage-Based Billing** — Generate invoices from metered
44
+ usage. Supports tiered, graduated, and flat-fee pricing
45
+ with automated invoice lifecycle management.
46
+ - **Usage Limits and Entitlements** — Enforce usage quotas
47
+ per feature with real-time balance tracking, boolean
48
+ feature flags, and grace periods.
49
+ - **Product Catalog** — Define plans, add-ons, features, and
50
+ rate cards. Manage subscriptions with mid-cycle changes,
51
+ prorating, and alignment.
52
+ - **Prepaid Credits** — Support paid or promotional credit grants
53
+ with priority-based burn-down and expiration.
54
+ - **Customer Portal** — Token-based self-service dashboards
55
+ so your customers can see their own usage.
56
+ - **Notifications** — Webhook-based alerts with configurable
57
+ rules and channels for usage thresholds and billing events.
58
+ - **LLM Cost Tracking** — First-class support for metering
59
+ AI token usage and computing model-specific costs.
60
+
61
+ ## Getting Started
62
+
63
+ ### Cloud
64
+
65
+ The fastest way to start.
66
+ [Start for free](https://cloud.konghq.com/register?utm_campaign=metering_and_billing)
67
+ and begin metering and billing in minutes —
68
+ no infrastructure to manage.
69
+
70
+ ### Self-Hosted
71
+
72
+ Run OpenMeter locally with Docker Compose:
73
+
74
+ ```sh
75
+ git clone git@github.com:openmeterio/openmeter.git
76
+ cd openmeter/quickstart
77
+ docker compose up -d
78
+ ```
79
+
80
+ Then ingest your first event:
81
+
82
+ ```sh
83
+ curl -X POST http://localhost:48888/api/v1/events \
84
+ -H 'Content-Type: application/cloudevents+json' \
85
+ --data-raw '{
86
+ "specversion": "1.0",
87
+ "type": "request",
88
+ "id": "00001",
89
+ "time": "2026-07-07T00:00:00.001Z",
90
+ "source": "my-service",
91
+ "subject": "customer-1",
92
+ "data": { "method": "GET", "route": "/api/hello" }
93
+ }'
94
+ ```
95
+
96
+ Query your usage:
97
+
98
+ ```sh
99
+ curl 'http://localhost:48888/api/v1/meters/api_requests_total/query?windowSize=HOUR' | jq
100
+ ```
101
+
102
+ See the full [quickstart guide](/quickstart) for more details.
103
+
104
+ ### Deploy to Production
105
+
106
+ Deploy to Kubernetes using our
107
+ [Helm chart](https://openmeter.io/docs/deploy/kubernetes).
108
+
109
+ ## SDKs
110
+
111
+ | Language | Package | Source |
112
+ |----------------------|--------------------------------------------------------------------------------|----------------------------------------------------|
113
+ | Go | [openmeter](https://pkg.go.dev/github.com/openmeterio/openmeter/api/client/go) | [api/client/go](/api/client/go) |
114
+ | JavaScript / Node.js | [@openmeter/sdk](https://www.npmjs.com/package/@openmeter/sdk) | [api/client/javascript](/api/client/javascript) |
115
+ | Python | [openmeter](https://pypi.org/project/openmeter) | [api/client/python](/api/client/python) |
116
+
117
+ Don't see your language? Use the
118
+ [OpenAPI spec](https://github.com/openmeterio/openmeter/blob/main/api/openapi.yaml)
119
+ directly or
120
+ [request an SDK](https://github.com/openmeterio/openmeter/issues/new?assignees=&labels=area%2Fapi%2Ckind%2Ffeature&projects=&template=feature_request.yaml).
121
+
122
+ ## Architecture
123
+
124
+ OpenMeter is built in Go with a stack optimized for
125
+ high-volume event ingestion and real-time aggregation:
126
+
127
+ | Component | Role |
128
+ |--------------------------|----------------------------------------------------------|
129
+ | **PostgreSQL** (Ent ORM) | Billing, subscriptions, entitlements, product catalog |
130
+ | **ClickHouse** | Real-time usage aggregation and analytics |
131
+ | **Kafka** | Event streaming and ingestion pipeline |
132
+ | **TypeSpec** | API-first design — OpenAPI spec and SDKs from TypeSpec |
133
+
134
+ ## Community
135
+
136
+ We'd love to have you involved:
137
+
138
+ - **[Contributing](CONTRIBUTING.md)** — Start here if you
139
+ want to contribute code.
140
+ - **[Code of Conduct](CODE_OF_CONDUCT.md)** — Our community
141
+ guidelines.
142
+ - **[Blog](https://openmeter.io/blog)** — Product updates
143
+ and engineering deep dives.
144
+
145
+ ## Development
146
+
147
+ Prerequisites: [Nix](https://nixos.org/download.html) and
148
+ [direnv](https://direnv.net/docs/installation.html) are
149
+ recommended. See [CONTRIBUTING.md](CONTRIBUTING.md) for
150
+ detailed setup instructions.
151
+
152
+ ```sh
153
+ make up # Start dependencies (Postgres, Kafka, ClickHouse)
154
+ make server # Run the API server with hot reload
155
+ make test # Run tests
156
+ make lint # Run linters
157
+ ```
158
+
159
+ ## License
160
+
161
+ Licensed under [Apache 2.0](LICENSE).
162
+
163
+ [![FOSSA Status](https://app.fossa.com/api/projects/custom%2B38090%2Fgithub.com%2Fopenmeterio%2Fopenmeter.svg?type=large)](https://app.fossa.com/projects/custom%2B38090%2Fgithub.com%2Fopenmeterio%2Fopenmeter?ref=badge_large)
SECURITY.md ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Security Policies and Procedures
2
+
3
+ This document outlines security procedures and general policies for OpenMeter.
4
+
5
+ - [Reporting a vulnerability](#reporting-a-vulnerability)
6
+ - [Disclosure policy](#disclosure-policy)
7
+
8
+ ## Reporting a vulnerability
9
+
10
+ The OpenMeter team and community take all security issues seriously. Thank you for improving the security of our projects.
11
+ We appreciate your efforts and responsible disclosure and will make every effort to acknowledge your contributions.
12
+
13
+ **Report security issues using GitHub's [vulnerability reporting feature](https://docs.github.com/en/code-security/security-advisories/guidance-on-reporting-and-writing/privately-reporting-a-security-vulnerability).**
14
+
15
+ _Alternatively, you can send an email to `security@openmeter.io`._
16
+
17
+ Somebody from the OpenMeter team will acknowledge your report within 48 hours,
18
+ and will follow up with a more detailed response after that indicating the next steps in handling your report.
19
+ After the initial reply to your report, the team will endeavor to keep you informed of the progress towards a fix and full announcement,
20
+ and may ask for additional information or guidance.
21
+
22
+ ## Disclosure policy
23
+
24
+ When the team receives a vulnerability report, they will assign it to a primary handler.
25
+ This person will coordinate the fix and release process, involving the following steps:
26
+
27
+ - Confirm the problem and determine the affected versions.
28
+ - Audit code to find any potential similar problems.
29
+ - Prepare fixes for all releases still under maintenance. These fixes will be released as quickly as possible.
api/api.gen.go ADDED
The diff for this file is too large to render. See raw diff
 
api/api.go ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ //go:generate go tool github.com/oapi-codegen/oapi-codegen/v2/cmd/oapi-codegen --config=codegen.yaml ./openapi.yaml
2
+ package api
api/client/go/README.md ADDED
@@ -0,0 +1,65 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # OpenMeter Go SDK
2
+
3
+ ## Install
4
+
5
+ ```sh
6
+ go get github.com/openmeterio/openmeter/api/client/go@v1.0.0-beta.53
7
+ ```
8
+
9
+ ## Usage
10
+
11
+ Initialize client.
12
+
13
+ ```go
14
+ import (
15
+ cloudevents "github.com/cloudevents/sdk-go/v2/event"
16
+ om "github.com/openmeterio/openmeter/api/client/go"
17
+ )
18
+
19
+ func main() {
20
+ // Initialize OpenMeter client
21
+ om, err := openmeter.NewClientWithResponses("http://localhost:8888")
22
+ if err != nil {
23
+ panic(err.Error())
24
+ }
25
+
26
+ // Use OpenMeter client
27
+ // ...
28
+ }
29
+ ```
30
+
31
+ ### Ingest Event
32
+
33
+ Report usage to OpenMeter.
34
+
35
+ ```go
36
+ e := cloudevents.New()
37
+ e.SetID("00001")
38
+ e.SetSource("my-app")
39
+ e.SetType("tokens")
40
+ e.SetSubject("user-id")
41
+ e.SetTime(time.Now())
42
+ e.SetData("application/json", map[string]string{
43
+ "tokens": "15",
44
+ "model": "gpt-4",
45
+ })
46
+
47
+ _, err := client.IngestEventWithResponse(ctx, e)
48
+ ```
49
+
50
+ ### Query Meter
51
+
52
+ Retreive usage from OpenMeter.
53
+
54
+ ```go
55
+ slug := "token-usage"
56
+ subject := []string{"user-id"}
57
+ from, _ := time.Parse(time.RFC3339, "2023-01-01T00:00:00Z")
58
+ to, _ := time.Parse(time.RFC3339, "2023-01-02T00:00:00Z")
59
+ resp, _ := client.QueryMeterWithResponse(ctx, slug, &om.QueryMeterParams{
60
+ Subject: &subject,
61
+ From: &from,
62
+ To: &to,
63
+ })
64
+ // resp.JSON200.Data
65
+ ```
api/client/go/client.gen.go ADDED
The diff for this file is too large to render. See raw diff
 
api/client/go/client.go ADDED
@@ -0,0 +1,48 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ //go:generate go tool github.com/oapi-codegen/oapi-codegen/v2/cmd/oapi-codegen --config=codegen.yaml ../../openapi.cloud.yaml
2
+ package openmeter
3
+
4
+ import (
5
+ "context"
6
+ "fmt"
7
+ "net/http"
8
+ )
9
+
10
+ func NewAuthClientWithResponses(server string, apiSecret string, opts ...ClientOption) (*ClientWithResponses, error) {
11
+ o := []ClientOption{WithRequestEditorFn(func(ctx context.Context, req *http.Request) error {
12
+ req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", apiSecret))
13
+ return nil
14
+ })}
15
+ o = append(opts, o...)
16
+
17
+ return NewClientWithResponses(server, o...)
18
+ }
19
+
20
+ func NewAuthClient(server string, apiSecret string, opts ...ClientOption) (*Client, error) {
21
+ o := []ClientOption{WithRequestEditorFn(func(ctx context.Context, req *http.Request) error {
22
+ req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", apiSecret))
23
+ return nil
24
+ })}
25
+ o = append(opts, o...)
26
+
27
+ return NewClient(server, o...)
28
+ }
29
+
30
+ // IngestEvents is a wrapper around generated client's IngestEventsWithApplicationCloudeventsPlusJSONBody
31
+ func (c *Client) IngestEvent(ctx context.Context, event Event, reqEditors ...RequestEditorFn) (*http.Response, error) {
32
+ return c.IngestEventsWithApplicationCloudeventsPlusJSONBody(ctx, event, reqEditors...)
33
+ }
34
+
35
+ // IngestEvents is a wrapper around generated client's IngestEventsWithApplicationCloudeventsBatchPlusJSONBody
36
+ func (c *Client) IngestEventBatch(ctx context.Context, events []Event, reqEditors ...RequestEditorFn) (*http.Response, error) {
37
+ return c.IngestEventsWithApplicationCloudeventsBatchPlusJSONBody(ctx, events, reqEditors...)
38
+ }
39
+
40
+ // IngestEventsWithResponse is a wrapper around generated client's IngestEventsWithApplicationCloudeventsPlusJSONBodyWithResponse
41
+ func (c *ClientWithResponses) IngestEventWithResponse(ctx context.Context, event Event, reqEditors ...RequestEditorFn) (*IngestEventsResponse, error) {
42
+ return c.IngestEventsWithApplicationCloudeventsPlusJSONBodyWithResponse(ctx, event, reqEditors...)
43
+ }
44
+
45
+ // IngestEventsWithResponse is a wrapper around generated client's IngestEventsWithApplicationCloudeventsBatchPlusJSONBodyWithResponse
46
+ func (c *ClientWithResponses) IngestEventBatchWithResponse(ctx context.Context, events []Event, reqEditors ...RequestEditorFn) (*IngestEventsResponse, error) {
47
+ return c.IngestEventsWithApplicationCloudeventsBatchPlusJSONBodyWithResponse(ctx, events, reqEditors...)
48
+ }
api/client/go/client_test.go ADDED
@@ -0,0 +1,327 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package openmeter
2
+
3
+ import (
4
+ "context"
5
+ "encoding/json"
6
+ "net/http"
7
+ "net/http/httptest"
8
+ "testing"
9
+ "time"
10
+
11
+ cloudevents "github.com/cloudevents/sdk-go/v2/event"
12
+ "github.com/samber/lo"
13
+ "github.com/stretchr/testify/assert"
14
+
15
+ "github.com/openmeterio/openmeter/openmeter/meter"
16
+ )
17
+
18
+ func TestIngest(t *testing.T) {
19
+ ctx := context.Background()
20
+
21
+ // Create a mock server to test the client
22
+ server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
23
+ assert.Equal(t, http.MethodPost, r.Method)
24
+ assert.Equal(t, "/api/v1/events", r.URL.Path)
25
+ assert.Equal(t, "application/cloudevents+json", r.Header.Get("Content-Type"))
26
+
27
+ w.WriteHeader(http.StatusOK)
28
+ }))
29
+ defer server.Close()
30
+
31
+ // Create a client with the mock server
32
+ om, err := NewClientWithResponses(server.URL)
33
+ assert.NoError(t, err)
34
+
35
+ resp, err := om.IngestEventWithResponse(ctx, mockEvent())
36
+ assert.NoError(t, err)
37
+
38
+ assert.Equal(t, http.StatusOK, resp.StatusCode())
39
+ }
40
+
41
+ func TestIngestBatch(t *testing.T) {
42
+ ctx := context.Background()
43
+
44
+ // Create a mock server to test the client
45
+ server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
46
+ assert.Equal(t, http.MethodPost, r.Method)
47
+ assert.Equal(t, "/api/v1/events", r.URL.Path)
48
+ assert.Equal(t, "application/cloudevents-batch+json", r.Header.Get("Content-Type"))
49
+
50
+ w.WriteHeader(http.StatusOK)
51
+ }))
52
+ defer server.Close()
53
+
54
+ // Create a client with the mock server
55
+ om, err := NewClientWithResponses(server.URL)
56
+ assert.NoError(t, err)
57
+
58
+ resp, err := om.IngestEventBatchWithResponse(ctx, []cloudevents.Event{mockEvent()})
59
+ assert.NoError(t, err)
60
+
61
+ assert.Equal(t, http.StatusOK, resp.StatusCode())
62
+ }
63
+
64
+ func TetsListEvents(t *testing.T) {
65
+ ctx := context.Background()
66
+
67
+ event := mockEvent()
68
+
69
+ // Create a mock server to test the client
70
+ server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
71
+ assert.Equal(t, http.MethodGet, r.Method)
72
+ assert.Equal(t, "/api/v1/events", r.URL.Path)
73
+
74
+ w.Header().Set("Content-Type", "application/json")
75
+ w.WriteHeader(http.StatusOK)
76
+
77
+ err := json.NewEncoder(w).Encode([]Event{event})
78
+ assert.NoError(t, err)
79
+ }))
80
+ defer server.Close()
81
+
82
+ // Create a client with the mock server
83
+ om, err := NewClientWithResponses(server.URL)
84
+ assert.NoError(t, err)
85
+
86
+ resp, err := om.ListEventsWithResponse(ctx, &ListEventsParams{})
87
+ assert.NoError(t, err)
88
+
89
+ assert.Equal(t, http.StatusOK, resp.StatusCode())
90
+ assert.Equal(t, lo.ToPtr([]Event{event}), resp.JSON200)
91
+ }
92
+
93
+ func TestAuth(t *testing.T) {
94
+ ctx := context.Background()
95
+
96
+ // Create a mock server to test the client
97
+ server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
98
+ assert.Equal(t, http.MethodPost, r.Method)
99
+ assert.Equal(t, "/api/v1/events", r.URL.Path)
100
+ assert.Equal(t, "application/cloudevents+json", r.Header.Get("Content-Type"))
101
+ assert.Equal(t, "Bearer test-api-token", r.Header.Get("Authorization"))
102
+
103
+ w.WriteHeader(http.StatusOK)
104
+ }))
105
+ defer server.Close()
106
+
107
+ // Create a client with the mock server
108
+ om, err := NewAuthClientWithResponses(server.URL, "test-api-token")
109
+ assert.NoError(t, err)
110
+
111
+ resp, err := om.IngestEventWithResponse(ctx, mockEvent())
112
+ assert.NoError(t, err)
113
+
114
+ assert.Equal(t, http.StatusOK, resp.StatusCode())
115
+ }
116
+
117
+ func TestGetMeter(t *testing.T) {
118
+ ctx := context.Background()
119
+
120
+ meter := Meter{
121
+ Slug: "meter-1",
122
+ Description: lo.ToPtr("Test Meter"),
123
+ Aggregation: MeterAggregation(meter.MeterAggregationSum),
124
+ ValueProperty: lo.ToPtr("$.tokens"),
125
+ GroupBy: lo.ToPtr(map[string]string{"model": "$.model", "type": "$.type"}),
126
+ }
127
+
128
+ // Create a mock server to test the client
129
+ server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
130
+ assert.Equal(t, http.MethodGet, r.Method)
131
+ assert.Equal(t, "/api/v1/meters/meter-1", r.URL.Path)
132
+
133
+ w.Header().Set("Content-Type", "application/json")
134
+ w.WriteHeader(http.StatusOK)
135
+
136
+ err := json.NewEncoder(w).Encode(meter)
137
+ assert.NoError(t, err)
138
+ }))
139
+ defer server.Close()
140
+
141
+ // Create a client with the mock server
142
+ om, err := NewClientWithResponses(server.URL)
143
+ assert.NoError(t, err)
144
+
145
+ resp, err := om.GetMeterWithResponse(ctx, "meter-1")
146
+ assert.NoError(t, err)
147
+
148
+ assert.Equal(t, http.StatusOK, resp.StatusCode())
149
+ assert.Equal(t, &meter, resp.JSON200)
150
+ }
151
+
152
+ func TestListMeters(t *testing.T) {
153
+ ctx := context.Background()
154
+
155
+ meters := []Meter{
156
+ {
157
+ Slug: "meter-1",
158
+ Description: lo.ToPtr("Test Meter"),
159
+ Aggregation: MeterAggregation(meter.MeterAggregationSum),
160
+ ValueProperty: lo.ToPtr("$.tokens"),
161
+ GroupBy: lo.ToPtr(map[string]string{"model": "$.model", "type": "$.type"}),
162
+ },
163
+ {
164
+ Slug: "meter-2",
165
+ Description: lo.ToPtr("Test Meter 2"),
166
+ Aggregation: MeterAggregation(meter.MeterAggregationSum),
167
+ ValueProperty: lo.ToPtr("$.tokens"),
168
+ GroupBy: lo.ToPtr(map[string]string{"model": "$.model", "type": "$.type"}),
169
+ },
170
+ }
171
+
172
+ // Create a mock server to test the client
173
+ server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
174
+ assert.Equal(t, http.MethodGet, r.Method)
175
+ assert.Equal(t, "/api/v1/meters", r.URL.Path)
176
+
177
+ w.Header().Set("Content-Type", "application/json")
178
+ w.WriteHeader(http.StatusOK)
179
+
180
+ err := json.NewEncoder(w).Encode(meters)
181
+ assert.NoError(t, err)
182
+ }))
183
+ defer server.Close()
184
+
185
+ // Create a client with the mock server
186
+ om, err := NewClientWithResponses(server.URL)
187
+ assert.NoError(t, err)
188
+
189
+ resp, err := om.ListMetersWithResponse(ctx, &ListMetersParams{})
190
+ assert.NoError(t, err)
191
+
192
+ assert.Equal(t, http.StatusOK, resp.StatusCode())
193
+ assert.Equal(t, &meters, resp.JSON200)
194
+ }
195
+
196
+ func TestMeterQuery(t *testing.T) {
197
+ ctx := context.Background()
198
+
199
+ result := MeterQueryResult{
200
+ Data: []MeterQueryRow{
201
+ {
202
+ Subject: lo.ToPtr("customer-1"),
203
+ WindowStart: time.Now().UTC(),
204
+ WindowEnd: time.Now().UTC(),
205
+ Value: 123,
206
+ },
207
+ },
208
+ }
209
+
210
+ // Create a mock server to test the client
211
+ server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
212
+ assert.Equal(t, http.MethodGet, r.Method)
213
+ assert.Equal(t, "/api/v1/meters/meter-1/query", r.URL.Path)
214
+
215
+ w.Header().Set("Content-Type", "application/json")
216
+ w.WriteHeader(http.StatusOK)
217
+
218
+ err := json.NewEncoder(w).Encode(result)
219
+ assert.NoError(t, err)
220
+ }))
221
+ defer server.Close()
222
+
223
+ // Create a client with the mock server
224
+ om, err := NewClientWithResponses(server.URL)
225
+ assert.NoError(t, err)
226
+
227
+ subjectFilter := []string{"customer-1"}
228
+
229
+ resp, err := om.QueryMeterWithResponse(ctx, "meter-1", &QueryMeterParams{
230
+ Subject: &subjectFilter,
231
+ })
232
+ assert.NoError(t, err)
233
+
234
+ assert.Equal(t, http.StatusOK, resp.StatusCode())
235
+ assert.Equal(t, &result, resp.JSON200)
236
+ }
237
+
238
+ func TestListSubjects(t *testing.T) {
239
+ ctx := context.Background()
240
+
241
+ subjects := []Subject{
242
+ {
243
+ Key: "customer-1",
244
+ DisplayName: lo.ToPtr("Customer 1"),
245
+ },
246
+ {
247
+ Key: "customer-2",
248
+ DisplayName: lo.ToPtr("Customer 2"),
249
+ },
250
+ }
251
+
252
+ // Create a mock server to test the client
253
+ server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
254
+ assert.Equal(t, http.MethodGet, r.Method)
255
+ assert.Equal(t, "/api/v1/subjects", r.URL.Path)
256
+
257
+ w.Header().Set("Content-Type", "application/json")
258
+ w.WriteHeader(http.StatusOK)
259
+
260
+ err := json.NewEncoder(w).Encode(subjects)
261
+ assert.NoError(t, err)
262
+ }))
263
+ defer server.Close()
264
+
265
+ // Create a client with the mock server
266
+ om, err := NewClientWithResponses(server.URL)
267
+ assert.NoError(t, err)
268
+
269
+ resp, err := om.ListSubjectsWithResponse(ctx)
270
+ assert.NoError(t, err)
271
+
272
+ assert.Equal(t, http.StatusOK, resp.StatusCode())
273
+ assert.Equal(t, &subjects, resp.JSON200)
274
+ }
275
+
276
+ func TestUpsertSubject(t *testing.T) {
277
+ ctx := context.Background()
278
+
279
+ subject := Subject{
280
+ Key: "customer-1",
281
+ DisplayName: lo.ToPtr("Customer 1"),
282
+ }
283
+
284
+ // Create a mock server to test the client
285
+ server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
286
+ assert.Equal(t, http.MethodPost, r.Method)
287
+ assert.Equal(t, "/api/v1/subjects", r.URL.Path)
288
+
289
+ w.Header().Set("Content-Type", "application/json")
290
+ w.WriteHeader(http.StatusOK)
291
+
292
+ err := json.NewEncoder(w).Encode([]Subject{subject})
293
+ assert.NoError(t, err)
294
+ }))
295
+ defer server.Close()
296
+
297
+ // Create a client with the mock server
298
+ om, err := NewClientWithResponses(server.URL)
299
+ assert.NoError(t, err)
300
+
301
+ resp, err := om.UpsertSubjectWithResponse(ctx, []SubjectUpsert{
302
+ {
303
+ Key: "customer-1",
304
+ DisplayName: lo.ToPtr("Customer 1"),
305
+ },
306
+ })
307
+ assert.NoError(t, err)
308
+
309
+ assert.Equal(t, http.StatusOK, resp.StatusCode())
310
+ assert.Equal(t, lo.ToPtr([]Subject{subject}), resp.JSON200)
311
+ }
312
+
313
+ // mockEvent creates a mock CloudEvent for testing
314
+ func mockEvent() cloudevents.Event {
315
+ e := cloudevents.New()
316
+ eventTime, _ := time.Parse(time.RFC3339, "2024-11-05T22:35:52.457Z")
317
+ e.SetTime(eventTime)
318
+ e.SetID("ec2672e8-458d-4c5e-8a3c-f3235dd38ba5")
319
+ e.SetSource("my-app")
320
+ e.SetType("usage-reports")
321
+ e.SetSubject("customer-1")
322
+ _ = e.SetData("application/json", map[string]string{
323
+ "reports": "123",
324
+ "type": "type",
325
+ })
326
+ return e
327
+ }
api/client/go/codegen.yaml ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # yaml-language-server: $schema=https://raw.githubusercontent.com/oapi-codegen/oapi-codegen/HEAD/configuration-schema.json
2
+ package: openmeter
3
+ generate:
4
+ client: true
5
+ models: true
6
+ embedded-spec: true
7
+ compatibility:
8
+ # See: https://github.com/oapi-codegen/oapi-codegen/issues/778
9
+ disable-required-readonly-as-pointer: true
10
+ always-prefix-enum-values: true
11
+ output: ./client.gen.go
api/client/go/error.go ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package openmeter
2
+
3
+ // ErrResponse renderer type for handling all sorts of errors.
4
+ // In the best case scenario, the excellent github.com/pkg/errors package
5
+ // helps reveal information on the error, setting it on Err, and in the Render()
6
+ // method, using it to set the application-specific error code in AppCode.
7
+ type ErrResponse struct {
8
+ Err error `json:"-"` // low-level runtime error
9
+
10
+ StatusCode int `json:"statusCode"` // http response status code
11
+ StatusText string `json:"status"` // user-level status message
12
+ AppCode int64 `json:"code,omitempty"` // application-specific error code
13
+ Message string `json:"message,omitempty"` // application-level error message, for debugging
14
+ }
api/client/javascript/.gitignore ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # dot-files (.env, .git, ...)
2
+
3
+ # logs
4
+ logs
5
+ *.log
6
+ npm-debug.log*
7
+
8
+ # dependency directory
9
+ # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git
10
+ node_modules
11
+
12
+ # editor
13
+ .vscode/*
14
+ !.vscode/settings.json
15
+ !.vscode/tasks.json
16
+ !.vscode/launch.json
17
+ !.vscode/extensions.json
18
+ !.vscode/*.code-snippets
19
+
20
+ # build
21
+ .duel-cache/
22
+ tsconfig.tsbuildinfo
23
+ dist/
api/client/javascript/.npmignore ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # dot-files (.env, .git, .npmrc, ...)
2
+ .*
3
+
4
+ # logs
5
+ logs
6
+ *.log
7
+ npm-debug.log*
8
+
9
+ # dependency directory
10
+ # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git
11
+ node_modules
12
+
13
+ # editor
14
+ .vscode/*
15
+ !.vscode/settings.json
16
+ !.vscode/tasks.json
17
+ !.vscode/launch.json
18
+ !.vscode/extensions.json
19
+ !.vscode/*.code-snippets
20
+
21
+ # source
22
+ index.ts
23
+ src/
24
+ scripts/
25
+ vitest.config.ts
26
+ eslint.config.js
27
+ tsconfig*.json
api/client/javascript/.npmrc ADDED
@@ -0,0 +1 @@
 
 
1
+ save-exact=true
api/client/javascript/Makefile ADDED
@@ -0,0 +1,42 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # A Self-Documenting Makefile: http://marmelab.com/blog/2016/02/29/auto-documented-makefile.html
2
+
3
+ .PHONY: generate
4
+ generate: ## Generate JavaScript SDK
5
+ $(call print-target)
6
+ pnpm --frozen-lockfile install
7
+ pnpm run generate
8
+ pnpm build
9
+ pnpm test
10
+
11
+ .PHONY: publish-javascript-sdk
12
+ publish-javascript-sdk: ## Publish JavaScript SDK
13
+ $(call print-target)
14
+ @if [ -z "$$JS_SDK_RELEASE_VERSION" ]; then \
15
+ echo "ERROR: JS_SDK_RELEASE_VERSION is required"; \
16
+ echo "Usage: JS_SDK_RELEASE_VERSION=1.2.3 make publish-javascript-sdk [JS_SDK_RELEASE_TAG=beta]"; \
17
+ exit 1; \
18
+ fi
19
+
20
+ @if [ -z "$$JS_SDK_RELEASE_TAG" ]; then \
21
+ echo "ERROR: JS_SDK_RELEASE_TAG is required"; \
22
+ echo "Usage: JS_SDK_RELEASE_VERSION=1.2.3 make publish-javascript-sdk [JS_SDK_RELEASE_TAG=beta]"; \
23
+ exit 1; \
24
+ fi
25
+
26
+ pnpm --frozen-lockfile install
27
+ pnpm version "$${JS_SDK_RELEASE_VERSION}" --no-git-tag-version
28
+ CACHE_BUSTER="$$(date --rfc-3339=seconds)" pnpm publish --no-git-checks --tag "$${JS_SDK_RELEASE_TAG}"
29
+ @echo "✅ Published $${JS_SDK_RELEASE_TAG} JavaScript SDK version $${JS_SDK_RELEASE_VERSION} with tag $${JS_SDK_RELEASE_TAG}"
30
+
31
+ .PHONY: help
32
+ .DEFAULT_GOAL := help
33
+ help:
34
+ @grep -h -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}'
35
+
36
+ # Variable outputting/exporting rules
37
+ var-%: ; @echo $($*)
38
+ varexport-%: ; @echo $*=$($*)
39
+
40
+ define print-target
41
+ @printf "Executing target: \033[36m$@\033[0m\n"
42
+ endef
api/client/javascript/README.md ADDED
@@ -0,0 +1,267 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # OpenMeter JavaScript SDK
2
+
3
+ ## Install
4
+
5
+ ```sh
6
+ npm install --save @openmeter/sdk
7
+ ```
8
+
9
+ ## Configuration for accessing the OpenMeter API
10
+
11
+ To use the OpenMeter SDK on your backend, you need to configure `baseUrl` and `apiKey` for OpenMeter Cloud:
12
+
13
+ ```ts
14
+ import { OpenMeter } from '@openmeter/sdk'
15
+
16
+ const openmeter = new OpenMeter({
17
+ baseUrl: 'https://openmeter.cloud',
18
+ apiKey: 'om_...',
19
+ })
20
+ ```
21
+
22
+ ## Configuration for accessing the OpenMeter Portal API
23
+
24
+ To use the OpenMeter Portal SDK on your frontend, you need to configure it use a portal token in your configuration:
25
+
26
+ ```ts
27
+ import { OpenMeter } from '@openmeter/sdk/portal'
28
+
29
+ const openmeter = new OpenMeter({
30
+ baseUrl: 'https://openmeter.cloud',
31
+ portalToken: 'om_portal_...',
32
+ })
33
+ ```
34
+
35
+ ## Configuration for accessing the OpenMeter React SDK
36
+
37
+ To use the OpenMeter React SDK for the portal API, you need to configure a Portal Client and a React Context:
38
+
39
+ ```ts
40
+ import {
41
+ OpenMeter,
42
+ OpenMeterProvider,
43
+ useOpenMeter,
44
+ } from '@openmeter/sdk/react'
45
+
46
+ function App() {
47
+ // get portal token from your backend
48
+ const openmeter = new OpenMeter({
49
+ baseUrl: 'https://openmeter.cloud',
50
+ portalToken,
51
+ })
52
+
53
+ return (
54
+ <OpenMeterProvider value={openmeter}>
55
+ <UsageComponent />
56
+ {/* ... */}
57
+ </OpenMeterProvider>
58
+ )
59
+ }
60
+
61
+ function UsageComponent() {
62
+ // get openmeter client from context
63
+ const openmeter = useOpenMeter()
64
+
65
+ // ...
66
+ }
67
+ ```
68
+
69
+ ## Ingest usage events
70
+
71
+ ```ts
72
+ // Ingest a single AI token usage event
73
+ await openmeter.events.ingest({
74
+ type: 'ai-tokens',
75
+ subject: 'customer-acme-corp',
76
+ id: 'evt_01234567', // optional: auto-generated if not provided
77
+ source: 'llm-api-gateway', // optional: defaults to '@openmeter/sdk'
78
+ time: new Date(), // optional: defaults to current time
79
+ data: {
80
+ model: 'gpt-4',
81
+ type: 'input',
82
+ tokens: 1250,
83
+ },
84
+ })
85
+
86
+ // Ingest multiple events in a batch for better performance
87
+ await openmeter.events.ingest([
88
+ {
89
+ type: 'ai-tokens',
90
+ subject: 'customer-acme-corp',
91
+ data: { model: 'gpt-4', type: 'input', tokens: 850 },
92
+ },
93
+ {
94
+ type: 'ai-tokens',
95
+ subject: 'customer-acme-corp',
96
+ data: { model: 'gpt-4', type: 'output', tokens: 850 },
97
+ },
98
+ ])
99
+ ```
100
+
101
+ ## Client API Reference
102
+
103
+ The OpenMeter SDK provides a comprehensive client interface organized into logical groups. Below is a complete reference of all available methods.
104
+
105
+ ### Overview
106
+
107
+ | Namespace | Resource | Operation | Method | Description |
108
+ |-----------|----------|-----------|--------|-------------|
109
+ | **[Events](#events)** | | | | Track usage by ingesting events |
110
+ | | Events | Create | [`openmeter.events.ingest(events)`](./src/client/events.ts#L19) | Ingest a single event or batch of events |
111
+ | | Events | Read | [`openmeter.events.list(params?)`](./src/client/events.ts#L41) | List ingested events with advanced filtering |
112
+ | | Events | Read | [`openmeter.events.listV2(params?)`](./src/client/events.ts#L60) | List ingested events with advanced filtering (V2) |
113
+ | **[Meters](#meters)** | | | | Track and aggregate usage data from events |
114
+ | | Meters | Create | [`openmeter.meters.create(meter)`](./src/client/meters.ts#L19) | Create a new meter |
115
+ | | Meters | Read | [`openmeter.meters.get(idOrSlug)`](./src/client/meters.ts#L34) | Get a meter by ID or slug |
116
+ | | Meters | Read | [`openmeter.meters.list()`](./src/client/meters.ts#L55) | List all meters |
117
+ | | Meters | Read | [`openmeter.meters.query(idOrSlug, query?)`](./src/client/meters.ts#L70) | Query usage data |
118
+ | | Meters | Update | [`openmeter.meters.update(idOrSlug, meter)`](./src/client/meters.ts#L100) | Update a meter by ID or slug |
119
+ | | Meters | Delete | [`openmeter.meters.delete(idOrSlug)`](./src/client/meters.ts#L124) | Delete a meter by ID or slug |
120
+ | **[Subjects](#subjects)** | | | | Manage entities that consume resources |
121
+ | | Subjects | Create | [`openmeter.subjects.upsert(subjects)`](./src/client/subjects.ts#L21) | Create or update one or multiple subjects |
122
+ | | Subjects | Read | [`openmeter.subjects.get(idOrKey)`](./src/client/subjects.ts#L39) | Get a subject by ID or key |
123
+ | | Subjects | Read | [`openmeter.subjects.list()`](./src/client/subjects.ts#L60) | List all subjects |
124
+ | | Subjects | Delete | [`openmeter.subjects.delete(idOrKey)`](./src/client/subjects.ts#L74) | Delete a subject by ID or key |
125
+ | **[Customers](#customers)** | | | | Manage customer subscription lifecycles and plan assignments |
126
+ | | Customers | Create | [`openmeter.customers.create(customer)`](./src/client/customers.ts#L37) | Create a new customer |
127
+ | | Customers | Read | [`openmeter.customers.get(customerIdOrKey)`](./src/client/customers.ts#L52) | Get a customer by ID or key |
128
+ | | Customers | Read | [`openmeter.customers.list(query?)`](./src/client/customers.ts#L123) | List all customers |
129
+ | | Customers | Read | [`openmeter.customers.getAccess(customerIdOrKey)`](./src/client/customers.ts#L143) | Get customer access information |
130
+ | | Customers | Read | [`openmeter.customers.listSubscriptions(customerIdOrKey, query?)`](./src/client/customers.ts#L169) | List customer subscriptions |
131
+ | | Customers | Update | [`openmeter.customers.update(customerIdOrKey, customer)`](./src/client/customers.ts#L75) | Update a customer |
132
+ | | Customers | Delete | [`openmeter.customers.delete(customerIdOrKey)`](./src/client/customers.ts#L99) | Delete a customer |
133
+ | | Apps | Update | [`openmeter.customers.apps.upsert(customerIdOrKey, appData)`](./src/client/customers.ts#L200) | Upsert app data |
134
+ | | Apps | Read | [`openmeter.customers.apps.list(customerIdOrKey)`](./src/client/customers.ts#L228) | List app data |
135
+ | | Apps | Delete | [`openmeter.customers.apps.delete(customerIdOrKey, appId)`](./src/client/customers.ts#L254) | Delete app data |
136
+ | | Stripe | Update | [`openmeter.customers.stripe.upsert(customerIdOrKey, appDataBase)`](./src/client/customers.ts#L285) | Upsert Stripe app data |
137
+ | | Stripe | Read | [`openmeter.customers.stripe.get(customerIdOrKey)`](./src/client/customers.ts#L313) | Get Stripe app data |
138
+ | | Stripe | Create | [`openmeter.customers.stripe.createPortalSession(customerIdOrKey, params)`](./src/client/customers.ts#L337) | Create a Stripe customer portal session |
139
+ | | Entitlements V1 | Read | [`openmeter.customers.entitlementsV1.value(customerIdOrKey, featureKey)`](./src/client/customers.ts#L372) | Get entitlement value (V1 API) |
140
+ | | Entitlements | Read | [`openmeter.customers.entitlements.list(customerIdOrKey)`](./src/client/customers.ts#L401) | List entitlements |
141
+ | | Entitlements | Create | [`openmeter.customers.entitlements.create(customerIdOrKey, entitlement)`](./src/client/customers.ts#L428) | Create an entitlement |
142
+ | | Entitlements | Read | [`openmeter.customers.entitlements.get(customerIdOrKey, featureKeyOrId)`](./src/client/customers.ts#L454) | Get an entitlement |
143
+ | | Entitlements | Delete | [`openmeter.customers.entitlements.delete(customerIdOrKey, entitlementId)`](./src/client/customers.ts#L479) | Delete an entitlement |
144
+ | | Entitlements | Update | [`openmeter.customers.entitlements.override(customerIdOrKey, featureKeyOrId, entitlement)`](./src/client/customers.ts#L505) | Override an entitlement |
145
+ | | Entitlements | Read | [`openmeter.customers.entitlements.value(customerIdOrKey, featureKeyOrId, query?)`](./src/client/customers.ts#L588) | Get entitlement value |
146
+ | | Entitlements | Read | [`openmeter.customers.entitlements.history(customerIdOrKey, featureKeyOrId, query?)`](./src/client/customers.ts#L617) | Get entitlement history |
147
+ | | Entitlements | Update | [`openmeter.customers.entitlements.resetUsage(customerIdOrKey, entitlementId, body?)`](./src/client/customers.ts#L653) | Reset usage |
148
+ | | Entitlements | Read | [`openmeter.customers.entitlements.listGrants(customerIdOrKey, featureKeyOrId, query?)`](./src/client/customers.ts#L532) | List grants |
149
+ | | Entitlements | Create | [`openmeter.customers.entitlements.createGrant(customerIdOrKey, featureKeyOrId, grant)`](./src/client/customers.ts#L561) | Create a grant |
150
+ | **[Features](#features)** | | | | Define application capabilities and services |
151
+ | | Features | Create | [`openmeter.features.create(feature)`](./src/client/features.ts#L24) | Create a new feature |
152
+ | | Features | Read | [`openmeter.features.get(featureIdOrKey)`](./src/client/features.ts#L39) | Get a feature by ID |
153
+ | | Features | Read | [`openmeter.features.list(params?)`](./src/client/features.ts#L61) | List all features |
154
+ | | Features | Delete | [`openmeter.features.delete(featureIdOrKey)`](./src/client/features.ts#L84) | Delete a feature by ID |
155
+ | **[Entitlements (V1)](#entitlements-v1)** | | | | Subject-based usage limits and access controls |
156
+ | | Entitlements | Create | [`openmeter.entitlementsV1.create(subjectIdOrKey, entitlement)`](./src/client/entitlements.ts#L40) | Create an entitlement for a subject |
157
+ | | Entitlements | Read | [`openmeter.entitlementsV1.get(entitlementId)`](./src/client/entitlements.ts#L68) | Get an entitlement by ID |
158
+ | | Entitlements | Read | [`openmeter.entitlementsV1.list(query?)`](./src/client/entitlements.ts#L91) | List all entitlements |
159
+ | | Entitlements | Read | [`openmeter.entitlementsV1.value(subjectIdOrKey, featureIdOrKey, query?)`](./src/client/entitlements.ts#L147) | Get the value of an entitlement |
160
+ | | Entitlements | Read | [`openmeter.entitlementsV1.history(subjectIdOrKey, entitlementIdOrFeatureKey, query?)`](./src/client/entitlements.ts#L180) | Get the history of an entitlement |
161
+ | | Entitlements | Update | [`openmeter.entitlementsV1.override(subjectIdOrKey, entitlementIdOrFeatureKey, override)`](./src/client/entitlements.ts#L213) | Override an entitlement |
162
+ | | Entitlements | Update | [`openmeter.entitlementsV1.reset(subjectIdOrKey, entitlementIdOrFeatureKey, reset?)`](./src/client/entitlements.ts#L247) | Reset entitlement usage |
163
+ | | Entitlements | Delete | [`openmeter.entitlementsV1.delete(subjectIdOrKey, entitlementId)`](./src/client/entitlements.ts#L116) | Delete an entitlement |
164
+ | | Grants | Create | [`openmeter.entitlementsV1.grants.create(subjectIdOrKey, entitlementIdOrFeatureKey, grant)`](./src/client/entitlements.ts#L283) | Create a grant for an entitlement |
165
+ | | Grants | Read | [`openmeter.entitlementsV1.grants.list(subjectIdOrKey, entitlementIdOrFeatureKey, query?)`](./src/client/entitlements.ts#L314) | List grants for an entitlement |
166
+ | | Grants | Read | [`openmeter.entitlementsV1.grants.listAll(query?)`](./src/client/entitlements.ts#L345) | List all grants |
167
+ | | Grants | Delete | [`openmeter.entitlementsV1.grants.void(entitlementId, grantId)`](./src/client/entitlements.ts#L369) | Void a grant |
168
+ | **[Entitlements](#entitlements)** | | | | Customer-based entitlements and access controls |
169
+ | | Entitlements | Read | [`openmeter.entitlements.list(query?)`](./src/client/entitlements.ts#L404) | List all entitlements (admin purposes) |
170
+ | | Entitlements | Read | [`openmeter.entitlements.get(entitlementId)`](./src/client/entitlements.ts#L425) | Get an entitlement by ID |
171
+ | | Grants | Read | [`openmeter.entitlements.grants.list(query?)`](./src/client/entitlements.ts#L453) | List all grants (admin purposes) |
172
+ | | Grants | Delete | [`openmeter.entitlements.grants.void(grantId)`](./src/client/entitlements.ts#L478) | Void a grant |
173
+ | **[Plans](#plans)** | | | | Manage subscription plans and pricing|
174
+ | | Plans | Create | [`openmeter.plans.create(plan)`](./src/client/plans.ts#L28) | Create a new plan|
175
+ | | Plans | Read | [`openmeter.plans.get(planId)`](./src/client/plans.ts#L44) | Get a plan by ID|
176
+ | | Plans | Read | [`openmeter.plans.list(query?)`](./src/client/plans.ts#L66) | List all plans|
177
+ | | Plans | Update | [`openmeter.plans.update(planId, plan)`](./src/client/plans.ts#L85) | Update a plan|
178
+ | | Plans | Delete | [`openmeter.plans.delete(planId)`](./src/client/plans.ts#L105) | Delete a plan by ID|
179
+ | | Plans | Other | [`openmeter.plans.archive(planId)`](./src/client/plans.ts#L123) | Archive a plan|
180
+ | | Plans | Other | [`openmeter.plans.publish(planId)`](./src/client/plans.ts#L141) | Publish a plan|
181
+ | | Addons | Read | [`openmeter.plans.addons.list(planId)`](./src/client/plans.ts#L168) | List addons|
182
+ | | Addons | Create | [`openmeter.plans.addons.create(planId, addon)`](./src/client/plans.ts#L191) | Create an addon|
183
+ | | Addons | Read | [`openmeter.plans.addons.get(planId, planAddonId)`](./src/client/plans.ts#L212) | Get an addon by ID|
184
+ | | Addons | Update | [`openmeter.plans.addons.update(planId, planAddonId, addon)`](./src/client/plans.ts#L238) | Update an addon|
185
+ | | Addons | Delete | [`openmeter.plans.addons.delete(planId, planAddonId)`](./src/client/plans.ts#L263) | Delete an addon by ID|
186
+ | **[Addons](#addons)** | | | | Manage standalone addons available across plans|
187
+ | | Addons | Create | [`openmeter.addons.create(addon)`](./src/client/addons.ts#L15) | Create a new addon|
188
+ | | Addons | Read | [`openmeter.addons.get(addonId)`](./src/client/addons.ts#L48) | Get an addon by ID|
189
+ | | Addons | Read | [`openmeter.addons.list(query?)`](./src/client/addons.ts#L30) | List all addons|
190
+ | | Addons | Update | [`openmeter.addons.update(addonId, addon)`](./src/client/addons.ts#L64) | Update an addon|
191
+ | | Addons | Delete | [`openmeter.addons.delete(addonId)`](./src/client/addons.ts#L84) | Delete an addon by ID|
192
+ | | Addons | Other | [`openmeter.addons.publish(addonId)`](./src/client/addons.ts#L99) | Publish an addon|
193
+ | | Addons | Other | [`openmeter.addons.archive(addonId)`](./src/client/addons.ts#L114) | Archive an addon|
194
+ | **[Subscriptions](#subscriptions)** | | | | Manage customer subscriptions|
195
+ | | Subscriptions | Create | [`openmeter.subscriptions.create(body)`](./src/client/subscriptions.ts#L24) | Create a new subscription|
196
+ | | Subscriptions | Read | [`openmeter.subscriptions.get(subscriptionId)`](./src/client/subscriptions.ts#L39) | Get a subscription by ID|
197
+ | | Subscriptions | Update | [`openmeter.subscriptions.edit(subscriptionId, body)`](./src/client/subscriptions.ts#L61) | Edit a subscription|
198
+ | | Subscriptions | Delete | [`openmeter.subscriptions.delete(subscriptionId)`](./src/client/subscriptions.ts#L180) | Delete a subscription (only scheduled)|
199
+ | | Subscriptions | Other | [`openmeter.subscriptions.cancel(subscriptionId, body?)`](./src/client/subscriptions.ts#L85) | Cancel a subscription|
200
+ | | Subscriptions | Other | [`openmeter.subscriptions.change(subscriptionId, body)`](./src/client/subscriptions.ts#L110) | Change a subscription (upgrade/downgrade)|
201
+ | | Subscriptions | Other | [`openmeter.subscriptions.migrate(subscriptionId, body)`](./src/client/subscriptions.ts#L135) | Migrate to a new plan version|
202
+ | | Subscriptions | Other | [`openmeter.subscriptions.unscheduleCancelation(subscriptionId)`](./src/client/subscriptions.ts#L158) | Unschedule a subscription cancelation|
203
+ | **[Subscription Addons](#subscription-addons)** | | | | Manage addons attached to specific subscriptions|
204
+ | | Subscription Addons | Create | [`openmeter.subscriptionAddons.create(subscriptionId, body)`](./src/client/subscription-addons.ts#L16) | Create a new subscription addon|
205
+ | | Subscription Addons | Read | [`openmeter.subscriptionAddons.get(subscriptionId, subscriptionAddonId)`](./src/client/subscription-addons.ts#L58) | Get a subscription addon by ID|
206
+ | | Subscription Addons | Read | [`openmeter.subscriptionAddons.list(subscriptionId)`](./src/client/subscription-addons.ts#L39) | List all addons of a subscription|
207
+ | | Subscription Addons | Update | [`openmeter.subscriptionAddons.update(subscriptionId, subscriptionAddonId, body)`](./src/client/subscription-addons.ts#L82) | Update a subscription addon|
208
+ | **[Billing](#billing)** | | | | Comprehensive billing management (profiles, invoices, overrides)|
209
+ | | Profiles | Create | [`openmeter.billing.profiles.create(profile)`](./src/client/billing.ts#L42) | Create a billing profile|
210
+ | | Profiles | Read | [`openmeter.billing.profiles.get(id)`](./src/client/billing.ts#L60) | Get a billing profile by ID|
211
+ | | Profiles | Read | [`openmeter.billing.profiles.list(query?)`](./src/client/billing.ts#L80) | List billing profiles|
212
+ | | Profiles | Update | [`openmeter.billing.profiles.update(id, profile)`](./src/client/billing.ts#L101) | Update a billing profile|
213
+ | | Profiles | Delete | [`openmeter.billing.profiles.delete(id)`](./src/client/billing.ts#L123) | Delete a billing profile|
214
+ | | Invoices | Read | [`openmeter.billing.invoices.list(query?)`](./src/client/billing.ts#L150) | List invoices|
215
+ | | Invoices | Read | [`openmeter.billing.invoices.get(id, query?)`](./src/client/billing.ts#L170) | Get an invoice by ID|
216
+ | | Invoices | Update | [`openmeter.billing.invoices.update(id, invoice)`](./src/client/billing.ts#L192) | Update an invoice (draft or earlier)|
217
+ | | Invoices | Delete | [`openmeter.billing.invoices.delete(id)`](./src/client/billing.ts#L213) | Delete an invoice (draft or earlier)|
218
+ | | Invoices | Other | [`openmeter.billing.invoices.advance(id)`](./src/client/billing.ts#L235) | Advance invoice to next status|
219
+ | | Invoices | Other | [`openmeter.billing.invoices.approve(id)`](./src/client/billing.ts#L257) | Approve an invoice (sends to customer)|
220
+ | | Invoices | Other | [`openmeter.billing.invoices.retry(id, body?)`](./src/client/billing.ts#L278) | Retry advancing after failure|
221
+ | | Invoices | Other | [`openmeter.billing.invoices.void(id)`](./src/client/billing.ts#L302) | Void an invoice|
222
+ | | Invoices | Other | [`openmeter.billing.invoices.recalculateTax(id)`](./src/client/billing.ts#L325) | Recalculate invoice tax amounts|
223
+ | | Invoices | Other | [`openmeter.billing.invoices.simulate(customerId, query?)`](./src/client/billing.ts#L346) | Simulate an invoice for a customer|
224
+ | | Invoices | Create | [`openmeter.billing.invoices.createLineItems(customerId, body)`](./src/client/billing.ts#L377) | Create pending line items|
225
+ | | Invoices | Create | [`openmeter.billing.invoices.invoicePendingLines(customerId)`](./src/client/billing.ts#L401) | Invoice pending lines|
226
+ | | Customers | Create | [`openmeter.billing.customers.createOverride(customerId, body)`](./src/client/billing.ts#L427) | Create or update a customer override|
227
+ | | Customers | Read | [`openmeter.billing.customers.getOverride(customerId, id)`](./src/client/billing.ts#L450) | Get a customer override|
228
+ | | Customers | Read | [`openmeter.billing.customers.listOverrides(customerId)`](./src/client/billing.ts#L471) | List customer overrides|
229
+ | | Customers | Delete | [`openmeter.billing.customers.deleteOverride(customerId, id)`](./src/client/billing.ts#L489) | Delete a customer override|
230
+ | **[Apps](#apps)** | | | | Manage integrations and app marketplace|
231
+ | | Apps | Read | [`openmeter.apps.list(query?)`](./src/client/apps.ts#L32) | List installed apps|
232
+ | | Apps | Read | [`openmeter.apps.get(id)`](./src/client/apps.ts#L50) | Get an app by ID|
233
+ | | Apps | Update | [`openmeter.apps.update(id, body)`](./src/client/apps.ts#L69) | Update an app|
234
+ | | Apps | Delete | [`openmeter.apps.uninstall(id)`](./src/client/apps.ts#L89) | Uninstall an app|
235
+ | | Marketplace | Read | [`openmeter.apps.marketplace.list(query?)`](./src/client/apps.ts#L115) | List available marketplace apps|
236
+ | | Marketplace | Read | [`openmeter.apps.marketplace.get(id)`](./src/client/apps.ts#L133) | Get marketplace listing details|
237
+ | | Marketplace | Read | [`openmeter.apps.marketplace.getOauth2InstallUrl(id, redirectUrl)`](./src/client/apps.ts#L151) | Get OAuth2 install URL|
238
+ | | Marketplace | Other | [`openmeter.apps.marketplace.authorizeOauth2(id, body)`](./src/client/apps.ts#L172) | Authorize OAuth2 code|
239
+ | | Marketplace | Create | [`openmeter.apps.marketplace.installWithAPIKey(id, body)`](./src/client/apps.ts#L193) | Install app with API key|
240
+ | | Stripe | Create | [`openmeter.apps.stripe.createCheckoutSession(body)`](./src/client/apps.ts#L223) | Create a Stripe checkout session|
241
+ | | Stripe | Update | [`openmeter.apps.stripe.updateApiKey(body)`](./src/client/apps.ts#L243) | Update Stripe API key|
242
+ | | Custom Invoicing | Other | [`openmeter.apps.customInvoicing.draftSynchronized(body)`](./src/client/apps.ts#L271) | Submit draft synchronization results|
243
+ | | Custom Invoicing | Other | [`openmeter.apps.customInvoicing.issuingSynchronized(body)`](./src/client/apps.ts#L295) | Submit issuing synchronization results|
244
+ | | Custom Invoicing | Update | [`openmeter.apps.customInvoicing.updatePaymentStatus(invoiceId, body)`](./src/client/apps.ts#L319) | Update payment status|
245
+ | **[Notifications](#notifications)** | | | | Set up automated notifications for usage thresholds|
246
+ | | Channels | Create | [`openmeter.notifications.channels.create(channel)`](./src/client/notifications.ts#L40) | Create a notification channel|
247
+ | | Channels | Read | [`openmeter.notifications.channels.get(channelId)`](./src/client/notifications.ts#L58) | Get a notification channel by ID|
248
+ | | Channels | Update | [`openmeter.notifications.channels.update(channelId, channel)`](./src/client/notifications.ts#L84) | Update a notification channel|
249
+ | | Channels | Read | [`openmeter.notifications.channels.list(query?)`](./src/client/notifications.ts#L111) | List notification channels|
250
+ | | Channels | Delete | [`openmeter.notifications.channels.delete(channelId)`](./src/client/notifications.ts#L131) | Delete a notification channel|
251
+ | | Rules | Create | [`openmeter.notifications.rules.create(rule)`](./src/client/notifications.ts#L164) | Create a notification rule|
252
+ | | Rules | Read | [`openmeter.notifications.rules.get(ruleId)`](./src/client/notifications.ts#L182) | Get a notification rule by ID|
253
+ | | Rules | Update | [`openmeter.notifications.rules.update(ruleId, rule)`](./src/client/notifications.ts#L205) | Update a notification rule|
254
+ | | Rules | Read | [`openmeter.notifications.rules.list(query?)`](./src/client/notifications.ts#L229) | List notification rules|
255
+ | | Rules | Delete | [`openmeter.notifications.rules.delete(ruleId)`](./src/client/notifications.ts#L249) | Delete a notification rule|
256
+ | | Events | Read | [`openmeter.notifications.events.get(eventId)`](./src/client/notifications.ts#L282) | Get a notification event by ID|
257
+ | | Events | Read | [`openmeter.notifications.events.list(query?)`](./src/client/notifications.ts#L307) | List notification events|
258
+ | **[Portal](#portal)** | | | | Manage consumer portal tokens for customer-facing interfaces|
259
+ | | Portal | Create | [`openmeter.portal.create(body)`](./src/client/portal.ts#L19) | Create a consumer portal token|
260
+ | | Portal | Read | [`openmeter.portal.list(query?)`](./src/client/portal.ts#L34) | List consumer portal tokens|
261
+ | | Portal | Other | [`openmeter.portal.invalidate(query?)`](./src/client/portal.ts#L52) | Invalidate consumer portal tokens|
262
+ | **[Info](#info)** | | | | Utility endpoints for system information|
263
+ | | Info | Read | [`openmeter.info.listCurrencies()`](./src/client/info.ts#L18) | List all supported currencies|
264
+ | | Info | Read | [`openmeter.info.getProgress(id)`](./src/client/info.ts#L32) | Get progress of a long-running operation|
265
+ | **[Debug](#debug)** | | | | Debug utilities for monitoring and troubleshooting|
266
+ | | Debug | Read | [`openmeter.debug.getMetrics()`](./src/client/debug.ts#L18) | Get event ingestion metrics (OpenMetrics format)|
267
+
api/client/javascript/biome.json ADDED
@@ -0,0 +1,67 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "$schema": "./node_modules/@biomejs/biome/configuration_schema.json",
3
+ "assist": {
4
+ "actions": {
5
+ "source": {
6
+ "organizeImports": "on",
7
+ "useSortedKeys": "on"
8
+ }
9
+ }
10
+ },
11
+ "files": {
12
+ "ignoreUnknown": false,
13
+ "includes": ["**"],
14
+ "maxSize": 20000000
15
+ },
16
+ "formatter": {
17
+ "enabled": true,
18
+ "indentStyle": "space",
19
+ "useEditorconfig": true
20
+ },
21
+ "javascript": {
22
+ "formatter": {
23
+ "arrowParentheses": "always",
24
+ "attributePosition": "auto",
25
+ "lineWidth": 80,
26
+ "quoteStyle": "single",
27
+ "semicolons": "asNeeded",
28
+ "trailingCommas": "all"
29
+ }
30
+ },
31
+ "linter": {
32
+ "enabled": true,
33
+ "rules": {
34
+ "recommended": true,
35
+ "style": {
36
+ "noInferrableTypes": "error",
37
+ "noParameterAssign": "error",
38
+ "noUnusedTemplateLiteral": "error",
39
+ "noUselessElse": "error",
40
+ "useAsConstAssertion": "error",
41
+ "useConst": "error",
42
+ "useDefaultParameterLast": "error",
43
+ "useEnumInitializers": "error",
44
+ "useNumberNamespace": "error",
45
+ "useSelfClosingElements": "error",
46
+ "useSingleVarDeclarator": "error"
47
+ }
48
+ }
49
+ },
50
+ "overrides": [
51
+ {
52
+ "assist": {
53
+ "actions": {
54
+ "source": {
55
+ "useSortedKeys": "off"
56
+ }
57
+ }
58
+ },
59
+ "includes": ["package.json"]
60
+ }
61
+ ],
62
+ "vcs": {
63
+ "clientKind": "git",
64
+ "enabled": true,
65
+ "useIgnoreFile": true
66
+ }
67
+ }
api/client/javascript/index.ts ADDED
@@ -0,0 +1 @@
 
 
1
+ export * from './src/client/index.js'
api/client/javascript/orval.config.ts ADDED
@@ -0,0 +1,38 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { defineConfig } from 'orval'
2
+
3
+ export default defineConfig({
4
+ openmeter: {
5
+ input: {
6
+ target: '../../openapi.cloud.yaml',
7
+ },
8
+ output: {
9
+ formatter: 'biome',
10
+ clean: true,
11
+ client: 'zod',
12
+ mode: 'single',
13
+ namingConvention: 'PascalCase',
14
+ override: {
15
+ useDates: true,
16
+ zod: {
17
+ coerce: {
18
+ body: true,
19
+ header: false,
20
+ param: true,
21
+ query: true,
22
+ response: false,
23
+ },
24
+ generate: {
25
+ body: true,
26
+ header: false,
27
+ param: true,
28
+ query: true,
29
+ response: false,
30
+ },
31
+ },
32
+ },
33
+ propertySortOrder: 'Alphabetical',
34
+ target: './src/zod/index.ts',
35
+ tsconfig: './tsconfig.json',
36
+ },
37
+ },
38
+ })
api/client/javascript/package.json ADDED
@@ -0,0 +1,102 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "@openmeter/sdk",
3
+ "version": "0.0.0",
4
+ "description": "Client for OpenMeter: Real-Time and Scalable Usage Metering",
5
+ "license": "Apache 2.0",
6
+ "homepage": "https://openmeter.io",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "https://github.com/openmeterio/openmeter.git",
10
+ "directory": "api/client/javascript"
11
+ },
12
+ "type": "module",
13
+ "files": [
14
+ "dist"
15
+ ],
16
+ "main": "./dist/index.js",
17
+ "types": "./dist/index.d.ts",
18
+ "exports": {
19
+ ".": {
20
+ "import": {
21
+ "types": "./dist/index.d.ts",
22
+ "default": "./dist/index.js"
23
+ },
24
+ "require": {
25
+ "types": "./dist/cjs/index.d.cts",
26
+ "default": "./dist/cjs/index.cjs"
27
+ },
28
+ "default": "./dist/index.js"
29
+ },
30
+ "./portal": {
31
+ "import": {
32
+ "types": "./dist/src/portal/index.d.ts",
33
+ "default": "./dist/src/portal/index.js"
34
+ },
35
+ "require": {
36
+ "types": "./dist/cjs/src/portal/index.d.cts",
37
+ "default": "./dist/cjs/src/portal/index.cjs"
38
+ },
39
+ "default": "./dist/src/portal/index.js"
40
+ },
41
+ "./react": {
42
+ "import": {
43
+ "types": "./dist/src/react/context.d.ts",
44
+ "default": "./dist/src/react/context.js"
45
+ },
46
+ "default": "./dist/src/react/context.js"
47
+ },
48
+ "./zod": {
49
+ "import": {
50
+ "types": "./dist/src/zod/index.d.ts",
51
+ "default": "./dist/src/zod/index.js"
52
+ },
53
+ "require": {
54
+ "types": "./dist/cjs/src/zod/index.d.cts",
55
+ "default": "./dist/cjs/src/zod/index.cjs"
56
+ },
57
+ "default": "./dist/src/zod/index.js"
58
+ }
59
+ },
60
+ "scripts": {
61
+ "build": "duel",
62
+ "format": "biome format --write .",
63
+ "generate": "pnpm run generate:client && pnpm run generate:zod",
64
+ "generate:client": "tsx scripts/generate.ts && biome format --write ./src/client/schemas.ts",
65
+ "generate:zod": "orval && tsx scripts/add-as-const.ts && biome lint --write ./src/zod/index.ts && biome format --write ./src/zod/index.ts",
66
+ "lint": "tsc --noEmit && biome lint .",
67
+ "prepublishOnly": "pnpm run generate && pnpm run build && pnpm run lint && pnpm run test",
68
+ "pretest": "pnpm run build",
69
+ "test": "vitest --run",
70
+ "test:watch": "vitest --watch"
71
+ },
72
+ "engines": {
73
+ "node": ">=22.0.0"
74
+ },
75
+ "dependencies": {
76
+ "openapi-fetch": "0.17.0",
77
+ "openapi-typescript-helpers": "0.1.0"
78
+ },
79
+ "devDependencies": {
80
+ "@biomejs/biome": "2.4.16",
81
+ "@fetch-mock/vitest": "0.2.18",
82
+ "@knighted/duel": "4.1.0",
83
+ "@types/node": "25.9.2",
84
+ "@types/node-fetch": "2.6.13",
85
+ "@types/react": "19.2.17",
86
+ "fetch-mock": "12.6.0",
87
+ "openapi-typescript": "7.13.0",
88
+ "orval": "8.15.0",
89
+ "prettier": "3.8.3",
90
+ "react": "19.2.7",
91
+ "rollup": "4.61.1",
92
+ "tslib": "2.8.1",
93
+ "tsx": "4.22.4",
94
+ "typescript": "5.9.3",
95
+ "vitest": "4.1.8",
96
+ "zod": "4.4.3"
97
+ },
98
+ "packageManager": "pnpm@11.1.2+sha512.415a1cc25974731e75455c1468371be74c5aa5fb7621b50d4056d222451609f11412f23fd602e6169f1e060466641f798597e1be961a10688836a67b16569499",
99
+ "peerDependencies": {
100
+ "react": ">=18.0.0"
101
+ }
102
+ }
api/client/javascript/patches/openapi-typescript.patch ADDED
The diff for this file is too large to render. See raw diff
 
api/client/javascript/pnpm-lock.yaml ADDED
The diff for this file is too large to render. See raw diff
 
api/client/javascript/pnpm-workspace.yaml ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ blockExoticSubdeps: true
2
+ minimumReleaseAge: 4320
3
+ trustPolicy: no-downgrade
4
+ patchedDependencies:
5
+ openapi-typescript: patches/openapi-typescript.patch
6
+ overrides:
7
+ 'vite@>=7.0.0 <=7.3.1': '>=7.3.2'
8
+ 'vite@>=7.1.0 <=7.3.1': '>=7.3.2'
9
+ allowBuilds:
10
+ esbuild: true
api/client/javascript/scripts/add-as-const.ts ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { readFileSync, writeFileSync } from 'node:fs'
2
+
3
+ /**
4
+ * Post-generation workaround for orval's zod output: object-literal defaults
5
+ * are emitted without `as const`, so property values widen to `string` and
6
+ * fail Zod's `.default()` signature when the schema expects a literal type.
7
+ * Remove this script once orval emits `as const` for object-literal defaults.
8
+ * See: https://github.com/orval-labs/orval/issues/3244
9
+ */
10
+ const file = new URL('../src/zod/index.ts', import.meta.url)
11
+ const src = readFileSync(file, 'utf8')
12
+ const fixed = src.replace(
13
+ /(^export const \w+Default =\s*\{[^{}]*\})/gm,
14
+ '$1 as const',
15
+ )
16
+ writeFileSync(file, fixed)
api/client/javascript/scripts/generate.ts ADDED
@@ -0,0 +1,51 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import fs from 'node:fs'
2
+ import openapiTS, { astToString } from 'openapi-typescript'
3
+ import { factory, SyntaxKind } from 'typescript'
4
+
5
+ const DATE = factory.createTypeReferenceNode(factory.createIdentifier('Date')) // `Date`
6
+ const NULL = factory.createLiteralTypeNode(factory.createNull()) // `null`
7
+ const STRING = factory.createKeywordTypeNode(SyntaxKind.StringKeyword) // `string`
8
+
9
+ const schema = new URL('../../../openapi.cloud.yaml', import.meta.url)
10
+
11
+ const ast = await openapiTS(schema, {
12
+ defaultNonNullable: false,
13
+ rootTypes: true,
14
+ rootTypesNoSchemaPrefix: true,
15
+ transform(schemaObject, metadata) {
16
+ if (metadata.path === '#/components/schemas/Event') {
17
+ if (
18
+ schemaObject.type === 'string' &&
19
+ !schemaObject.nullable &&
20
+ !['customer-id', 'com.example.someevent'].includes(schemaObject.example)
21
+ ) {
22
+ return {
23
+ questionToken: true,
24
+ schema: STRING,
25
+ }
26
+ }
27
+ }
28
+ if (schemaObject.format === 'date-time') {
29
+ const allowString =
30
+ (metadata.schema &&
31
+ 'in' in metadata.schema &&
32
+ metadata.schema.in === 'query') ||
33
+ metadata.path?.includes('/parameters/query')
34
+
35
+ // allow string in query parameters
36
+ if (allowString) {
37
+ return schemaObject.nullable
38
+ ? factory.createUnionTypeNode([DATE, NULL, STRING])
39
+ : factory.createUnionTypeNode([DATE, STRING])
40
+ }
41
+
42
+ return schemaObject.nullable
43
+ ? factory.createUnionTypeNode([DATE, NULL])
44
+ : DATE
45
+ }
46
+ },
47
+ })
48
+
49
+ const contents = astToString(ast)
50
+
51
+ fs.writeFileSync('./src/client/schemas.ts', contents)
api/client/javascript/src/client/addons.ts ADDED
@@ -0,0 +1,122 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import type { Client } from 'openapi-fetch'
2
+ import type { RequestOptions } from './common.js'
3
+ import type { AddonCreate, operations, paths } from './schemas.js'
4
+ import { transformResponse } from './utils.js'
5
+
6
+ export class Addons {
7
+ constructor(private client: Client<paths, `${string}/${string}`>) {}
8
+
9
+ /**
10
+ * Create a addon
11
+ * @param addon - The addon to create
12
+ * @param options - Optional request options
13
+ * @returns The created addon
14
+ */
15
+ public async create(addon: AddonCreate, options?: RequestOptions) {
16
+ const resp = await this.client.POST('/api/v1/addons', {
17
+ body: addon,
18
+ ...options,
19
+ })
20
+
21
+ return transformResponse(resp)
22
+ }
23
+
24
+ /**
25
+ * List addons
26
+ * @param params - Optional parameters for listing addons
27
+ * @param options - Optional request options
28
+ * @returns A list of addons
29
+ */
30
+ public async list(
31
+ params?: operations['listAddons']['parameters']['query'],
32
+ options?: RequestOptions,
33
+ ) {
34
+ const resp = await this.client.GET('/api/v1/addons', {
35
+ params: { query: params },
36
+ ...options,
37
+ })
38
+
39
+ return transformResponse(resp)
40
+ }
41
+
42
+ /**
43
+ * Get an addon by ID
44
+ * @param addonId - The ID of the addon to retrieve
45
+ * @param options - Optional request options
46
+ * @returns The addon
47
+ */
48
+ public async get(addonId: string, options?: RequestOptions) {
49
+ const resp = await this.client.GET('/api/v1/addons/{addonId}', {
50
+ params: { path: { addonId } },
51
+ ...options,
52
+ })
53
+
54
+ return transformResponse(resp)
55
+ }
56
+
57
+ /**
58
+ * Update an addon
59
+ * @param addonId - The ID of the addon to update
60
+ * @param addon - The addon data to update
61
+ * @param options - Optional request options
62
+ * @returns The updated addon
63
+ */
64
+ public async update(
65
+ addonId: string,
66
+ addon: operations['updateAddon']['requestBody']['content']['application/json'],
67
+ options?: RequestOptions,
68
+ ) {
69
+ const resp = await this.client.PUT('/api/v1/addons/{addonId}', {
70
+ body: addon,
71
+ params: { path: { addonId } },
72
+ ...options,
73
+ })
74
+
75
+ return transformResponse(resp)
76
+ }
77
+
78
+ /**
79
+ * Delete an addon by ID
80
+ * @param addonId - The ID of the addon to delete
81
+ * @param options - Optional request options
82
+ * @returns void or standard error response structure
83
+ */
84
+ public async delete(addonId: string, options?: RequestOptions) {
85
+ const resp = await this.client.DELETE('/api/v1/addons/{addonId}', {
86
+ params: { path: { addonId } },
87
+ ...options,
88
+ })
89
+
90
+ return transformResponse(resp)
91
+ }
92
+
93
+ /**
94
+ * Publish an addon
95
+ * @param addonId - The ID of the addon to publish
96
+ * @param options - Optional request options
97
+ * @returns The published addon
98
+ */
99
+ public async publish(addonId: string, options?: RequestOptions) {
100
+ const resp = await this.client.POST('/api/v1/addons/{addonId}/publish', {
101
+ params: { path: { addonId } },
102
+ ...options,
103
+ })
104
+
105
+ return transformResponse(resp)
106
+ }
107
+
108
+ /**
109
+ * Archive an addon
110
+ * @param addonId - The ID of the addon to archive
111
+ * @param options - Optional request options
112
+ * @returns The archived addon
113
+ */
114
+ public async archive(addonId: string, options?: RequestOptions) {
115
+ const resp = await this.client.POST('/api/v1/addons/{addonId}/archive', {
116
+ params: { path: { addonId } },
117
+ ...options,
118
+ })
119
+
120
+ return transformResponse(resp)
121
+ }
122
+ }
api/client/javascript/src/client/apps.ts ADDED
@@ -0,0 +1,335 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import type { Client } from 'openapi-fetch'
2
+ import type { RequestOptions } from './common.js'
3
+ import type {
4
+ AppReplaceUpdate,
5
+ CreateStripeCheckoutSessionRequest,
6
+ operations,
7
+ paths,
8
+ } from './schemas.js'
9
+ import { transformResponse } from './utils.js'
10
+
11
+ /**
12
+ * Apps
13
+ * Manage integrations for extending OpenMeter's functionality.
14
+ */
15
+ export class Apps {
16
+ public marketplace: AppMarketplace
17
+ public stripe: AppStripe
18
+ public customInvoicing: AppCustomInvoicing
19
+
20
+ constructor(private client: Client<paths, `${string}/${string}`>) {
21
+ this.marketplace = new AppMarketplace(client)
22
+ this.stripe = new AppStripe(client)
23
+ this.customInvoicing = new AppCustomInvoicing(client)
24
+ }
25
+
26
+ /**
27
+ * List apps
28
+ * @param query - The query parameters
29
+ * @param signal - An optional abort signal
30
+ * @returns The apps
31
+ */
32
+ public async list(
33
+ query?: operations['listApps']['parameters']['query'],
34
+ options?: RequestOptions,
35
+ ) {
36
+ const resp = await this.client.GET('/api/v1/apps', {
37
+ params: { query },
38
+ ...options,
39
+ })
40
+
41
+ return transformResponse(resp)
42
+ }
43
+
44
+ /**
45
+ * Get an app
46
+ * @param id - The ID of the app
47
+ * @param signal - An optional abort signal
48
+ * @returns The app
49
+ */
50
+ public async get(
51
+ id: operations['getApp']['parameters']['path']['id'],
52
+ options?: RequestOptions,
53
+ ) {
54
+ const resp = await this.client.GET('/api/v1/apps/{id}', {
55
+ params: { path: { id } },
56
+ ...options,
57
+ })
58
+
59
+ return transformResponse(resp)
60
+ }
61
+
62
+ /**
63
+ * Update an app
64
+ * @param id - The ID of the app
65
+ * @param body - The body of the request
66
+ * @param signal - An optional abort signal
67
+ * @returns The app
68
+ */
69
+ public async update(
70
+ id: operations['updateApp']['parameters']['path']['id'],
71
+ body: AppReplaceUpdate,
72
+ options?: RequestOptions,
73
+ ) {
74
+ const resp = await this.client.PUT('/api/v1/apps/{id}', {
75
+ body,
76
+ params: { path: { id } },
77
+ ...options,
78
+ })
79
+
80
+ return transformResponse(resp)
81
+ }
82
+
83
+ /**
84
+ * Uninstall an app
85
+ * @param id - The ID of the app
86
+ * @param signal - An optional abort signal
87
+ * @returns The app
88
+ */
89
+ public async uninstall(
90
+ id: operations['uninstallApp']['parameters']['path']['id'],
91
+ options?: RequestOptions,
92
+ ) {
93
+ const resp = await this.client.DELETE('/api/v1/apps/{id}', {
94
+ params: { path: { id } },
95
+ ...options,
96
+ })
97
+
98
+ return transformResponse(resp)
99
+ }
100
+ }
101
+
102
+ /**
103
+ * App Marketplace
104
+ * Available apps from the OpenMeter Marketplace.
105
+ */
106
+ export class AppMarketplace {
107
+ constructor(private client: Client<paths, `${string}/${string}`>) {}
108
+
109
+ /**
110
+ * List available apps
111
+ * @param query - The query parameters
112
+ * @param signal - An optional abort signal
113
+ * @returns The apps
114
+ */
115
+ public async list(
116
+ query?: operations['listMarketplaceListings']['parameters']['query'],
117
+ options?: RequestOptions,
118
+ ) {
119
+ const resp = await this.client.GET('/api/v1/marketplace/listings', {
120
+ params: { query },
121
+ ...options,
122
+ })
123
+
124
+ return transformResponse(resp)
125
+ }
126
+
127
+ /**
128
+ * Get details for a listing
129
+ * @param type - The type of the listing
130
+ * @param signal - An optional abort signal
131
+ * @returns The listing
132
+ */
133
+ public async get(
134
+ type: operations['getMarketplaceListing']['parameters']['path']['type'],
135
+ options?: RequestOptions,
136
+ ) {
137
+ const resp = await this.client.GET('/api/v1/marketplace/listings/{type}', {
138
+ params: { path: { type } },
139
+ ...options,
140
+ })
141
+
142
+ return transformResponse(resp)
143
+ }
144
+
145
+ /**
146
+ * Install an app via OAuth. Returns a URL to start the OAuth 2.0 flow.
147
+ * @param type - The type of the listing
148
+ * @param signal - An optional abort signal
149
+ * @returns The OAuth2 install URL
150
+ */
151
+ public async getOauth2InstallUrl(
152
+ type: operations['marketplaceOAuth2InstallGetURL']['parameters']['path']['type'],
153
+ options?: RequestOptions,
154
+ ) {
155
+ const resp = await this.client.GET(
156
+ '/api/v1/marketplace/listings/{type}/install/oauth2',
157
+ {
158
+ params: { path: { type } },
159
+ ...options,
160
+ },
161
+ )
162
+
163
+ return transformResponse(resp)
164
+ }
165
+
166
+ /**
167
+ * Authorize OAuth2 code. Verifies the OAuth code and exchanges it for a token and refresh token
168
+ * @param type - The type of the listing
169
+ * @param signal - An optional abort signal
170
+ * @returns The authorization URL
171
+ */
172
+ public async authorizeOauth2(
173
+ type: operations['marketplaceOAuth2InstallAuthorize']['parameters']['path']['type'],
174
+ options?: RequestOptions,
175
+ ) {
176
+ const resp = await this.client.GET(
177
+ '/api/v1/marketplace/listings/{type}/install/oauth2/authorize',
178
+ {
179
+ params: { path: { type } },
180
+ ...options,
181
+ },
182
+ )
183
+
184
+ return transformResponse(resp)
185
+ }
186
+
187
+ /**
188
+ * Install an app via API key.
189
+ * @param type - The type of the listing
190
+ * @param signal - An optional abort signal
191
+ * @returns The installation
192
+ */
193
+ public async installWithAPIKey(
194
+ type: operations['marketplaceAppAPIKeyInstall']['parameters']['path']['type'],
195
+ body: operations['marketplaceAppAPIKeyInstall']['requestBody']['content']['application/json'],
196
+ options?: RequestOptions,
197
+ ) {
198
+ const resp = await this.client.POST(
199
+ '/api/v1/marketplace/listings/{type}/install/apikey',
200
+ {
201
+ body,
202
+ params: { path: { type } },
203
+ ...options,
204
+ },
205
+ )
206
+
207
+ return transformResponse(resp)
208
+ }
209
+ }
210
+
211
+ /**
212
+ * Stripe App
213
+ */
214
+ export class AppStripe {
215
+ constructor(private client: Client<paths, `${string}/${string}`>) {}
216
+
217
+ /**
218
+ * Create a checkout session
219
+ * @param body - The body of the request
220
+ * @param signal - An optional abort signal
221
+ * @returns The checkout session
222
+ */
223
+ public async createCheckoutSession(
224
+ body: CreateStripeCheckoutSessionRequest,
225
+ options?: RequestOptions,
226
+ ) {
227
+ const resp = await this.client.POST('/api/v1/stripe/checkout/sessions', {
228
+ body,
229
+ ...options,
230
+ })
231
+
232
+ return transformResponse(resp)
233
+ }
234
+
235
+ /**
236
+ * Update Stripe API key
237
+ * @param id - The ID of the app
238
+ * @param body - The API key data
239
+ * @param options - The request options
240
+ * @returns The updated API key
241
+ * @deprecated
242
+ */
243
+ public async updateApiKey(
244
+ id: string,
245
+ body: operations['updateStripeAPIKey']['requestBody']['content']['application/json'],
246
+ options?: RequestOptions,
247
+ ) {
248
+ const resp = await this.client.PUT('/api/v1/apps/{id}/stripe/api-key', {
249
+ body,
250
+ params: { path: { id } },
251
+ ...options,
252
+ })
253
+
254
+ return transformResponse(resp)
255
+ }
256
+ }
257
+
258
+ /**
259
+ * Custom Invoicing App
260
+ */
261
+ export class AppCustomInvoicing {
262
+ constructor(private client: Client<paths, `${string}/${string}`>) {}
263
+
264
+ /**
265
+ * Submit draft synchronization results
266
+ * @param invoiceId - The ID of the invoice
267
+ * @param body - The body of the request
268
+ * @param options - The request options
269
+ * @returns The synchronization result
270
+ */
271
+ public async draftSynchronized(
272
+ invoiceId: string,
273
+ body: operations['appCustomInvoicingDraftSynchronized']['requestBody']['content']['application/json'],
274
+ options?: RequestOptions,
275
+ ) {
276
+ const resp = await this.client.POST(
277
+ '/api/v1/apps/custom-invoicing/{invoiceId}/draft/synchronized',
278
+ {
279
+ body,
280
+ params: { path: { invoiceId } },
281
+ ...options,
282
+ },
283
+ )
284
+
285
+ return transformResponse(resp)
286
+ }
287
+
288
+ /**
289
+ * Submit issuing synchronization results
290
+ * @param invoiceId - The ID of the invoice
291
+ * @param body - The body of the request
292
+ * @param options - The request options
293
+ * @returns The synchronization result
294
+ */
295
+ public async issuingSynchronized(
296
+ invoiceId: string,
297
+ body: operations['appCustomInvoicingIssuingSynchronized']['requestBody']['content']['application/json'],
298
+ options?: RequestOptions,
299
+ ) {
300
+ const resp = await this.client.POST(
301
+ '/api/v1/apps/custom-invoicing/{invoiceId}/issuing/synchronized',
302
+ {
303
+ body,
304
+ params: { path: { invoiceId } },
305
+ ...options,
306
+ },
307
+ )
308
+
309
+ return transformResponse(resp)
310
+ }
311
+
312
+ /**
313
+ * Update payment status
314
+ * @param invoiceId - The ID of the invoice
315
+ * @param body - The body of the request
316
+ * @param options - The request options
317
+ * @returns The payment status update result
318
+ */
319
+ public async updatePaymentStatus(
320
+ invoiceId: string,
321
+ body: operations['appCustomInvoicingUpdatePaymentStatus']['requestBody']['content']['application/json'],
322
+ options?: RequestOptions,
323
+ ) {
324
+ const resp = await this.client.POST(
325
+ '/api/v1/apps/custom-invoicing/{invoiceId}/payment/status',
326
+ {
327
+ body,
328
+ params: { path: { invoiceId } },
329
+ ...options,
330
+ },
331
+ )
332
+
333
+ return transformResponse(resp)
334
+ }
335
+ }
api/client/javascript/src/client/billing.ts ADDED
@@ -0,0 +1,525 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import type { Client } from 'openapi-fetch'
2
+ import type { RequestOptions } from './common.js'
3
+ import type {
4
+ BillingProfileCreate,
5
+ BillingProfileCustomerOverrideCreate,
6
+ BillingProfileReplaceUpdateWithWorkflow,
7
+ InvoicePendingLineCreateInput,
8
+ InvoiceReplaceUpdate,
9
+ InvoiceSimulationInput,
10
+ operations,
11
+ paths,
12
+ VoidInvoiceActionInput,
13
+ } from './schemas.js'
14
+ import { transformResponse } from './utils.js'
15
+ /**
16
+ * Billing
17
+ */
18
+ export class Billing {
19
+ public profiles: BillingProfiles
20
+ public invoices: BillingInvoices
21
+ public customers: BillingCustomers
22
+
23
+ constructor(private client: Client<paths, `${string}/${string}`>) {
24
+ this.profiles = new BillingProfiles(this.client)
25
+ this.invoices = new BillingInvoices(this.client)
26
+ this.customers = new BillingCustomers(this.client)
27
+ }
28
+ }
29
+
30
+ /**
31
+ * Billing Profiles
32
+ */
33
+ export class BillingProfiles {
34
+ constructor(private client: Client<paths, `${string}/${string}`>) {}
35
+
36
+ /**
37
+ * Create a billing profile
38
+ * @param billingProfile - The billing profile to create
39
+ * @param signal - An optional abort signal
40
+ * @returns The created billing profile
41
+ */
42
+ public async create(
43
+ billingProfile: BillingProfileCreate,
44
+ options?: RequestOptions,
45
+ ) {
46
+ const resp = await this.client.POST('/api/v1/billing/profiles', {
47
+ body: billingProfile,
48
+ ...options,
49
+ })
50
+
51
+ return transformResponse(resp)
52
+ }
53
+
54
+ /**
55
+ * Get a billing profile by ID
56
+ * @param id - The ID of the billing profile to get
57
+ * @param signal - An optional abort signal
58
+ * @returns The billing profile
59
+ */
60
+ public async get(
61
+ id: operations['getBillingProfile']['parameters']['path']['id'],
62
+ options?: RequestOptions,
63
+ ) {
64
+ const resp = await this.client.GET('/api/v1/billing/profiles/{id}', {
65
+ params: {
66
+ path: { id },
67
+ },
68
+ ...options,
69
+ })
70
+
71
+ return transformResponse(resp)
72
+ }
73
+
74
+ /**
75
+ * List billing profiles
76
+ * @param query - The query parameters
77
+ * @param signal - An optional abort signal
78
+ * @returns The billing profiles
79
+ */
80
+ public async list(
81
+ query?: operations['listBillingProfiles']['parameters']['query'],
82
+ options?: RequestOptions,
83
+ ) {
84
+ const resp = await this.client.GET('/api/v1/billing/profiles', {
85
+ params: {
86
+ query,
87
+ },
88
+ ...options,
89
+ })
90
+
91
+ return transformResponse(resp)
92
+ }
93
+
94
+ /**
95
+ * Update a billing profile
96
+ * @param id - The ID of the billing profile to update
97
+ * @param billingProfile - The billing profile to update
98
+ * @param signal - An optional abort signal
99
+ * @returns The updated billing profile
100
+ */
101
+ public async update(
102
+ id: operations['updateBillingProfile']['parameters']['path']['id'],
103
+ billingProfile: BillingProfileReplaceUpdateWithWorkflow,
104
+ options?: RequestOptions,
105
+ ) {
106
+ const resp = await this.client.PUT('/api/v1/billing/profiles/{id}', {
107
+ body: billingProfile,
108
+ params: {
109
+ path: { id },
110
+ },
111
+ ...options,
112
+ })
113
+
114
+ return transformResponse(resp)
115
+ }
116
+
117
+ /**
118
+ * Delete a billing profile
119
+ * @param id - The ID of the billing profile to delete
120
+ * @param options - The request options
121
+ * @returns The deleted billing profile
122
+ */
123
+ public async delete(
124
+ id: operations['deleteBillingProfile']['parameters']['path']['id'],
125
+ options?: RequestOptions,
126
+ ) {
127
+ const resp = await this.client.DELETE('/api/v1/billing/profiles/{id}', {
128
+ params: {
129
+ path: { id },
130
+ },
131
+ ...options,
132
+ })
133
+
134
+ return transformResponse(resp)
135
+ }
136
+ }
137
+
138
+ /**
139
+ * Billing Invoices
140
+ */
141
+ export class BillingInvoices {
142
+ constructor(private client: Client<paths, `${string}/${string}`>) {}
143
+
144
+ /**
145
+ * List invoices
146
+ * @param query - The query parameters
147
+ * @param signal - An optional abort signal
148
+ * @returns The invoices
149
+ */
150
+ public async list(
151
+ query?: operations['listInvoices']['parameters']['query'],
152
+ options?: RequestOptions,
153
+ ) {
154
+ const resp = await this.client.GET('/api/v1/billing/invoices', {
155
+ params: {
156
+ query,
157
+ },
158
+ ...options,
159
+ })
160
+
161
+ return transformResponse(resp)
162
+ }
163
+
164
+ /**
165
+ * Get an invoice by ID
166
+ * @param id - The ID of the invoice to get
167
+ * @param signal - An optional abort signal
168
+ * @returns The invoice
169
+ */
170
+ public async get(
171
+ id: operations['getInvoice']['parameters']['path']['invoiceId'],
172
+ options?: RequestOptions,
173
+ ) {
174
+ const resp = await this.client.GET('/api/v1/billing/invoices/{invoiceId}', {
175
+ params: {
176
+ path: { invoiceId: id },
177
+ },
178
+ ...options,
179
+ })
180
+
181
+ return transformResponse(resp)
182
+ }
183
+
184
+ /**
185
+ * Update an invoice
186
+ * @description Only invoices in draft or earlier status can be updated.
187
+ * @param id - The ID of the invoice to update
188
+ * @param invoice - The invoice to update
189
+ * @param signal - An optional abort signal
190
+ * @returns The updated invoice
191
+ */
192
+ public async update(
193
+ id: operations['updateInvoice']['parameters']['path']['invoiceId'],
194
+ invoice: InvoiceReplaceUpdate,
195
+ options?: RequestOptions,
196
+ ) {
197
+ const resp = await this.client.PUT('/api/v1/billing/invoices/{invoiceId}', {
198
+ body: invoice,
199
+ params: { path: { invoiceId: id } },
200
+ ...options,
201
+ })
202
+
203
+ return transformResponse(resp)
204
+ }
205
+
206
+ /**
207
+ * Delete an invoice
208
+ * @description Only invoices that are in the draft (or earlier) status can be deleted.
209
+ * @param id - The ID of the invoice to delete
210
+ * @param options - The request options
211
+ * @returns The deleted invoice
212
+ */
213
+ public async delete(
214
+ id: operations['deleteInvoice']['parameters']['path']['invoiceId'],
215
+ options?: RequestOptions,
216
+ ) {
217
+ const resp = await this.client.DELETE(
218
+ '/api/v1/billing/invoices/{invoiceId}',
219
+ {
220
+ params: { path: { invoiceId: id } },
221
+ ...options,
222
+ },
223
+ )
224
+
225
+ return transformResponse(resp)
226
+ }
227
+
228
+ /**
229
+ * Advance the invoice to the next status
230
+ * @description The call doesn't "approve the invoice", it only advances the invoice to the next status if the transition would be automatic. The action can be called when the invoice's statusDetails' actions field contain the "advance" action.
231
+ * @param id - The ID of the invoice to advance
232
+ * @param signal - An optional abort signal
233
+ * @returns The advanced invoice
234
+ */
235
+ public async advance(
236
+ id: operations['advanceInvoiceAction']['parameters']['path']['invoiceId'],
237
+ options?: RequestOptions,
238
+ ) {
239
+ const resp = await this.client.POST(
240
+ '/api/v1/billing/invoices/{invoiceId}/advance',
241
+ {
242
+ params: { path: { invoiceId: id } },
243
+ ...options,
244
+ },
245
+ )
246
+
247
+ return transformResponse(resp)
248
+ }
249
+
250
+ /**
251
+ * Approve an invoice
252
+ * @description This call instantly sends the invoice to the customer using the configured billing profile app.
253
+ * @param id - The ID of the invoice to approve
254
+ * @param signal - An optional abort signal
255
+ * @returns The approved invoice
256
+ */
257
+ public async approve(
258
+ id: operations['approveInvoiceAction']['parameters']['path']['invoiceId'],
259
+ options?: RequestOptions,
260
+ ) {
261
+ const resp = await this.client.POST(
262
+ '/api/v1/billing/invoices/{invoiceId}/approve',
263
+ {
264
+ params: { path: { invoiceId: id } },
265
+ ...options,
266
+ },
267
+ )
268
+
269
+ return transformResponse(resp)
270
+ }
271
+
272
+ /**
273
+ * Retry advancing the invoice after a failed attempt.
274
+ * @param id - The ID of the invoice to retry
275
+ * @param signal - An optional abort signal
276
+ * @returns The retried invoice
277
+ */
278
+ public async retry(
279
+ id: operations['retryInvoiceAction']['parameters']['path']['invoiceId'],
280
+ options?: RequestOptions,
281
+ ) {
282
+ const resp = await this.client.POST(
283
+ '/api/v1/billing/invoices/{invoiceId}/retry',
284
+ {
285
+ params: { path: { invoiceId: id } },
286
+ ...options,
287
+ },
288
+ )
289
+
290
+ return transformResponse(resp)
291
+ }
292
+
293
+ /**
294
+ * Void an invoice
295
+ * @description Void an invoice
296
+ *
297
+ * Only invoices that have been alread issued can be voided.
298
+ * @param id - The ID of the invoice to void
299
+ * @param signal - An optional abort signal
300
+ * @returns The voided invoice
301
+ */
302
+ public async void(
303
+ id: operations['voidInvoiceAction']['parameters']['path']['invoiceId'],
304
+ body: VoidInvoiceActionInput,
305
+ options?: RequestOptions,
306
+ ) {
307
+ const resp = await this.client.POST(
308
+ '/api/v1/billing/invoices/{invoiceId}/void',
309
+ {
310
+ body,
311
+ params: { path: { invoiceId: id } },
312
+ ...options,
313
+ },
314
+ )
315
+
316
+ return transformResponse(resp)
317
+ }
318
+
319
+ /**
320
+ * Recalculate an invoice's tax amounts
321
+ * @param id - The ID of the invoice to recalculate
322
+ * @param signal - An optional abort signal
323
+ * @returns The recalculated invoice
324
+ */
325
+ public async recalculateTax(
326
+ id: operations['recalculateInvoiceTaxAction']['parameters']['path']['invoiceId'],
327
+ options?: RequestOptions,
328
+ ) {
329
+ const resp = await this.client.POST(
330
+ '/api/v1/billing/invoices/{invoiceId}/taxes/recalculate',
331
+ {
332
+ params: { path: { invoiceId: id } },
333
+ ...options,
334
+ },
335
+ )
336
+
337
+ return transformResponse(resp)
338
+ }
339
+
340
+ /**
341
+ * Snapshot invoice line item quantities
342
+ * @description Snapshot the quantities of the invoice line items. This is useful for invoices that have usage-based line items.
343
+ * @param id - The ID of the invoice to snapshot
344
+ * @param signal - An optional abort signal
345
+ * @returns The invoice with snapshotted quantities
346
+ */
347
+ public async snapshotQuantities(
348
+ id: operations['snapshotQuantitiesInvoiceAction']['parameters']['path']['invoiceId'],
349
+ options?: RequestOptions,
350
+ ) {
351
+ const resp = await this.client.POST(
352
+ '/api/v1/billing/invoices/{invoiceId}/snapshot-quantities',
353
+ {
354
+ params: { path: { invoiceId: id } },
355
+ ...options,
356
+ },
357
+ )
358
+
359
+ return transformResponse(resp)
360
+ }
361
+
362
+ /**
363
+ * Simulate an invoice for a customer
364
+ * @param id - The ID of the customer to simulate the invoice for
365
+ * @param signal - An optional abort signal
366
+ * @returns The simulated invoice
367
+ */
368
+ public async simulate(
369
+ id: operations['simulateInvoice']['parameters']['path']['customerId'],
370
+ body: InvoiceSimulationInput,
371
+ options?: RequestOptions,
372
+ ) {
373
+ const resp = await this.client.POST(
374
+ '/api/v1/billing/customers/{customerId}/invoices/simulate',
375
+ {
376
+ body,
377
+ params: { path: { customerId: id } },
378
+ ...options,
379
+ },
380
+ )
381
+
382
+ return transformResponse(resp)
383
+ }
384
+
385
+ /**
386
+ * Create pending line items
387
+ * @description Create new pending line items (charges).
388
+ * This call is used to create a new pending line item for the customer if required a new
389
+ * gathering invoice will be created.
390
+ *
391
+ * A new invoice will be created if:
392
+ * - there is no invoice in gathering state
393
+ * - the currency of the line item doesn't match the currency of any invoices in gathering state
394
+ * @param customerId - The ID of the customer to create the line items for
395
+ * @param body - The line items to create
396
+ * @param signal - An optional abort signal
397
+ * @returns The created line items
398
+ */
399
+ public async createLineItems(
400
+ customerId: operations['createPendingInvoiceLine']['parameters']['path']['customerId'],
401
+ body: InvoicePendingLineCreateInput,
402
+ options?: RequestOptions,
403
+ ) {
404
+ const resp = await this.client.POST(
405
+ '/api/v1/billing/customers/{customerId}/invoices/pending-lines',
406
+ {
407
+ body,
408
+ params: { path: { customerId } },
409
+ ...options,
410
+ },
411
+ )
412
+
413
+ return transformResponse(resp)
414
+ }
415
+
416
+ /**
417
+ * Invoice a customer based on the pending line items
418
+ * @description Create a new invoice from the pending line items. This should only be called if for some reason we need to invoice a customer outside of the normal billing cycle.
419
+ * @param body - The invoice data
420
+ * @param options - The request options
421
+ * @returns The created invoices
422
+ */
423
+ public async invoicePendingLines(
424
+ body: operations['invoicePendingLinesAction']['requestBody']['content']['application/json'],
425
+ options?: RequestOptions,
426
+ ) {
427
+ const resp = await this.client.POST('/api/v1/billing/invoices/invoice', {
428
+ body,
429
+ ...options,
430
+ })
431
+
432
+ return transformResponse(resp)
433
+ }
434
+ }
435
+
436
+ /**
437
+ * Billing Customer Invoices and Overrides
438
+ */
439
+ export class BillingCustomers {
440
+ constructor(private client: Client<paths, `${string}/${string}`>) {}
441
+
442
+ /**
443
+ * Create or update a customer override
444
+ * @param id - The ID of the customer to create the override for
445
+ * @param body - The customer override to create
446
+ * @param signal - An optional abort signal
447
+ * @returns The created customer override
448
+ */
449
+ public async createOverride(
450
+ id: operations['upsertBillingProfileCustomerOverride']['parameters']['path']['customerId'],
451
+ body: BillingProfileCustomerOverrideCreate,
452
+ options?: RequestOptions,
453
+ ) {
454
+ const resp = await this.client.PUT(
455
+ '/api/v1/billing/customers/{customerId}',
456
+ {
457
+ body,
458
+ params: { path: { customerId: id } },
459
+ ...options,
460
+ },
461
+ )
462
+
463
+ return transformResponse(resp)
464
+ }
465
+
466
+ /**
467
+ * Get a customer override
468
+ * @param id - The ID of the customer to get the override for
469
+ * @param signal - An optional abort signal
470
+ * @returns The customer override
471
+ */
472
+ public async getOverride(
473
+ id: operations['getBillingProfileCustomerOverride']['parameters']['path']['customerId'],
474
+ options?: RequestOptions,
475
+ ) {
476
+ const resp = await this.client.GET(
477
+ '/api/v1/billing/customers/{customerId}',
478
+ {
479
+ params: { path: { customerId: id } },
480
+ ...options,
481
+ },
482
+ )
483
+
484
+ return transformResponse(resp)
485
+ }
486
+
487
+ /**
488
+ * List customer overrides
489
+ * @param query - The query parameters
490
+ * @param signal - An optional abort signal
491
+ * @returns The customer overrides
492
+ */
493
+ public async listOverrides(
494
+ query?: operations['listBillingProfileCustomerOverrides']['parameters']['query'],
495
+ options?: RequestOptions,
496
+ ) {
497
+ const resp = await this.client.GET('/api/v1/billing/customers', {
498
+ params: { query },
499
+ ...options,
500
+ })
501
+
502
+ return transformResponse(resp)
503
+ }
504
+
505
+ /**
506
+ * Delete a customer override
507
+ * @param id - The ID of the customer to delete the override for
508
+ * @param signal - An optional abort signal
509
+ * @returns The deleted customer override
510
+ */
511
+ public async deleteOverride(
512
+ id: operations['deleteBillingProfileCustomerOverride']['parameters']['path']['customerId'],
513
+ options?: RequestOptions,
514
+ ) {
515
+ const resp = await this.client.DELETE(
516
+ '/api/v1/billing/customers/{customerId}',
517
+ {
518
+ params: { path: { customerId: id } },
519
+ ...options,
520
+ },
521
+ )
522
+
523
+ return transformResponse(resp)
524
+ }
525
+ }
api/client/javascript/src/client/common.ts ADDED
@@ -0,0 +1,66 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import type { UnexpectedProblemResponse } from './schemas.js'
2
+
3
+ /**
4
+ * Request options
5
+ */
6
+ export type RequestOptions = Pick<RequestInit, 'signal'>
7
+
8
+ /**
9
+ * An error that occurred during an HTTP request
10
+ */
11
+ export class HTTPError extends Error {
12
+ public name = 'HTTPError'
13
+ public client = 'OpenMeter'
14
+
15
+ constructor(
16
+ public message: string,
17
+ public type: string,
18
+ public title: string,
19
+ public status: number,
20
+ public url: string,
21
+ protected __raw?: Record<string, unknown>,
22
+ ) {
23
+ super(message)
24
+ }
25
+
26
+ static fromResponse(resp: {
27
+ response: Response
28
+ error?: UnexpectedProblemResponse
29
+ }): HTTPError {
30
+ if (
31
+ resp.response.headers.get('Content-Type') ===
32
+ 'application/problem+json' &&
33
+ resp.error
34
+ ) {
35
+ return new HTTPError(
36
+ `Request failed (${resp.response.url}) [${resp.response.status}]: ${resp.error.detail}`,
37
+ resp.error.type,
38
+ resp.error.title,
39
+ resp.error.status ?? resp.response.status,
40
+ resp.response.url,
41
+ resp.error,
42
+ )
43
+ }
44
+
45
+ return new HTTPError(
46
+ `Request failed (${resp.response.url}) [${resp.response.status}]: ${resp.response.statusText}`,
47
+ resp.response.statusText,
48
+ resp.response.statusText,
49
+ resp.response.status,
50
+ resp.response.url,
51
+ )
52
+ }
53
+
54
+ getField(key: string) {
55
+ return this.__raw?.[key]
56
+ }
57
+ }
58
+
59
+ /**
60
+ * Check if an error is an HTTPError
61
+ * @param error - The error to check
62
+ * @returns Whether the error is an HTTPError
63
+ */
64
+ export function isHTTPError(error: unknown): error is HTTPError {
65
+ return error instanceof HTTPError
66
+ }
api/client/javascript/src/client/customers.ts ADDED
@@ -0,0 +1,672 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import type { Client } from 'openapi-fetch'
2
+ import type { RequestOptions } from './common.js'
3
+ import type {
4
+ CreateStripeCustomerPortalSessionParams,
5
+ CustomerAppData,
6
+ CustomerCreate,
7
+ CustomerReplaceUpdate,
8
+ operations,
9
+ paths,
10
+ StripeCustomerAppDataBase,
11
+ } from './schemas.js'
12
+ import { transformResponse } from './utils.js'
13
+
14
+ /**
15
+ * Customers
16
+ * Manage customer subscription lifecycles and plan assignments.
17
+ */
18
+ export class Customers {
19
+ public apps: CustomerApps
20
+ public entitlementsV1: CustomerEntitlements
21
+ public entitlements: CustomerEntitlementsV2
22
+ public stripe: CustomerStripe
23
+
24
+ constructor(private client: Client<paths, `${string}/${string}`>) {
25
+ this.apps = new CustomerApps(client)
26
+ this.entitlementsV1 = new CustomerEntitlements(client)
27
+ this.entitlements = new CustomerEntitlementsV2(client)
28
+ this.stripe = new CustomerStripe(client)
29
+ }
30
+
31
+ /**
32
+ * Create a customer
33
+ * @param customer - The customer to create
34
+ * @param signal - An optional abort signal
35
+ * @returns The created customer
36
+ */
37
+ public async create(customer: CustomerCreate, options?: RequestOptions) {
38
+ const resp = await this.client.POST('/api/v1/customers', {
39
+ body: customer,
40
+ ...options,
41
+ })
42
+
43
+ return transformResponse(resp)
44
+ }
45
+
46
+ /**
47
+ * Get a customer by ID
48
+ * @param customerIdOrKey - The ID or Key of the customer
49
+ * @param signal - An optional abort signal
50
+ * @returns The customer
51
+ */
52
+ public async get(
53
+ customerIdOrKey: operations['getCustomer']['parameters']['path']['customerIdOrKey'],
54
+ options?: RequestOptions,
55
+ ) {
56
+ const resp = await this.client.GET('/api/v1/customers/{customerIdOrKey}', {
57
+ params: {
58
+ path: {
59
+ customerIdOrKey,
60
+ },
61
+ },
62
+ ...options,
63
+ })
64
+
65
+ return transformResponse(resp)
66
+ }
67
+
68
+ /**
69
+ * Update a customer
70
+ * @param customerIdOrKey - The ID or Key of the customer
71
+ * @param customer - The customer to update
72
+ * @param signal - An optional abort signal
73
+ * @returns The updated customer
74
+ */
75
+ public async update(
76
+ customerIdOrKey: operations['updateCustomer']['parameters']['path']['customerIdOrKey'],
77
+ customer: CustomerReplaceUpdate,
78
+ options?: RequestOptions,
79
+ ) {
80
+ const resp = await this.client.PUT('/api/v1/customers/{customerIdOrKey}', {
81
+ body: customer,
82
+ params: {
83
+ path: {
84
+ customerIdOrKey,
85
+ },
86
+ },
87
+ ...options,
88
+ })
89
+
90
+ return transformResponse(resp)
91
+ }
92
+
93
+ /**
94
+ * Delete a customer
95
+ * @param customerIdOrKey - The ID or Key of the customer
96
+ * @param signal - An optional abort signal
97
+ * @returns The deleted customer
98
+ */
99
+ public async delete(
100
+ customerIdOrKey: operations['deleteCustomer']['parameters']['path']['customerIdOrKey'],
101
+ options?: RequestOptions,
102
+ ) {
103
+ const resp = await this.client.DELETE(
104
+ '/api/v1/customers/{customerIdOrKey}',
105
+ {
106
+ params: {
107
+ path: {
108
+ customerIdOrKey,
109
+ },
110
+ },
111
+ ...options,
112
+ },
113
+ )
114
+
115
+ return transformResponse(resp)
116
+ }
117
+
118
+ /**
119
+ * List customers
120
+ * @param signal - An optional abort signal
121
+ * @returns The list of customers
122
+ */
123
+ public async list(
124
+ query?: operations['listCustomers']['parameters']['query'],
125
+ options?: RequestOptions,
126
+ ) {
127
+ const resp = await this.client.GET('/api/v1/customers', {
128
+ params: {
129
+ query,
130
+ },
131
+ ...options,
132
+ })
133
+
134
+ return transformResponse(resp)
135
+ }
136
+
137
+ /**
138
+ * Get customer access
139
+ * @param customerIdOrKey - The ID or Key of the customer
140
+ * @param options - Optional request options
141
+ * @returns The customer access information
142
+ */
143
+ public async getAccess(
144
+ customerIdOrKey: operations['getCustomerAccess']['parameters']['path']['customerIdOrKey'],
145
+ options?: RequestOptions,
146
+ ) {
147
+ const resp = await this.client.GET(
148
+ '/api/v1/customers/{customerIdOrKey}/access',
149
+ {
150
+ params: {
151
+ path: {
152
+ customerIdOrKey,
153
+ },
154
+ },
155
+ ...options,
156
+ },
157
+ )
158
+
159
+ return transformResponse(resp)
160
+ }
161
+
162
+ /**
163
+ * List customer subscriptions
164
+ * @param customerIdOrKey - The ID or key of the customer
165
+ * @param query - The query parameters
166
+ * @param signal - An optional abort signal
167
+ * @returns The list of customer subscriptions
168
+ */
169
+ public async listSubscriptions(
170
+ customerIdOrKey: operations['listCustomerSubscriptions']['parameters']['path']['customerIdOrKey'],
171
+ query?: operations['listCustomerSubscriptions']['parameters']['query'],
172
+ options?: RequestOptions,
173
+ ) {
174
+ const resp = await this.client.GET(
175
+ '/api/v1/customers/{customerIdOrKey}/subscriptions',
176
+ {
177
+ params: { path: { customerIdOrKey }, query },
178
+ ...options,
179
+ },
180
+ )
181
+
182
+ return transformResponse(resp)
183
+ }
184
+ }
185
+
186
+ /**
187
+ * Customer Apps
188
+ * Manage customer apps.
189
+ */
190
+ export class CustomerApps {
191
+ constructor(private client: Client<paths, `${string}/${string}`>) {}
192
+
193
+ /**
194
+ * Upsert customer app data
195
+ * @param customerIdOrKey - The ID or Key of the customer
196
+ * @param appData - The app data to upsert
197
+ * @param signal - An optional abort signal
198
+ * @returns The upserted app data
199
+ */
200
+ public async upsert(
201
+ customerIdOrKey: operations['upsertCustomerAppData']['parameters']['path']['customerIdOrKey'],
202
+ appData: CustomerAppData[],
203
+ options?: RequestOptions,
204
+ ) {
205
+ const resp = await this.client.PUT(
206
+ '/api/v1/customers/{customerIdOrKey}/apps',
207
+ {
208
+ body: appData,
209
+ params: {
210
+ path: {
211
+ customerIdOrKey,
212
+ },
213
+ },
214
+ ...options,
215
+ },
216
+ )
217
+
218
+ return transformResponse(resp)
219
+ }
220
+
221
+ /**
222
+ * List customer app data
223
+ * @param customerIdOrKey - The ID or key of the customer
224
+ * @param query - The query parameters
225
+ * @param signal - An optional abort signal
226
+ * @returns The list of customer app data
227
+ */
228
+ public async list(
229
+ customerIdOrKey: operations['listCustomerAppData']['parameters']['path']['customerIdOrKey'],
230
+ query?: operations['listCustomerAppData']['parameters']['query'],
231
+ options?: RequestOptions,
232
+ ) {
233
+ const resp = await this.client.GET(
234
+ '/api/v1/customers/{customerIdOrKey}/apps',
235
+ {
236
+ params: {
237
+ path: { customerIdOrKey },
238
+ query,
239
+ },
240
+ ...options,
241
+ },
242
+ )
243
+
244
+ return transformResponse(resp)
245
+ }
246
+
247
+ /**
248
+ * Delete customer app data
249
+ * @param customerIdOrKey - The ID or key of the customer
250
+ * @param appId - The ID of the app
251
+ * @param signal - An optional abort signal
252
+ * @returns The deleted customer app data
253
+ */
254
+ public async delete(
255
+ customerIdOrKey: operations['deleteCustomerAppData']['parameters']['path']['customerIdOrKey'],
256
+ appId: operations['deleteCustomerAppData']['parameters']['path']['appId'],
257
+ options?: RequestOptions,
258
+ ) {
259
+ const resp = await this.client.DELETE(
260
+ '/api/v1/customers/{customerIdOrKey}/apps/{appId}',
261
+ {
262
+ params: { path: { appId, customerIdOrKey } },
263
+ ...options,
264
+ },
265
+ )
266
+
267
+ return transformResponse(resp)
268
+ }
269
+ }
270
+
271
+ /**
272
+ * Customer Stripe
273
+ * Manage customer Stripe data.
274
+ */
275
+ export class CustomerStripe {
276
+ constructor(private client: Client<paths, `${string}/${string}`>) {}
277
+
278
+ /**
279
+ * Upsert customer stripe app data
280
+ * @param customerIdOrKey - The ID or Key of the customer
281
+ * @param appData - The app data to upsert
282
+ * @param signal - An optional abort signal
283
+ * @returns The upserted customer stripe app data
284
+ */
285
+ public async upsert(
286
+ customerIdOrKey: operations['upsertCustomerStripeAppData']['parameters']['path']['customerIdOrKey'],
287
+ stripeAppData: StripeCustomerAppDataBase,
288
+ options?: RequestOptions,
289
+ ) {
290
+ const resp = await this.client.PUT(
291
+ '/api/v1/customers/{customerIdOrKey}/stripe',
292
+ {
293
+ body: stripeAppData,
294
+ params: {
295
+ path: {
296
+ customerIdOrKey,
297
+ },
298
+ },
299
+ ...options,
300
+ },
301
+ )
302
+
303
+ return transformResponse(resp)
304
+ }
305
+
306
+ /**
307
+ * Get customer stripe app data
308
+ * @param customerIdOrKey - The ID or key of the customer
309
+ * @param query - The query parameters
310
+ * @param signal - An optional abort signal
311
+ * @returns The customer stripe app data
312
+ */
313
+ public async get(
314
+ customerIdOrKey: operations['getCustomerStripeAppData']['parameters']['path']['customerIdOrKey'],
315
+ options?: RequestOptions,
316
+ ) {
317
+ const resp = await this.client.GET(
318
+ '/api/v1/customers/{customerIdOrKey}/stripe',
319
+ {
320
+ params: {
321
+ path: { customerIdOrKey },
322
+ },
323
+ ...options,
324
+ },
325
+ )
326
+
327
+ return transformResponse(resp)
328
+ }
329
+
330
+ /**
331
+ * Create a Stripe customer portal session
332
+ * @param customerIdOrKey - The ID or Key of the customer
333
+ * @param params - The parameters for creating a Stripe customer portal session
334
+ * @param signal - An optional abort signal
335
+ * @returns The Stripe customer portal session
336
+ */
337
+ public async createPortalSession(
338
+ customerIdOrKey: operations['createCustomerStripePortalSession']['parameters']['path']['customerIdOrKey'],
339
+ params: CreateStripeCustomerPortalSessionParams,
340
+ options?: RequestOptions,
341
+ ) {
342
+ const resp = await this.client.POST(
343
+ '/api/v1/customers/{customerIdOrKey}/stripe/portal',
344
+ {
345
+ body: params,
346
+ params: {
347
+ path: {
348
+ customerIdOrKey,
349
+ },
350
+ },
351
+ ...options,
352
+ },
353
+ )
354
+
355
+ return transformResponse(resp)
356
+ }
357
+ }
358
+
359
+ /**
360
+ * Customer Entitlements
361
+ */
362
+ export class CustomerEntitlements {
363
+ constructor(private client: Client<paths, `${string}/${string}`>) {}
364
+
365
+ /**
366
+ * Get the value of an entitlement for a customer
367
+ * @param customerIdOrKey - The ID or Key of the customer
368
+ * @param featureKey - The key of the feature
369
+ * @param signal - An optional abort signal
370
+ * @returns The value of the entitlement
371
+ */
372
+ public async value(
373
+ customerIdOrKey: operations['getCustomerEntitlementValue']['parameters']['path']['customerIdOrKey'],
374
+ featureKey: operations['getCustomerEntitlementValue']['parameters']['path']['featureKey'],
375
+ options?: RequestOptions,
376
+ ) {
377
+ const resp = await this.client.GET(
378
+ '/api/v1/customers/{customerIdOrKey}/entitlements/{featureKey}/value',
379
+ {
380
+ params: { path: { customerIdOrKey, featureKey } },
381
+ ...options,
382
+ },
383
+ )
384
+
385
+ return transformResponse(resp)
386
+ }
387
+ }
388
+
389
+ /**
390
+ * Customer Entitlements V2
391
+ */
392
+ export class CustomerEntitlementsV2 {
393
+ constructor(private client: Client<paths, `${string}/${string}`>) {}
394
+
395
+ /**
396
+ * List all entitlements for a customer
397
+ * @param customerIdOrKey - The ID or Key of the customer
398
+ * @param options - Request options including query parameters
399
+ * @returns List of customer entitlements
400
+ */
401
+ public async list(
402
+ customerIdOrKey: operations['listCustomerEntitlementsV2']['parameters']['path']['customerIdOrKey'],
403
+ options?: RequestOptions & {
404
+ query?: operations['listCustomerEntitlementsV2']['parameters']['query']
405
+ },
406
+ ) {
407
+ const resp = await this.client.GET(
408
+ '/api/v2/customers/{customerIdOrKey}/entitlements',
409
+ {
410
+ params: {
411
+ path: { customerIdOrKey },
412
+ query: options?.query,
413
+ },
414
+ ...options,
415
+ },
416
+ )
417
+
418
+ return transformResponse(resp)
419
+ }
420
+
421
+ /**
422
+ * Create a customer entitlement
423
+ * @param customerIdOrKey - The ID or Key of the customer
424
+ * @param entitlement - The entitlement data to create
425
+ * @param options - Request options
426
+ * @returns The created entitlement
427
+ */
428
+ public async create(
429
+ customerIdOrKey: operations['createCustomerEntitlementV2']['parameters']['path']['customerIdOrKey'],
430
+ entitlement: operations['createCustomerEntitlementV2']['requestBody']['content']['application/json'],
431
+ options?: RequestOptions,
432
+ ) {
433
+ const resp = await this.client.POST(
434
+ '/api/v2/customers/{customerIdOrKey}/entitlements',
435
+ {
436
+ body: entitlement,
437
+ params: {
438
+ path: { customerIdOrKey },
439
+ },
440
+ ...options,
441
+ },
442
+ )
443
+
444
+ return transformResponse(resp)
445
+ }
446
+
447
+ /**
448
+ * Get a specific customer entitlement
449
+ * @param customerIdOrKey - The ID or Key of the customer
450
+ * @param entitlementIdOrFeatureKey - The ID or feature key of the entitlement
451
+ * @param options - Request options
452
+ * @returns The entitlement
453
+ */
454
+ public async get(
455
+ customerIdOrKey: operations['getCustomerEntitlementV2']['parameters']['path']['customerIdOrKey'],
456
+ entitlementIdOrFeatureKey: operations['getCustomerEntitlementV2']['parameters']['path']['entitlementIdOrFeatureKey'],
457
+ options?: RequestOptions,
458
+ ) {
459
+ const resp = await this.client.GET(
460
+ '/api/v2/customers/{customerIdOrKey}/entitlements/{entitlementIdOrFeatureKey}',
461
+ {
462
+ params: {
463
+ path: { customerIdOrKey, entitlementIdOrFeatureKey },
464
+ },
465
+ ...options,
466
+ },
467
+ )
468
+
469
+ return transformResponse(resp)
470
+ }
471
+
472
+ /**
473
+ * Delete a customer entitlement
474
+ * @param customerIdOrKey - The ID or Key of the customer
475
+ * @param entitlementIdOrFeatureKey - The ID or feature key of the entitlement
476
+ * @param options - Request options
477
+ * @returns The deletion response
478
+ */
479
+ public async delete(
480
+ customerIdOrKey: operations['deleteCustomerEntitlementV2']['parameters']['path']['customerIdOrKey'],
481
+ entitlementIdOrFeatureKey: operations['deleteCustomerEntitlementV2']['parameters']['path']['entitlementIdOrFeatureKey'],
482
+ options?: RequestOptions,
483
+ ) {
484
+ const resp = await this.client.DELETE(
485
+ '/api/v2/customers/{customerIdOrKey}/entitlements/{entitlementIdOrFeatureKey}',
486
+ {
487
+ params: {
488
+ path: { customerIdOrKey, entitlementIdOrFeatureKey },
489
+ },
490
+ ...options,
491
+ },
492
+ )
493
+
494
+ return transformResponse(resp)
495
+ }
496
+
497
+ /**
498
+ * Override a customer entitlement
499
+ * @param customerIdOrKey - The ID or Key of the customer
500
+ * @param entitlementIdOrFeatureKey - The ID or feature key of the entitlement
501
+ * @param entitlement - The new entitlement data
502
+ * @param options - Request options
503
+ * @returns The overridden entitlement
504
+ */
505
+ public async override(
506
+ customerIdOrKey: operations['overrideCustomerEntitlementV2']['parameters']['path']['customerIdOrKey'],
507
+ entitlementIdOrFeatureKey: operations['overrideCustomerEntitlementV2']['parameters']['path']['entitlementIdOrFeatureKey'],
508
+ entitlement: operations['overrideCustomerEntitlementV2']['requestBody']['content']['application/json'],
509
+ options?: RequestOptions,
510
+ ) {
511
+ const resp = await this.client.PUT(
512
+ '/api/v2/customers/{customerIdOrKey}/entitlements/{entitlementIdOrFeatureKey}/override',
513
+ {
514
+ body: entitlement,
515
+ params: {
516
+ path: { customerIdOrKey, entitlementIdOrFeatureKey },
517
+ },
518
+ ...options,
519
+ },
520
+ )
521
+
522
+ return transformResponse(resp)
523
+ }
524
+
525
+ /**
526
+ * List grants for a customer entitlement
527
+ * @param customerIdOrKey - The ID or Key of the customer
528
+ * @param entitlementIdOrFeatureKey - The ID or feature key of the entitlement
529
+ * @param options - Request options including query parameters
530
+ * @returns List of entitlement grants
531
+ */
532
+ public async listGrants(
533
+ customerIdOrKey: operations['listCustomerEntitlementGrantsV2']['parameters']['path']['customerIdOrKey'],
534
+ entitlementIdOrFeatureKey: operations['listCustomerEntitlementGrantsV2']['parameters']['path']['entitlementIdOrFeatureKey'],
535
+ options?: RequestOptions & {
536
+ query?: operations['listCustomerEntitlementGrantsV2']['parameters']['query']
537
+ },
538
+ ) {
539
+ const resp = await this.client.GET(
540
+ '/api/v2/customers/{customerIdOrKey}/entitlements/{entitlementIdOrFeatureKey}/grants',
541
+ {
542
+ params: {
543
+ path: { customerIdOrKey, entitlementIdOrFeatureKey },
544
+ query: options?.query,
545
+ },
546
+ ...options,
547
+ },
548
+ )
549
+
550
+ return transformResponse(resp)
551
+ }
552
+
553
+ /**
554
+ * Create a grant for a customer entitlement
555
+ * @param customerIdOrKey - The ID or Key of the customer
556
+ * @param entitlementIdOrFeatureKey - The ID or feature key of the entitlement
557
+ * @param grant - The grant data to create
558
+ * @param options - Request options
559
+ * @returns The created grant
560
+ */
561
+ public async createGrant(
562
+ customerIdOrKey: operations['createCustomerEntitlementGrantV2']['parameters']['path']['customerIdOrKey'],
563
+ entitlementIdOrFeatureKey: operations['createCustomerEntitlementGrantV2']['parameters']['path']['entitlementIdOrFeatureKey'],
564
+ grant: operations['createCustomerEntitlementGrantV2']['requestBody']['content']['application/json'],
565
+ options?: RequestOptions,
566
+ ) {
567
+ const resp = await this.client.POST(
568
+ '/api/v2/customers/{customerIdOrKey}/entitlements/{entitlementIdOrFeatureKey}/grants',
569
+ {
570
+ body: grant,
571
+ params: {
572
+ path: { customerIdOrKey, entitlementIdOrFeatureKey },
573
+ },
574
+ ...options,
575
+ },
576
+ )
577
+
578
+ return transformResponse(resp)
579
+ }
580
+
581
+ /**
582
+ * Get the value of a customer entitlement
583
+ * @param customerIdOrKey - The ID or Key of the customer
584
+ * @param entitlementIdOrFeatureKey - The ID or feature key of the entitlement
585
+ * @param options - Request options including query parameters
586
+ * @returns The entitlement value
587
+ */
588
+ public async value(
589
+ customerIdOrKey: operations['getCustomerEntitlementValueV2']['parameters']['path']['customerIdOrKey'],
590
+ entitlementIdOrFeatureKey: operations['getCustomerEntitlementValueV2']['parameters']['path']['entitlementIdOrFeatureKey'],
591
+ options?: RequestOptions & {
592
+ query?: operations['getCustomerEntitlementValueV2']['parameters']['query']
593
+ },
594
+ ) {
595
+ const resp = await this.client.GET(
596
+ '/api/v2/customers/{customerIdOrKey}/entitlements/{entitlementIdOrFeatureKey}/value',
597
+ {
598
+ params: {
599
+ path: { customerIdOrKey, entitlementIdOrFeatureKey },
600
+ query: options?.query,
601
+ },
602
+ ...options,
603
+ },
604
+ )
605
+
606
+ return transformResponse(resp)
607
+ }
608
+
609
+ /**
610
+ * Get the history of a customer entitlement
611
+ * @param customerIdOrKey - The ID or Key of the customer
612
+ * @param entitlementIdOrFeatureKey - The ID or feature key of the entitlement
613
+ * @param windowSize - The window size for the history
614
+ * @param options - Request options including query parameters
615
+ * @returns The entitlement history
616
+ */
617
+ public async history(
618
+ customerIdOrKey: operations['getCustomerEntitlementHistoryV2']['parameters']['path']['customerIdOrKey'],
619
+ entitlementIdOrFeatureKey: operations['getCustomerEntitlementHistoryV2']['parameters']['path']['entitlementIdOrFeatureKey'],
620
+ windowSize: operations['getCustomerEntitlementHistoryV2']['parameters']['query']['windowSize'],
621
+ options?: RequestOptions & {
622
+ query?: Omit<
623
+ operations['getCustomerEntitlementHistoryV2']['parameters']['query'],
624
+ 'windowSize'
625
+ >
626
+ },
627
+ ) {
628
+ const resp = await this.client.GET(
629
+ '/api/v2/customers/{customerIdOrKey}/entitlements/{entitlementIdOrFeatureKey}/history',
630
+ {
631
+ params: {
632
+ path: { customerIdOrKey, entitlementIdOrFeatureKey },
633
+ query: {
634
+ windowSize,
635
+ ...options?.query,
636
+ },
637
+ },
638
+ ...options,
639
+ },
640
+ )
641
+
642
+ return transformResponse(resp)
643
+ }
644
+
645
+ /**
646
+ * Reset the usage of a customer entitlement
647
+ * @param customerIdOrKey - The ID or Key of the customer
648
+ * @param entitlementIdOrFeatureKey - The ID or feature key of the entitlement
649
+ * @param reset - The reset data
650
+ * @param options - Request options
651
+ * @returns The reset response
652
+ */
653
+ public async resetUsage(
654
+ customerIdOrKey: operations['resetCustomerEntitlementUsageV2']['parameters']['path']['customerIdOrKey'],
655
+ entitlementIdOrFeatureKey: operations['resetCustomerEntitlementUsageV2']['parameters']['path']['entitlementIdOrFeatureKey'],
656
+ reset: operations['resetCustomerEntitlementUsageV2']['requestBody']['content']['application/json'],
657
+ options?: RequestOptions,
658
+ ) {
659
+ const resp = await this.client.POST(
660
+ '/api/v2/customers/{customerIdOrKey}/entitlements/{entitlementIdOrFeatureKey}/reset',
661
+ {
662
+ body: reset,
663
+ params: {
664
+ path: { customerIdOrKey, entitlementIdOrFeatureKey },
665
+ },
666
+ ...options,
667
+ },
668
+ )
669
+
670
+ return transformResponse(resp)
671
+ }
672
+ }
api/client/javascript/src/client/debug.ts ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import type { Client } from 'openapi-fetch'
2
+ import type { RequestOptions } from './common.js'
3
+ import type { paths } from './schemas.js'
4
+ import { transformResponse } from './utils.js'
5
+
6
+ /**
7
+ * Debug utilities for OpenMeter
8
+ */
9
+ export class Debug {
10
+ constructor(private client: Client<paths, `${string}/${string}`>) {}
11
+
12
+ /**
13
+ * Get event metrics
14
+ * @description Returns debug metrics (in OpenMetrics format) like the number of ingested events since mindnight UTC.
15
+ * @param options - The request options
16
+ * @returns The debug metrics
17
+ */
18
+ public async getMetrics(options?: RequestOptions) {
19
+ const resp = await this.client.GET('/api/v1/debug/metrics', {
20
+ ...options,
21
+ })
22
+
23
+ return transformResponse(resp)
24
+ }
25
+ }