{"_id":"@bytepunx/signet-client","_rev":"7-71ab848177edda07d61b946a47d321b6","name":"@bytepunx/signet-client","dist-tags":{"latest":"0.9.0"},"versions":{"0.3.0":{"name":"@bytepunx/signet-client","version":"0.3.0","license":"Apache-2.0","_id":"@bytepunx/signet-client@0.3.0","maintainers":[{"name":"arobson","email":"asrobson@gmail.com"}],"dist":{"shasum":"1b0d05260309aba81dde8da47fc3ce2a817c0779","tarball":"https://registry.npmjs.org/@bytepunx/signet-client/-/signet-client-0.3.0.tgz","fileCount":28,"integrity":"sha512-lhH684XK5bQNxmGZ6u/1A3hoPKx4+gP/gSTvFB0fshQ6RpAAS72vJGIrQKPzbjc5OXRfQJ8S6bFtLIftkpm1eQ==","signatures":[{"sig":"MEUCIQCs1J+TekEHcR8RgXJriPf60Zv3wAEnrgbzT1URqESQYAIgE6eUSdCuce0Sp6uTZBJjWQpPv8psRD3st4nl4KXMufE=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":360933},"main":"dist/index.js","type":"module","types":"dist/index.d.ts","exports":{".":{"types":"./dist/index.d.ts","default":"./dist/index.js"}},"gitHead":"da9ae0380bb1be8e0a822cd9331b89fd48852fcb","scripts":{"test":"node --test dist/**/*.test.js","build":"tsc -p tsconfig.json && tsc -p examples/echo/tsconfig.json"},"_npmUser":{"name":"arobson","email":"asrobson@gmail.com"},"_npmVersion":"10.9.8","description":"TypeScript/Node client for signet, generated from bytepunx/signet-proto","directories":{},"_nodeVersion":"22.23.1","dependencies":{"spiffe":"^0.5.1","@grpc/grpc-js":"^1.10.0","@bufbuild/protobuf":"^2.0.0"},"_hasShrinkwrap":false,"devDependencies":{"typescript":"^5.5.0"},"_npmOperationalInternal":{"tmp":"tmp/signet-client_0.3.0_1784565740856_0.249047257994786","host":"s3://npm-registry-packages-npm-production"}},"0.4.1":{"name":"@bytepunx/signet-client","version":"0.4.1","license":"Apache-2.0","_id":"@bytepunx/signet-client@0.4.1","maintainers":[{"name":"arobson","email":"asrobson@gmail.com"}],"homepage":"https://github.com/bytepunx/signet-clients#readme","bugs":{"url":"https://github.com/bytepunx/signet-clients/issues"},"dist":{"shasum":"36f6baa69c0c6a6534ce5f4f832dc5d6ca1f5db2","tarball":"https://registry.npmjs.org/@bytepunx/signet-client/-/signet-client-0.4.1.tgz","fileCount":28,"integrity":"sha512-5CrDFu/Oow5GQel/Ig1vwRAcvtNcjsxHLHrQaM06sy9FVP9gbUM3PHAM0hVnZFq8SCPc95vF70j3087XXCppCg==","signatures":[{"sig":"MEUCIQDexgVMEvqzQeJ0/aSqSIqjtDSMEqBGuJ4YJmN7zsKJLgIgMLtZCZi+Ti+rRPgaiKRYfULtxwwQtgL5ar1xBv2GykQ=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@bytepunx%2fsignet-client@0.4.1","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"unpackedSize":385653},"main":"dist/index.js","type":"module","types":"dist/index.d.ts","exports":{".":{"types":"./dist/index.d.ts","default":"./dist/index.js"}},"gitHead":"eed397fbdfd5501a5af0f52e73790ecceb57a536","scripts":{"test":"node --test dist/**/*.test.js","build":"tsc -p tsconfig.json && tsc -p examples/echo/tsconfig.json"},"_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"oidc:0d9f9933-2b8e-4f4a-aae0-ba345701da9c"}},"repository":{"url":"git+https://github.com/bytepunx/signet-clients.git","type":"git","directory":"typescript"},"_npmVersion":"11.19.0","description":"TypeScript/Node client for signet, generated from bytepunx/signet-proto","directories":{},"_nodeVersion":"22.23.1","dependencies":{"spiffe":"^0.5.1","@grpc/grpc-js":"^1.10.0","@bufbuild/protobuf":"^2.0.0"},"_hasShrinkwrap":false,"devDependencies":{"typescript":"^5.5.0"},"_npmOperationalInternal":{"tmp":"tmp/signet-client_0.4.1_1786281678983_0.39051195295768504","host":"s3://npm-registry-packages-npm-production"}},"0.5.0":{"name":"@bytepunx/signet-client","version":"0.5.0","license":"Apache-2.0","_id":"@bytepunx/signet-client@0.5.0","maintainers":[{"name":"arobson","email":"asrobson@gmail.com"}],"homepage":"https://github.com/bytepunx/signet-clients#readme","bugs":{"url":"https://github.com/bytepunx/signet-clients/issues"},"dist":{"shasum":"c8111a6018785388fb0a76800497eede48c1ae45","tarball":"https://registry.npmjs.org/@bytepunx/signet-client/-/signet-client-0.5.0.tgz","fileCount":32,"integrity":"sha512-fSz5WrI31oyP/UnAbZcPOQ6pw91v1RHTrKxmqUQC8gS9bU5z3kWtO1IUW9ldJmA6XMn/d1Bk1xxOSYG6jT9+iw==","signatures":[{"sig":"MEYCIQC/KmgvY8cIMq7wBvnMm01motuuSvgvPKWHX9x+aCFJogIhAKadxcH6zChjoeA3YSejsldbuUJvwQVKSM9B9Fa4+24T","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@bytepunx%2fsignet-client@0.5.0","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"unpackedSize":421891},"main":"dist/index.js","type":"module","types":"dist/index.d.ts","exports":{".":{"types":"./dist/index.d.ts","default":"./dist/index.js"}},"gitHead":"312b1f544307d16e53ece6ce4a960675d2cc52d1","scripts":{"test":"node --test dist/**/*.test.js","build":"tsc -p tsconfig.json && tsc -p examples/echo/tsconfig.json"},"_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"oidc:0d9f9933-2b8e-4f4a-aae0-ba345701da9c"}},"repository":{"url":"git+https://github.com/bytepunx/signet-clients.git","type":"git","directory":"typescript"},"_npmVersion":"11.19.0","description":"TypeScript/Node client for signet, generated from bytepunx/signet-proto","directories":{},"_nodeVersion":"22.23.2","dependencies":{"spiffe":"^0.5.1","@grpc/grpc-js":"^1.10.0","@bufbuild/protobuf":"^2.0.0"},"_hasShrinkwrap":false,"devDependencies":{"typescript":"^5.5.0"},"_npmOperationalInternal":{"tmp":"tmp/signet-client_0.5.0_1786686832929_0.917865171165738","host":"s3://npm-registry-packages-npm-production"}},"0.6.0":{"name":"@bytepunx/signet-client","version":"0.6.0","license":"Apache-2.0","_id":"@bytepunx/signet-client@0.6.0","maintainers":[{"name":"arobson","email":"asrobson@gmail.com"}],"homepage":"https://github.com/bytepunx/signet-clients#readme","bugs":{"url":"https://github.com/bytepunx/signet-clients/issues"},"dist":{"shasum":"fef97e9ac0c1dc74620dfdda5a2d269fcd4cfe35","tarball":"https://registry.npmjs.org/@bytepunx/signet-client/-/signet-client-0.6.0.tgz","fileCount":36,"integrity":"sha512-qS6IIiQeylIpkmu1UzKb3gWZA80ChIKFLsT1YE3E7qkkwvztOwAKE1vZTT83LQcCf3JmZ9OR+Qm6yJeDrHUQJQ==","signatures":[{"sig":"MEYCIQDMAkP8WYmhk/PNSbeHsNKKjuyIVseHlzcgMoFdGLN3VQIhAOSJgO9xrTV0BvdeigUf8Anlnmsm9m0HE+EqtCuutpiF","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@bytepunx%2fsignet-client@0.6.0","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"unpackedSize":432997},"main":"dist/index.js","type":"module","types":"dist/index.d.ts","exports":{".":{"types":"./dist/index.d.ts","default":"./dist/index.js"}},"gitHead":"67eb53a2e66881e7c21bc45a1a1e679e2bdd4b31","scripts":{"test":"node --test dist/**/*.test.js","build":"tsc -p tsconfig.json && tsc -p examples/echo/tsconfig.json"},"_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"oidc:0d9f9933-2b8e-4f4a-aae0-ba345701da9c"}},"repository":{"url":"git+https://github.com/bytepunx/signet-clients.git","type":"git","directory":"typescript"},"_npmVersion":"11.19.0","description":"TypeScript/Node client for signet, generated from bytepunx/signet-proto","directories":{},"_nodeVersion":"22.23.2","dependencies":{"yaml":"^2.9.0","spiffe":"^0.5.1","@grpc/grpc-js":"^1.10.0","age-encryption":"^0.3.0","@bufbuild/protobuf":"^2.0.0"},"_hasShrinkwrap":false,"devDependencies":{"typescript":"^5.5.0"},"_npmOperationalInternal":{"tmp":"tmp/signet-client_0.6.0_1786782007243_0.9749831346520215","host":"s3://npm-registry-packages-npm-production"}},"0.7.0":{"name":"@bytepunx/signet-client","version":"0.7.0","license":"Apache-2.0","_id":"@bytepunx/signet-client@0.7.0","maintainers":[{"name":"arobson","email":"asrobson@gmail.com"}],"homepage":"https://github.com/bytepunx/signet-clients#readme","bugs":{"url":"https://github.com/bytepunx/signet-clients/issues"},"dist":{"shasum":"9ba0129012ca15ab49c7e88a8dc69e511dae2040","tarball":"https://registry.npmjs.org/@bytepunx/signet-client/-/signet-client-0.7.0.tgz","fileCount":36,"integrity":"sha512-TlwHYzD0wexk6zmJD610REnkl/mtuY3nZLh3qB3JH2YMN6Ol2jwX7KGHzhw5rzlIa3Mo+ZWBqhScddz8pkl4og==","signatures":[{"sig":"MEUCIG0VxdDo8RkdEai5RSBzFDu/8m8kOv6N6ORG8sDlyb0EAiEA4B5yvy5w53+DiazxP3WwJ0Tz4a4oATKs0aVPSz9EmxA=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@bytepunx%2fsignet-client@0.7.0","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"unpackedSize":448817},"main":"dist/index.js","type":"module","types":"dist/index.d.ts","exports":{".":{"types":"./dist/index.d.ts","default":"./dist/index.js"}},"gitHead":"bf8ca35a1dcef0edb9cdeab371c56a5a09668612","scripts":{"test":"node --test dist/**/*.test.js","build":"tsc -p tsconfig.json && tsc -p examples/echo/tsconfig.json"},"_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"oidc:0d9f9933-2b8e-4f4a-aae0-ba345701da9c"}},"repository":{"url":"git+https://github.com/bytepunx/signet-clients.git","type":"git","directory":"typescript"},"_npmVersion":"11.19.0","description":"TypeScript/Node client for signet, generated from bytepunx/signet-proto","directories":{},"_nodeVersion":"22.23.2","dependencies":{"yaml":"^2.9.0","spiffe":"^0.5.1","@grpc/grpc-js":"^1.10.0","age-encryption":"^0.3.0","@bufbuild/protobuf":"^2.0.0"},"_hasShrinkwrap":false,"devDependencies":{"typescript":"^5.5.0"},"_npmOperationalInternal":{"tmp":"tmp/signet-client_0.7.0_1787286386907_0.6194485192669859","host":"s3://npm-registry-packages-npm-production"}},"0.8.0":{"name":"@bytepunx/signet-client","version":"0.8.0","license":"Apache-2.0","_id":"@bytepunx/signet-client@0.8.0","maintainers":[{"name":"arobson","email":"asrobson@gmail.com"}],"homepage":"https://github.com/bytepunx/signet-clients#readme","bugs":{"url":"https://github.com/bytepunx/signet-clients/issues"},"dist":{"shasum":"e71900b4832b3f36f5ac01c2b321eb2206413b06","tarball":"https://registry.npmjs.org/@bytepunx/signet-client/-/signet-client-0.8.0.tgz","fileCount":36,"integrity":"sha512-VSl5c+GsSrwJ/RIest6UXjMzjHG2i3at4HuREUsk6yoh3wjCazZQfmiwagwj5Zsrxy6c0GGccaXWRONZjuEVNQ==","signatures":[{"sig":"MEYCIQClFPEGWKeSzAx99KUBixPSREZnVC1HD2qNpKGR0k9M9QIhAO39sscp1+u1P1mp7L46l9UMToaYZ+o+YBo39+H8BaZq","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@bytepunx%2fsignet-client@0.8.0","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"unpackedSize":505365},"main":"dist/index.js","type":"module","types":"dist/index.d.ts","exports":{".":{"types":"./dist/index.d.ts","default":"./dist/index.js"}},"gitHead":"5445a07cffe40154b5e982a3f924b765565b1d49","scripts":{"test":"node --test dist/**/*.test.js","build":"tsc -p tsconfig.json && tsc -p examples/echo/tsconfig.json && tsc -p examples/gitops-workload/tsconfig.json"},"_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"oidc:0d9f9933-2b8e-4f4a-aae0-ba345701da9c"}},"repository":{"url":"git+https://github.com/bytepunx/signet-clients.git","type":"git","directory":"typescript"},"_npmVersion":"11.19.1","description":"TypeScript/Node client for signet, generated from bytepunx/signet-proto","directories":{},"_nodeVersion":"22.23.2","dependencies":{"yaml":"^2.9.0","spiffe":"^0.5.1","@grpc/grpc-js":"^1.10.0","age-encryption":"^0.3.0","@bufbuild/protobuf":"^2.0.0"},"_hasShrinkwrap":false,"devDependencies":{"typescript":"^5.5.0","@peculiar/x509":"^1.14.0","@peculiar/webcrypto":"^1.7.0"},"_npmOperationalInternal":{"tmp":"tmp/signet-client_0.8.0_1788306895756_0.41962716600436933","host":"s3://npm-registry-packages-npm-production"}},"0.9.0":{"name":"@bytepunx/signet-client","version":"0.9.0","description":"TypeScript/Node client for signet, generated from bytepunx/signet-proto","license":"Apache-2.0","repository":{"type":"git","url":"git+https://github.com/bytepunx/signet-clients.git","directory":"typescript"},"type":"module","main":"dist/index.js","types":"dist/index.d.ts","exports":{".":{"types":"./dist/index.d.ts","default":"./dist/index.js"}},"scripts":{"build":"tsc -p tsconfig.json && tsc -p examples/echo/tsconfig.json && tsc -p examples/gitops-workload/tsconfig.json","test":"node --test dist/**/*.test.js"},"dependencies":{"@bufbuild/protobuf":"^2.0.0","@grpc/grpc-js":"^1.10.0","age-encryption":"^0.3.0","spiffe":"^0.5.1","yaml":"^2.9.0"},"devDependencies":{"typescript":"^5.5.0","@peculiar/webcrypto":"^1.7.0","@peculiar/x509":"^1.14.0"},"gitHead":"d9888ac76f7f8881aefc1c023c3db4bc1398b9f4","_id":"@bytepunx/signet-client@0.9.0","bugs":{"url":"https://github.com/bytepunx/signet-clients/issues"},"homepage":"https://github.com/bytepunx/signet-clients#readme","_nodeVersion":"22.23.2","_npmVersion":"11.19.1","dist":{"integrity":"sha512-Rl/ayoaJjCSdve3SY2IwbxJCL0aHeHNZFvJOir4/LaMrYu7RvrJHeL8G5LrVJGuo31WYgKKC70g9UHIRm4w7pA==","shasum":"be064173dd1e87691da0e6a89ad21ea039e5b233","tarball":"https://registry.npmjs.org/@bytepunx/signet-client/-/signet-client-0.9.0.tgz","fileCount":36,"unpackedSize":519893,"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@bytepunx%2fsignet-client@0.9.0","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEYCIQDAgSbkM+gex1aBjaqNkdZcsLYA2i+zxiJVL0QgE96XbQIhAPzhA0OEw/iHZDHYJW1wcSh95+aNWCGS8OJ6/9xB/AVW"}]},"_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"oidc:0d9f9933-2b8e-4f4a-aae0-ba345701da9c"}},"directories":{},"maintainers":[{"name":"arobson","email":"asrobson@gmail.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/signet-client_0.9.0_1788416068025_0.8128378170906179"},"_hasShrinkwrap":false}},"time":{"created":"2026-07-20T16:42:20.641Z","modified":"2026-09-03T06:14:28.564Z","0.3.0":"2026-07-20T16:42:21.010Z","0.4.1":"2026-08-09T13:21:19.130Z","0.5.0":"2026-08-14T05:53:53.075Z","0.6.0":"2026-08-15T08:20:07.434Z","0.7.0":"2026-08-21T04:26:27.061Z","0.8.0":"2026-09-01T23:54:55.966Z","0.9.0":"2026-09-03T06:14:28.188Z"},"bugs":{"url":"https://github.com/bytepunx/signet-clients/issues"},"license":"Apache-2.0","homepage":"https://github.com/bytepunx/signet-clients#readme","repository":{"type":"git","url":"git+https://github.com/bytepunx/signet-clients.git","directory":"typescript"},"description":"TypeScript/Node client for signet, generated from bytepunx/signet-proto","maintainers":[{"name":"arobson","email":"asrobson@gmail.com"}],"readme":"# @bytepunx/signet-client (TypeScript / Node)\n\nNode.js gRPC client for [signet](https://github.com/bytepunx/signet), generated from\n[bytepunx/signet-proto](https://github.com/bytepunx/signet-proto) via\n[ts-proto](https://github.com/stephenh/ts-proto), pinned independently of the server's own\nrelease cycle (see `buf.gen.yaml`).\n\n**Status:** implemented — connection helpers for both bearer-token (admin) access and SPIFFE\nmTLS (workload) access, plus in-memory coordinated-restart support (`watchBundle`/\n`acquireLock`/`waitForRestart`), mirroring the [Go client](../go). The one real gap is noted\nbelow: there is no SPIFFE library for Node as mature as Go's `go-spiffe`, so `dialWorkload`'s\nmTLS credentials are fetched once at dial time rather than rotated automatically in the\nbackground.\n\n```\nnpm install @bytepunx/signet-client\n```\n\nPublished to [npm](https://www.npmjs.com/package/@bytepunx/signet-client) automatically by\n[`publish-typescript.yml`](../.github/workflows/publish-typescript.yml) whenever\nrelease-please tags a `typescript-v*` release.\n\n## Regenerating stubs\n\n```\nbuf generate\n```\n\nPulls `signet/v1` and `admin/v1` from `buf.build/bytepunx/signet-proto` and regenerates\n`src/gen`, which is committed (matching the [Go client](../go)'s `gen/` — this package now\nhas real code depending on it, so it's tracked rather than gitignored like the\nnot-yet-implemented clients). Edit the module reference in `buf.gen.yaml` to pin a\ndifferent schema version, then commit the regenerated `src/gen`.\n\n## Usage\n\n### Operator access (AdminService/GitOpsService, bearer token)\n\n```ts\nimport { dialAdmin } from \"@bytepunx/signet-client\";\n\nconst admin = dialAdmin({ address: \"localhost:8444\", token });\nconst status = await new Promise((resolve, reject) =>\n  admin.status({}, (err, resp) => (err ? reject(err) : resolve(resp))),\n);\nadmin.close();\n```\n\nLoopback addresses (the documented `kubectl port-forward` workflow) use plaintext by\ndefault; every other address is upgraded to TLS automatically using the system trust store,\nor the CA in `caPem` if provided. `forceTLS` requests TLS even for a loopback address —\nmatching the Go client's `DialAdmin` exactly. The bearer token is injected via a grpc-js\n*interceptor* rather than composed call credentials: grpc-js's insecure channel credentials\nexplicitly refuse to compose with call credentials (\"Cannot compose insecure credentials\"),\nwhich would otherwise break the plaintext-loopback dev workflow that Go's client supports via\n`PerRPCCredentials.RequireTransportSecurity() == false`. See `authInterceptor` in\n`src/client.ts`.\n\n`plaintext: true` forces insecure transport credentials even for a **non-loopback** address,\nbypassing the loopback heuristic entirely (bytepunx/signet-clients#32). This is required once\nsignet exposes a real in-cluster admin listener (bytepunx/signet#19): dialing that Service by\nits cluster-DNS name is a non-loopback address, but the listener is intentionally still\nplaintext-behind-bearer-token, not TLS-terminated — without `plaintext`, the loopback\nheuristic would pick TLS and the handshake would fail immediately (\"wrong version number\")\nagainst a server that never speaks TLS on that listener. Per-RPC bearer-token authentication\nis unaffected either way. `plaintext` is mutually exclusive with `forceTLS` and with a\nnon-empty `caPem`; `dialAdmin`/`gitOpsClient`/`adminChannelCredentials` throw if either\ncombination is requested. Matches the Go client's `DialAdmin` `plaintext` parameter exactly.\n\n```ts\nconst admin = dialAdmin({ address: \"signet-admin.signet.svc.cluster.local:8444\", token, plaintext: true });\n```\n\n#### Encrypting a secret value for `SyncBundle`/`TriggerSync` (`encryptForSecret`)\n\nsignetd never encrypts secrets on a caller's behalf — `SyncBundle`/`TriggerSync` both require\ncontent that is already real [SOPS](https://github.com/getsops/sops) ciphertext (signet's\ndocumented trust model is \"only SOPS ciphertext leaves the operator's machine\"). `encryptForSecret`\nproduces that ciphertext client-side: a SOPS-encrypted YAML document holding a single `value` field,\nencrypted to one age (X25519) recipient — normally the key returned by\n`AdminService.GetSOPSPublicKey` (bytepunx/signet-clients#49).\n\n```ts\nimport { encryptForSecret, dialAdmin } from \"@bytepunx/signet-client\";\n\nconst admin = dialAdmin({ address: \"localhost:8444\", token });\nconst { publicKey } = await new Promise((resolve, reject) =>\n  admin.getSopsPublicKey({}, (err, resp) => (err ? reject(err) : resolve(resp))),\n);\n\nconst content = await encryptForSecret(publicKey, \"s3cr3t-api-key\");\n// `content` is now ready to hand to SyncBundle/TriggerSync as an encrypted secret's contents.\n```\n\nThis is a from-scratch implementation of the narrow slice of the SOPS envelope format this\npackage needs, built on [`age-encryption`](https://www.npmjs.com/package/age-encryption) (the\nofficial TypeScript port of age) — no npm package implements the real SOPS format (everything\nunder \"sops\" on npm is decrypt-only, or produces an incompatible custom format). Output is\nverified to round-trip through the real `sops` binary; see `src/sopsEncrypt.test.ts`.\n\n#### Patching a service's plain config (`GitOpsService.PatchServiceConfig`)\n\n`SyncBundle`'s config path is a full-document replace — pushing a file covering only the\ntenant/field you care about silently drops every other entry the live document already has.\n`PatchServiceConfig` applies an [RFC 6902](https://www.rfc-editor.org/rfc/rfc6902) JSON Patch\natomically server-side instead, so a caller never needs to read the current document just to\nadd or remove one entry (bytepunx/signet#38). `jsonPatchAppend`/`jsonPatchAdd`/\n`jsonPatchReplace`/`jsonPatchRemove`/`jsonPatchTest` build the `{op, path, from, value}` shape\nby hand-writing it out at every call site:\n\n```ts\nimport { gitOpsClient, jsonPatchAppend } from \"@bytepunx/signet-client\";\n\nconst gitops = gitOpsClient({ address: \"signet-admin.signet.svc.cluster.local:8444\", token, plaintext: true });\n\nawait new Promise((resolve, reject) =>\n  gitops.patchServiceConfig(\n    {\n      namespace: \"authstar\",\n      service: \"portcullis\",\n      operations: [\n        // \"/-\" appends without needing to know the array's current length.\n        jsonPatchAppend(\"/tenants/acme/sessionKeyGenerations\", { version: 2, effectiveFrom: new Date().toISOString() }),\n      ],\n    },\n    (err, resp) => (err ? reject(err) : resolve(resp)),\n  ),\n);\n```\n\nFails `NotFound` if no config document exists yet for `namespace`/`service` — this RPC only\nmutates an existing document (use `SyncBundle` or git sync to create the initial one). The whole\npatch either fully applies or fails with no partial effect; `jsonPatchTest` is useful as a\nconcurrency guard (assert an array entry hasn't changed since you last read it, rather than\nsilently overwriting or removing the wrong one).\n\n### Workload access (SecretsService, SPIFFE mTLS)\n\n```ts\nimport { dialWorkload } from \"@bytepunx/signet-client\";\n\nconst { client, close } = await dialWorkload({\n  address: \"signet.internal:8443\",\n  workloadSocket: \"unix:///run/spire/sockets/agent.sock\",\n  trustDomain: \"example.org\",\n});\ntry {\n  const resp = await new Promise((resolve, reject) =>\n    client.getSecret({ namespace: \"default\", service: \"example\", name: \"api-key\" }, (err, r) =>\n      err ? reject(err) : resolve(r),\n    ),\n  );\n} finally {\n  close();\n}\n```\n\n`dialWorkload` retries automatically — no opt-in required — if the Workload API reports \"no\nidentity issued\" before it can hand back a connection (bytepunx/signet-clients#33). SPIRE's\ncontroller-manager reconciles a brand-new pod's SPIFFE identity registration reactively, off\nthe pod's own creation event, and that registration takes a few seconds to propagate from\nthere to the node-local SPIRE agent `dialWorkload` dials over `workloadSocket`. A\nfreshly-created pod's very first `dialWorkload` call — a Job's is the sharpest case, since a\nJob has no prior pod that might have already won this race for the same ServiceAccount — can\nlose it outright and see \"no identity issued\" even though the identity shows up moments\nlater. This is the same race first hit (and hand-rolled around) in\n[bytepunx/kluster](https://github.com/bytepunx/kluster)'s RabbitMQ credential-provisioning\nJob; the fix now lives here instead, matching the [Go client](../go)'s `DialWorkload`.\n\nThe retry makes up to 5 attempts total — the initial attempt plus up to 4 retries — backing\noff 1s, 2s, 4s, 8s between them. Only the \"no identity issued\" failure (a `PERMISSION_DENIED`\nstatus from the Workload API) is retried: every other error (a bad `workloadSocket`, a\nmalformed `trustDomain`, a genuine authorization problem once identity issuance is actually\nbroken rather than merely delayed, ...) is returned to the caller immediately, unretried, so\nthis never masks a real misconfiguration as a transient blip. See `retryUntilIdentityIssued`\nand `isNoIdentityIssuedErr` in `src/workload.ts`.\n\n#### GitOpsService/AdminService over the same workload identity (`workloadChannelCredentials`)\n\nsignet's server accepts a workload's own SPIFFE identity — no admin bearer token needed — for\n`SyncBundle`, `PatchServiceConfig`, and `GetSOPSPublicKey`, letting a workload self-service its\nown bundle/config writes and fetch the active SOPS public key (bytepunx/signet#23, #38, #78).\nCross-namespace/service writes still require an explicit `CreatePolicy` grant from an operator;\nevery other `GitOpsService`/`AdminService` RPC remains reachable only via the bearer-token path\nabove.\n\n`dialWorkload` builds its `ChannelCredentials` via `workloadChannelCredentials`, exported\nstandalone so any other grpc-js client can be bound to the same workload identity:\n\n```ts\nimport { workloadChannelCredentials, GitOpsServiceClient } from \"@bytepunx/signet-client\";\n\nconst credentials = await workloadChannelCredentials({\n  address: \"signet.internal:8443\",\n  workloadSocket: \"unix:///run/spire/sockets/agent.sock\",\n  trustDomain: \"example.org\",\n});\nconst gitops = new GitOpsServiceClient(\"signet.internal:8443\", credentials);\nconst resp = await new Promise((resolve, reject) =>\n  gitops.getSopsPublicKey({}, (err, r) => (err ? reject(err) : resolve(r))),\n);\ngitops.close();\n```\n\n`dialWorkloadGitOps` wraps that up with the same open/close ergonomics `dialWorkload` provides\nfor `SecretsServiceClient`:\n\n```ts\nimport { dialWorkloadGitOps } from \"@bytepunx/signet-client\";\n\nconst { client, close } = await dialWorkloadGitOps({\n  address: \"signet.internal:8443\",\n  workloadSocket: \"unix:///run/spire/sockets/agent.sock\",\n  trustDomain: \"example.org\",\n});\nconst resp = await new Promise((resolve, reject) =>\n  client.getSopsPublicKey({}, (err, r) => (err ? reject(err) : resolve(r))),\n);\nclose();\n```\n\nSee `examples/gitops-workload/` for a runnable, CLI-driven version of the above.\n\n#### The SPIFFE gap (please read before relying on this in production)\n\nGo's client builds on [`go-spiffe`](https://github.com/spiffe/go-spiffe), a mature, official\nSPIFFE SDK: `workloadapi.NewX509Source` fetches and **continuously rotates** X.509 SVIDs in\nthe background, and `tlsconfig.AuthorizeMemberOf` verifies a peer's SPIFFE ID against an\nexpected trust domain — both battle-tested, both essentially free.\n\nNothing at that level of maturity exists for Node. We surveyed the npm registry\n(`npm search spiffe`, plus manual review) for a maintained SPIFFE Workload API client and\nfound exactly one real candidate: [`spiffe`](https://www.npmjs.com/package/spiffe)\n([`depot/node-spiffe`](https://github.com/depot/node-spiffe) on GitHub). It is thin: 7 GitHub\nstars, 9 open issues, and a ~2-year maintenance gap between v0.4.0 (Nov 2023) and v0.5.0 (Jan\n2026). It only fetches raw X.509 SVID bytes over the Workload API's gRPC/UDS protocol — it\ndoes not provide certificate rotation, mTLS credential construction, or trust-domain\nauthorization.\n\n`dialWorkload` uses it anyway, for the one thing it's actually useful for (speaking the\nWorkload API's UDS/gRPC/protobuf protocol, which would otherwise mean hand-rolling a\nnon-trivial wire protocol), and this library fills in the two pieces `go-spiffe` normally\nprovides for free:\n\n**A consequence for the #33 retry above:** go-spiffe exposes two distinct primitives —\n`FetchX509Context`, a single-shot, non-watching probe, and `NewX509Source`, a separate\nlong-lived, internally-self-retrying source — so Go's `DialWorkload` probes readiness with\nthe former before establishing the real connection via the latter (probing with a\nself-retrying source directly would mask the very error the retry needs to classify). `spiffe`\nhas no long-lived/background-rotating equivalent of `NewX509Source` at all — every\n`fetchX509SVID` call is already a single-shot, non-watching fetch, identical in kind to\n`FetchX509Context`. So here, unlike Go, there's only one primitive to retry: the probe and the\nreal fetch are the same operation. `retryUntilIdentityIssued` wraps the SVID fetch directly and\nits result becomes the connection's credentials, rather than probing once and re-fetching\nthrough a second, differently-shaped call. The externally observable behavior — 5 attempts,\n1s/2s/4s/8s backoff, retrying only \"no identity issued\" — is unchanged.\n\n- **Credential construction** (`credentialsFromSVID` in `src/workload.ts`): converts the\n  fetched DER cert chain, private key, and trust bundle to PEM and builds `@grpc/grpc-js`\n  mTLS `ChannelCredentials`.\n- **Trust-domain authorization** (`authorizeTrustDomainMember` in `src/workload.ts`): a\n  `checkServerIdentity` callback that parses the `URI:spiffe://...` subject alternative name\n  off the server's presented leaf certificate and rejects it unless it belongs to the\n  expected trust domain — the same policy as `tlsconfig.AuthorizeMemberOf`, reimplemented\n  here because nothing off-the-shelf provides it for Node. It's unit tested against\n  hand-written fake certificates in `src/workload.test.ts`.\n\n**The real, named gap:** `dialWorkload` fetches the SVID **once**, at dial time. There is no\n`X509Source`-equivalent background rotation. A long-lived connection holds onto whatever\ncertificate was current when you dialed; if your process runs longer than SPIRE's configured\nSVID TTL (often on the order of an hour), redial periodically, or simply restart the process\non a schedule (this pairs naturally with `waitForRestart` below) rather than assuming this\nconnection stays valid indefinitely.\n\nThis path has not been exercised against a live SPIRE deployment. Audit it before depending\non it in production. If that's not acceptable for your deployment, two alternatives:\n\n1. Terminate mTLS somewhere better-tested (an Envoy/SPIRE sidecar, for example) and have this\n   client talk to that over already-established mTLS or plaintext-over-a-trusted-network.\n2. Build your own `ChannelCredentials` however you trust (any Node TLS/SPIFFE tooling you've\n   already vetted) and construct the client directly with the exported `secretsClient(address,\n   credentials)` — `dialWorkload` is a convenience, not a requirement.\n\n### Coordinated restarts (no process host, no environment injection)\n\n`watchBundle`/`acquireLock`/`waitForRestart` let a service pull its own configuration\ndirectly and in-memory, and safely coordinate a fleet-wide serialized restart when it\nchanges — without [kickr](https://github.com/bytepunx/kickr) (a process-host base image that\ninjects the bundle into a child process's environment) and without ever writing secrets to\nthe OS environment, where they'd be readable via `/proc/<pid>/environ` by anything sharing\nthe pod's PID namespace.\n\nThis library deliberately does **not**:\n- spawn or supervise a child process — it's embedded directly in the process that's\n  configuring itself from the bundle;\n- write the bundle to environment variables, files, or anywhere outside memory;\n- refetch the bundle after the lock is acquired — since there's no replacement process to\n  hand it to, the *next* process instance (started fresh by Kubernetes after this one exits)\n  fetches it during its own normal startup;\n- call `process.exit()` anywhere — the caller decides when to actually terminate.\n\n```ts\nimport { dialWorkload, waitForRestart } from \"@bytepunx/signet-client\";\n\nconst { client } = await dialWorkload({ address: \"signet.internal:8443\", trustDomain: \"example.org\" });\n\n// Fetch once at startup, configure the app in memory.\nconst bundle = await new Promise((resolve, reject) =>\n  client.getServiceBundle({ namespace: \"default\", service: \"example\" }, (err, r) =>\n    err ? reject(err) : resolve(r),\n  ),\n);\n\n// ... serve traffic ...\n\n// Block until signet reports a change AND this replica holds the restart lock (at most one\n// replica restarts at a time, fleet-wide).\nconst lock = await waitForRestart(\n  client,\n  \"default\",\n  \"example\",\n  30 /* lock TTL in seconds — must cover your graceful shutdown */,\n  10_000 /* debounce in ms — absorb rapid successive changes */,\n);\n\n// Do your own graceful shutdown: drain in-flight requests, close resources.\n\nawait lock.release(); // release the lock for the next waiting replica\nprocess.exit(0); // Kubernetes restarts the pod; the new process fetches fresh config\n```\n\n`lock.lost` is a Promise that resolves with an `Error` if the lock is lost unexpectedly\n(stream error, or the server closing the stream) before you call `release()` — never\nsettling at all after a clean `release()`, so it safely distinguishes loss from an\nintentional release even if you attach a handler late. A `'lost'` event is also emitted on\nthe `Lock` (it's an `EventEmitter`) for consumers who prefer that idiom; both fire from the\nsame underlying state, at most once.\n\n```ts\nlock.lost.then((err) => log.warn(\"restart lock lost unexpectedly\", err));\n// or:\nlock.on(\"lost\", (err) => log.warn(\"restart lock lost unexpectedly\", err));\n```\n\nHeartbeats are sent automatically at `ttlSeconds / 4` — **not** `ttlSeconds / 2` — matching\nsignet's documented convention that 4 consecutive missed heartbeats exhaust the TTL (see\n[signet's restart-lock docs](https://github.com/bytepunx/signet/blob/main/docs/restart-lock.md));\nthis exact bug (heartbeating at `ttl/2`) was found and fixed in kickr, signet's existing\nprocess-host client, which is why the Go and TypeScript clients both call it out explicitly.\n\n`acquireLock`/`watchBundle`/`waitForRestart` all accept an optional `{ signal }` — a standard\n`AbortSignal` — as their cancellation mechanism (the idiomatic Node/web-platform equivalent\nof Go's `context.Context`).\n\nSee `examples/echo/` for a minimal end-to-end program (env-var configured, built as a\nDocker image) used by the [signet-smoke-test](https://github.com/bytepunx/signet-smoke-test)\nharness to verify this client against a real signet + SPIRE deployment, and\n`examples/gitops-workload/` for a minimal, CLI-flag-configured program demonstrating\n`dialWorkloadGitOps`/`workloadChannelCredentials` (see \"GitOpsService/AdminService over the\nsame workload identity\" above).\n\n## Testing\n\n```\nnpm run build && npm test\n```\n\n`npm test` runs `node --test dist/**/*.test.js` — Node's built-in test runner, against\ncompiled output (kept as-is from the original scaffold). 63 test cases across three files,\nall driven against hand-written fakes implementing narrow `LockStream`/`WatchStream`\ninterfaces (mirroring `go/restart_test.go`'s fake-based pattern) — no live network connection\nor signet instance is used anywhere:\n\n- **`src/restart.test.ts`** (23 cases) — every case in `go/restart_test.go`, ported: `ttl <=\n  0` rejected before opening any stream; `QUEUE_POSITION*` → `ACQUIRED` handoff; a stream\n  error before `ACQUIRED` surfaces as a clear rejection; heartbeat interval is\n  `ttlSeconds / 4`; `TTL_EXTENDED` updates the tracked expiry; lock loss is detectable and\n  distinct from an intentional `release()` (via both the `lost` Promise and the `'lost'`\n  event); `release()` is idempotent; `watchBundle` coalesces rapid successive changes into\n  one pending signal; `watchBundle` reconnects with backoff after a stream error and\n  eventually delivers a change. Plus cases beyond the Go set: a failing (rejecting) `open()`\n  is treated the same as a mid-stream error; `AbortSignal` cancellation while waiting to\n  acquire a lock, and while watching for changes, is honored promptly rather than hanging.\n- **`src/client.test.ts`** (20 cases) — every case in `go/client_test.go`, ported:\n  `isLoopbackHost` table; loopback-defaults-to-plaintext / non-loopback-requires-TLS /\n  `forceTLS`-forces-TLS-on-loopback; empty/whitespace token rejected with a clear message;\n  invalid CA PEM produces a clear parse error, not a raw OpenSSL exception (including a\n  syntactically-present-but-malformed PEM block, which Go's suite doesn't separately cover).\n  Plus `authInterceptor` coverage specific to this port's design (see the admin-access\n  section above for why it exists), and the `plaintext` override coverage for\n  bytepunx/signet-clients#32: overrides TLS on a non-loopback address, is a no-op (still\n  plaintext) on loopback, leaves existing behavior unchanged when omitted, and is rejected\n  when combined with `forceTLS` or `caPem`, at both the `adminTransportMode` and `dialAdmin`\n  entry points.\n- **`src/workload.test.ts`** (20 cases) — `authorizeTrustDomainMember` (the\n  `tlsconfig.AuthorizeMemberOf` reimplementation) against hand-written fake certificates:\n  accepts a matching trust domain, rejects a mismatched one, rejects a missing SPIFFE URI\n  SAN, normalizes a `spiffe://` prefix, rejects malformed input up front; `derToPem`\n  round-trip coverage; the identity-issuance retry coverage for\n  bytepunx/signet-clients#33, porting every case in `go/client_test.go`'s retry suite:\n  `isNoIdentityIssuedErr` classification (PERMISSION_DENIED in various shapes vs. other\n  codes vs. non-RpcError values), `retryUntilIdentityIssued` succeeding immediately with no\n  sleep, succeeding after a couple of retries with the right partial backoff, exhausting all\n  `workloadDialMaxAttempts` (5) with the full 1s/2s/4s/8s schedule, and passing an unrelated\n  error straight through unretried — all via a fake `sleep` injected in place of the default\n  `setTimeout`-based one, so the full-schedule case runs in under a millisecond instead of\n  ~15s; and, for the GitOps-over-workload-mTLS credential exposure, `workloadChannelCredentials`\n  wrapping a synchronous Workload API connection failure with a clear, prefixed error (no\n  socket or SPIRE instance needed), plus a real (if minimal) self-signed certificate generated\n  via `@peculiar/x509` proving `credentialsFromSVID`'s output — exactly what\n  `workloadChannelCredentials` resolves to — constructs `GitOpsServiceClient`,\n  `AdminServiceClient`, and `SecretsServiceClient` without connecting.\n\nThe two timing-sensitive tests that must observe real elapsed time (the default heartbeat\ninterval, and one coalescing test) use real timers and take ~200ms–1.3s each; every\nreconnect/backoff test overrides `backoffMinMs`/`backoffMaxMs` to keep the suite fast without\nweakening what's asserted about the default (1s, doubling, capped at 30s) production\nbehavior — that shape is exercised structurally, not by waiting out the real default backoff\nin every case.\n","readmeFilename":"README.md"}