@ponharu/pkgflare 0.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,114 @@
1
+ # Operating a registry
2
+
3
+ This guide covers the deployment project that installs `@ponharu/pkgflare`. Start with the [README](../README.md) to create a registry and publish a package.
4
+
5
+ ## Cloudflare authentication and domains
6
+
7
+ Cloudflare deployment credentials manage infrastructure. Registry read and publish tokens authenticate npm clients. They are separate credentials: never put a Cloudflare API token in `.npmrc`.
8
+
9
+ For interactive deployment, use `npx wrangler login`. For CI, store `CLOUDFLARE_API_TOKEN` in the CI secret store and set `accountId` in `pkgflare.config.ts` or supply `CLOUDFLARE_ACCOUNT_ID`. Explicit account selection avoids needing to enumerate accessible accounts. Conflicting account IDs in the environment, configuration, and saved state stop deployment.
10
+
11
+ A deployment token needs these account permissions for the operations pkgflare performs:
12
+
13
+ | Permission | Purpose |
14
+ | ------------------------ | ---------------------------------------------------- |
15
+ | Workers Scripts: Edit | Inspect and deploy the Worker and manage its Secrets |
16
+ | D1: Edit | Find or create the database and apply migrations |
17
+ | Workers R2 Storage: Edit | Inspect or create the package bucket |
18
+
19
+ Scope the token to the deployment account. These are Cloudflare account API permissions; an R2 S3 object-access credential alone cannot provision this registry. See Cloudflare's [permission reference](https://developers.cloudflare.com/fundamentals/api/reference/permissions/) for current permission names and scopes.
20
+
21
+ For a custom hostname, add `hostname: "packages.example.com"` to the config. The hostname must belong to an active Cloudflare zone in the same account. Give the deployment credentials Zone: Read and Workers Routes: Edit access for that zone in addition to the account permissions. Wrangler configures the custom domain; pkgflare disables its `workers.dev` endpoint when a hostname is configured. Follow Cloudflare's [custom domain requirements](https://developers.cloudflare.com/workers/configuration/routing/custom-domains/) when choosing the hostname.
22
+
23
+ Without `hostname`, use the `workers.dev` URL printed by deploy. The account must have a Workers subdomain configured. Keep the chosen registry hostname stable: `.npmrc` entries and existing lockfiles can contain absolute tarball URLs.
24
+
25
+ ## Deployment state
26
+
27
+ Use a private deployment repository and commit:
28
+
29
+ - `package.json`, the package-manager lockfile, and `pkgflare.config.ts`
30
+ - `.pkgflare/wrangler.json`, `.pkgflare/ownership.json`, and `.pkgflare/.gitignore`
31
+
32
+ The two JSON files contain account/resource identifiers, registry configuration, and Worker ownership state. They contain no Secret values, but expose deployment details. The generated `.gitignore` excludes only copied runtime and migrations; do not ignore the entire `.pkgflare` directory. Do not manually change resource identifiers to point at a different registry.
33
+
34
+ Keep token files out of Git. If using the secrets-file option below, add `.secrets.env` to the deployment project's `.gitignore` before creating it.
35
+
36
+ Run only one deployment at a time for each registry. The CLI does not lock state across processes or CI jobs; configure CI concurrency accordingly. Keep the same saved state available on subsequent runs, including any changes after a failed deployment. State files identify resources; they are not backups of package data.
37
+
38
+ ## CI deployment and updates
39
+
40
+ Install from the committed lockfile, provide Cloudflare credentials through CI secrets, and run:
41
+
42
+ ```sh
43
+ npm ci
44
+ npx pkgflare deploy
45
+ ```
46
+
47
+ For initial token provisioning, a private Wrangler-compatible dotenv file can contain the configured bindings:
48
+
49
+ ```dotenv
50
+ PKGFLARE_READ_TOKEN=<read-token>
51
+ PKGFLARE_PUBLISH_TOKEN=<publish-token>
52
+ ```
53
+
54
+ Generate the values with `pkgflare token generate` and populate the file from your secret store. The placeholders above are not usable tokens. Deploy with:
55
+
56
+ ```sh
57
+ npx pkgflare deploy --secrets-file .secrets.env
58
+ ```
59
+
60
+ Wrangler applies this file additively; omitted Secrets are preserved. Remove temporary secret files after use. Subsequent deployments can omit `--secrets-file` when the Secrets already exist.
61
+
62
+ To update pkgflare, review the version's changes, update the dependency and lockfile, and run deploy. The CLI applies pending migrations before replacing the Worker. Migrations must support the previous Worker so a failed Worker deployment can be retried. This does not guarantee arbitrary downgrades to older pkgflare versions; do not remove migration history as a rollback technique.
63
+
64
+ ## Recovering a failed deployment
65
+
66
+ If a command fails, fix the reported cause and rerun it with the same configuration and saved state. A failure does not undo resources or migrations already created or applied.
67
+
68
+ If state is missing and a matching resource already exists, pkgflare stops rather than assuming ownership. Verify the account and all three resource names before adoption:
69
+
70
+ | Resource | Name for `name: "acme-registry"` |
71
+ | -------- | -------------------------------- |
72
+ | Worker | `acme-registry` |
73
+ | D1 | `acme-registry-metadata` |
74
+ | R2 | `acme-registry-packages` |
75
+
76
+ Then run:
77
+
78
+ ```sh
79
+ npx pkgflare deploy --adopt-existing
80
+ ```
81
+
82
+ Adoption permits applying migrations and deploying over existing resources; it does not import or validate arbitrary registry data. Use it only for resources intended for this pkgflare deployment. A successful Worker creation whose result was lost can also require adoption on retry. Saved D1 and R2 identifiers are reused.
83
+
84
+ ## Tokens and access
85
+
86
+ All tokens cover the whole registry. Publish permission includes read access and dist-tag changes. Use read-only tokens for installation jobs and keep publish tokens limited to package release jobs.
87
+
88
+ For rotation, add a new binding while retaining the old one, deploy, register the new Secret, and switch clients to it. Once clients have switched, remove the old binding and deploy again, then delete the old Secret. Removing a configured binding revokes that token after deployment. For urgent revocation, delete its Secret through Cloudflare; other configured tokens remain usable. Revocation cannot retract package bytes already downloaded by clients.
89
+
90
+ ## Backups and restoration
91
+
92
+ pkgflare does not provide an automated backup or restore command. A recoverable registry needs the D1 metadata, every R2 object referenced by that metadata, the deployment configuration/state, and access to the token secret store. Back up D1 and R2 separately using Cloudflare or compatible storage tooling. D1 [Time Travel](https://developers.cloudflare.com/d1/reference/time-travel/) covers database recovery, not R2 objects or Worker Secrets.
93
+
94
+ For a coordinated snapshot, pause publishing and tag changes, wait for in-flight writes to finish, export D1, and copy the R2 objects before resuming writes. Preserve object keys and bytes: the D1 version rows reference those exact keys. Store backups with access controls appropriate for private source packages.
95
+
96
+ Before reopening a restored registry, verify that every restored version's `tarball_key` exists in R2 and matches its stored size and integrity. Restore compatible runtime/configuration and Secrets, then check metadata and a cold-cache install. An older D1 snapshot can omit newer versions even if their objects remain in R2; confirm the intended recovery point. Do not treat successful database restoration alone as complete registry recovery.
97
+
98
+ Failed publishing may leave unreachable objects. Automatic orphan collection is not implemented. Do not apply age-based deletion to the whole bucket: old referenced tarballs remain valid indefinitely. Listing an object without a current D1 reference is also insufficient proof that it is safe to delete while publishing is active.
99
+
100
+ ## Troubleshooting
101
+
102
+ | Symptom | What to check |
103
+ | ----------------------- | ---------------------------------------------------------------------------------------------------------- |
104
+ | 401 | `NPM_TOKEN` is set and the `.npmrc` authentication hostname matches the registry URL |
105
+ | 403 | The token matches a configured Secret and grants the required permission |
106
+ | 409 on publish | The version already exists; inspect it before deciding whether to publish a new version |
107
+ | 413 | Publish metadata exceeds 1 MiB, or the encoded request exceeds Cloudflare's limit |
108
+ | 503 | Retry after checking storage availability; a publish result may be uncertain, so inspect the version first |
109
+ | Existing resource error | Restore saved state or verify ownership before explicit adoption |
110
+ | Account mismatch | Configuration, environment, and saved state select the same Cloudflare account |
111
+
112
+ Worker responses include `x-pkgflare-request-id`. Use that ID to correlate unexpected errors with Worker logs. Share the request ID and sanitized reproduction steps when reporting a problem; keep tokens and private package contents out of public issues. Cloudflare-generated responses may not include this header.
113
+
114
+ Large publishes consume CPU for parsing, Base64 decoding, and hashing, even though tarball memory use is bounded. Consult [Workers limits](https://developers.cloudflare.com/workers/platform/limits/) and measure representative package sizes on the intended plan. Request-body limits apply to the encoded JSON request, which is larger than the tarball.
@@ -0,0 +1,114 @@
1
+ # pkgflare v1 specification
2
+
3
+ ## Product boundary
4
+
5
+ pkgflare deploys a scoped private npm-compatible Registry into the user's own Cloudflare account. It provides a CLI, Worker runtime, D1 migrations, R2 bindings, configuration types, and compatibility tests. It is not a hosted service and does not provide a web UI, public Registry proxy, user database, team model, or a custom package client.
6
+
7
+ Users publish, inspect, install, promote, and roll back packages with standard npm-compatible commands. Registry infrastructure and package data remain in the user's Cloudflare account.
8
+
9
+ ## Supported clients
10
+
11
+ The compatibility baseline is:
12
+
13
+ - publish: npm and Bun
14
+ - metadata and install: npm, pnpm, Yarn Classic, and Bun
15
+ - dist-tag mutation: npm
16
+
17
+ Compatibility claims apply to the versions exercised by the project's end-to-end test. Other npm-compatible clients and Yarn Berry may work but are not part of the v1 guarantee until tested.
18
+
19
+ ## Configuration
20
+
21
+ The deployment repository contains `pkgflare.config.ts` and a package-manager lockfile. The configuration defines:
22
+
23
+ - a lowercase deployment name of at most 54 characters
24
+ - one or more allowed npm scopes
25
+ - an optional Cloudflare account ID, required when Wrangler credentials expose multiple accounts
26
+ - an optional custom hostname
27
+ - Cloudflare Secret binding names and their `read` or `publish` permissions
28
+
29
+ Secret values are never stored in configuration or deployment state. A `publish` token also grants read access. Every token applies to all packages in the registry; per-scope and per-package authorization are not supported.
30
+
31
+ ## Deployment state and resources
32
+
33
+ `pkgflare deploy` creates a Worker, D1 database, and R2 bucket with deterministic names. The generated `.pkgflare/wrangler.json` stores the resolved account ID and exact resource identifiers. `.pkgflare/ownership.json` records that pkgflare created or explicitly adopted the Worker. Neither file contains Secret values, and both must be preserved for subsequent deployments.
34
+
35
+ If ownership state is absent and a deterministic resource name already exists, deployment stops. Existing resources are adopted only when the user reruns with `--adopt-existing` after verifying the active account and resources. More than one matching D1 database is always an error. Conflicting account IDs in configuration, deployment state, and the environment are rejected; access to the selected account is verified by the targeted Wrangler operations rather than requiring account-list permission.
36
+
37
+ Every packaged SQL migration is copied into deployment state. Pending migrations run before the new Worker is deployed. Migrations are append-only and use expand/contract changes so the previous Worker remains compatible if migration succeeds but deployment fails. Both migration and deployment failures stop the command and are safe to retry.
38
+
39
+ If the Worker was created but the deploy result was lost before ownership could be recorded, the retry treats it as an existing unowned Worker and requires `--adopt-existing`. Previously saved D1 and R2 identifiers are reused rather than recreated.
40
+
41
+ ## Registry operations
42
+
43
+ v1 accepts scoped names up to 214 characters. Scope segments contain lowercase letters, digits, `.`, `_`, or `-` and start and end with a letter or digit. Package segments contain the same characters and may begin or end with `-` or `_`, but may not begin with `.`. Unscoped names are not supported.
44
+
45
+ The Registry supports:
46
+
47
+ - npm CouchDB-style package publish
48
+ - complete package metadata and version metadata
49
+ - immutable tarball GET and HEAD with byte ranges
50
+ - dist-tag GET, PUT, and DELETE used by `npm dist-tag`
51
+ - authenticated ping
52
+
53
+ The login/adduser, unpublish, deprecate, search, and audit APIs are not implemented.
54
+
55
+ Package versions are immutable. Dist-tags are independently mutable and may point to any existing version, enabling promotion and rollback without republishing bytes.
56
+
57
+ ## Streaming publish pipeline
58
+
59
+ Standard npm clients embed the tarball as Base64 in a JSON attachment. pkgflare preserves that wire format and does not require a signed-upload protocol or custom publish command.
60
+
61
+ The Worker incrementally tokenizes strict UTF-8 JSON. It rejects duplicate object keys, invalid escapes, invalid or non-canonical Base64, more than one attachment, metadata larger than 1 MiB, and nesting deeper than 128 levels. Property order and input chunk boundaries do not affect the result.
62
+
63
+ Attachment data is decoded incrementally into uniform 5 MiB R2 multipart parts. The final part may be smaller. SHA-1 and SHA-512 are calculated with streaming digest APIs under backpressure. The complete tarball and Base64 string are never retained in Worker memory.
64
+
65
+ Each attempt uses a random R2 object key. After the complete request, attachment length, manifest, scope, version, tags, and client-provided checksums have been validated, the multipart upload is completed. D1 then atomically inserts package metadata, the immutable version row, and initial dist-tags. The D1 primary key on package name and version determines the sole winner of concurrent attempts.
66
+
67
+ If D1 reports an error after R2 completion, pkgflare reads the version back:
68
+
69
+ - the same object key and checksums mean the attempt committed successfully
70
+ - another object key means the attempt lost the immutable-version race
71
+ - no row or a failed read-back means the outcome is retryable or unknown
72
+
73
+ An object is deleted immediately only when it is known to be a losing attempt. Otherwise it is retained as an unreachable orphan. Reads resolve the D1 row first, so incomplete and orphaned uploads are never installable. Automatic orphan garbage collection is outside v1.
74
+
75
+ ## Size contract
76
+
77
+ pkgflare imposes no tarball-size ceiling below Cloudflare's platform limits. Memory use is bounded by parser metadata, one multipart part, stream queues, and runtime overhead rather than total tarball size.
78
+
79
+ The complete Base64-encoded publish request remains subject to Cloudflare's request-body limit. Cloudflare can reject an oversized request before the Worker executes, in which case pkgflare cannot customize the response. Processing is also subject to the Worker's CPU limit, which is governed separately from the request-body limit. The 1 MiB metadata and 128-level nesting limits are security limits, not tarball limits.
80
+
81
+ Supported publish sizes depend on the deployment plan and request processing cost; pkgflare does not promise a universal maximum package size. See [platform limits](https://developers.cloudflare.com/workers/platform/limits/).
82
+
83
+ ## Authentication and rotation
84
+
85
+ Every Registry operation requires a Bearer token. Tokens are compared against configured Cloudflare Secret bindings without logging request credentials. Missing bindings do not disable other valid bindings.
86
+
87
+ Rotation uses overlapping bindings: add the new binding, deploy, register and distribute the new Secret, remove the old binding and deploy, then delete the old Secret. Both tokens work during the overlap; the removed token stops working after the second deployment.
88
+
89
+ ## Consistent reads
90
+
91
+ D1 is the visibility boundary. Package metadata reads obtain versions and dist-tags in one D1 batch so a publish cannot produce a response combining two different snapshots. A tarball is returned only when referenced by a committed version row. A missing referenced object produces a retryable storage-consistency response.
92
+
93
+ ## Diagnostics and privacy
94
+
95
+ Every Worker response includes an opaque request ID. Unexpected failures emit a structured log containing the request ID, operation, and error type. Error logs and error responses never include Bearer tokens, Authorization headers, Secret values, request bodies, manifests, or tarball contents.
96
+
97
+ Input errors return 400, authorization failures return 401 or 403, immutable conflicts return 409, application-enforced size limits return 413, and transient or uncertain storage failures return 503. Platform-generated errors may use Cloudflare's response format.
98
+
99
+ ## Acceptance criteria
100
+
101
+ v1 is accepted when automated tests demonstrate:
102
+
103
+ 1. first deployment, repeat deployment, account pinning, explicit adoption, and all-migration copying
104
+ 2. npm and Bun publish through the packaged Worker runtime
105
+ 3. cold-cache, read-token-only install through npm, pnpm, Yarn Classic, and Bun
106
+ 4. npm dist-tag promotion, rollback, listing, and removal
107
+ 5. immutable and concurrent version publication with only a D1-referenced winner visible
108
+ 6. property-order and chunk-boundary independence, strict JSON/Base64 rejection, depth and metadata limits, multipart boundaries, disconnect cleanup, and backpressure
109
+ 7. D1 commit error reconciliation and safe orphan behavior
110
+ 8. consistent package metadata snapshots during publication
111
+ 9. overlapping-token rotation and missing-binding tolerance
112
+ 10. absence of credentials and package contents from diagnostics
113
+
114
+ Deployment acceptance should also exercise initial setup, repeat deployment, failure recovery, and large publishes on the intended Cloudflare account. Record the account plan, encoded request size, tarball size, CPU time, and result when assessing capacity.
@@ -0,0 +1,34 @@
1
+ PRAGMA foreign_keys = ON;
2
+
3
+ CREATE TABLE packages (
4
+ name TEXT PRIMARY KEY,
5
+ created_at TEXT NOT NULL,
6
+ updated_at TEXT NOT NULL
7
+ );
8
+
9
+ CREATE TABLE versions (
10
+ package_name TEXT NOT NULL,
11
+ version TEXT NOT NULL,
12
+ manifest_json TEXT NOT NULL,
13
+ tarball_key TEXT NOT NULL UNIQUE,
14
+ tarball_file TEXT NOT NULL,
15
+ shasum TEXT NOT NULL,
16
+ integrity TEXT NOT NULL,
17
+ tarball_size INTEGER NOT NULL,
18
+ published_at TEXT NOT NULL,
19
+ PRIMARY KEY (package_name, version),
20
+ UNIQUE (package_name, tarball_file),
21
+ FOREIGN KEY (package_name) REFERENCES packages(name) ON DELETE CASCADE
22
+ );
23
+
24
+ CREATE TABLE dist_tags (
25
+ package_name TEXT NOT NULL,
26
+ tag TEXT NOT NULL,
27
+ version TEXT NOT NULL,
28
+ PRIMARY KEY (package_name, tag),
29
+ FOREIGN KEY (package_name, version)
30
+ REFERENCES versions(package_name, version) ON DELETE CASCADE
31
+ );
32
+
33
+ CREATE INDEX versions_package_published
34
+ ON versions(package_name, published_at);
package/package.json ADDED
@@ -0,0 +1,93 @@
1
+ {
2
+ "name": "@ponharu/pkgflare",
3
+ "version": "0.0.0",
4
+ "description": "Deploy a private npm registry to your own Cloudflare account.",
5
+ "keywords": [
6
+ "cloudflare",
7
+ "d1",
8
+ "npm",
9
+ "private-registry",
10
+ "r2",
11
+ "workers"
12
+ ],
13
+ "homepage": "https://github.com/ponharu/pkgflare#readme",
14
+ "bugs": {
15
+ "url": "https://github.com/ponharu/pkgflare/issues"
16
+ },
17
+ "license": "MIT",
18
+ "repository": {
19
+ "type": "git",
20
+ "url": "git+https://github.com/ponharu/pkgflare.git"
21
+ },
22
+ "bin": {
23
+ "pkgflare": "./dist/cli.js"
24
+ },
25
+ "files": [
26
+ "dist",
27
+ "docs",
28
+ "migrations",
29
+ "LICENSE",
30
+ "README.md",
31
+ "CONTRIBUTING.md",
32
+ "SECURITY.md"
33
+ ],
34
+ "type": "module",
35
+ "exports": {
36
+ ".": {
37
+ "types": "./dist/index.d.ts",
38
+ "import": "./dist/index.js"
39
+ }
40
+ },
41
+ "publishConfig": {
42
+ "access": "public",
43
+ "provenance": true,
44
+ "registry": "https://registry.npmjs.org"
45
+ },
46
+ "scripts": {
47
+ "build": "tsup",
48
+ "check": "bun run typecheck && bun run lint && bun run format:check",
49
+ "typecheck": "tsc --noEmit",
50
+ "lint": "oxlint --type-aware .",
51
+ "format": "oxfmt --write .",
52
+ "format:check": "oxfmt --check .",
53
+ "test": "bun run build && vitest run && vitest run --config vitest.worker.config.ts",
54
+ "test:e2e": "bun run build && bun scripts/test-clients.ts",
55
+ "test:package": "bun run build && bun scripts/test-package.ts",
56
+ "test:coverage": "bun run build && vitest run --coverage && vitest run --config vitest.worker.config.ts --coverage --coverage.provider=istanbul",
57
+ "prepack": "bun run build",
58
+ "prepare": "lefthook install",
59
+ "release": "semantic-release"
60
+ },
61
+ "dependencies": {
62
+ "jiti": "^2.7.0",
63
+ "semver": "^7.8.5",
64
+ "wrangler": "^4.125.0"
65
+ },
66
+ "devDependencies": {
67
+ "@cloudflare/vitest-pool-workers": "^0.22.0",
68
+ "@cloudflare/workers-types": "^5.20260822.1",
69
+ "@semantic-release/commit-analyzer": "13.0.1",
70
+ "@semantic-release/github": "12.0.6",
71
+ "@semantic-release/npm": "13.1.5",
72
+ "@semantic-release/release-notes-generator": "14.1.0",
73
+ "@types/node": "^24.3.0",
74
+ "@types/semver": "^7.7.1",
75
+ "@vitest/coverage-istanbul": "^4.1.11",
76
+ "@vitest/coverage-v8": "^4.1.11",
77
+ "conventional-changelog-conventionalcommits": "9.1.0",
78
+ "lefthook": "^2.1.10",
79
+ "oxfmt": "^0.64.0",
80
+ "oxlint": "^1.79.0",
81
+ "oxlint-tsgolint": "^7.0.2001",
82
+ "pnpm": "^11.22.0",
83
+ "semantic-release": "25.0.3",
84
+ "tsup": "^8.5.0",
85
+ "typescript": "^5.9.3",
86
+ "vitest": "^4.1.11",
87
+ "yarn": "^1.22.22"
88
+ },
89
+ "engines": {
90
+ "node": ">=22.0.0"
91
+ },
92
+ "packageManager": "bun@1.4.0"
93
+ }