@shelbyb/daywalker-sdk 0.4.0 → 0.5.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.
- package/README.md +79 -7
- package/dist/cli.js +11 -10
- package/dist/client.d.ts +4 -7
- package/dist/client.js +25 -6
- package/dist/config.d.ts +8 -0
- package/dist/config.js +11 -1
- package/dist/crypto/index.d.ts +1 -1
- package/dist/crypto/index.js +1 -1
- package/dist/crypto/signatures.d.ts +5 -0
- package/dist/crypto/signatures.js +22 -0
- package/dist/index.d.ts +3 -3
- package/dist/index.js +2 -2
- package/dist/types.d.ts +32 -0
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -4,6 +4,10 @@
|
|
|
4
4
|
|
|
5
5
|
# @shelbyb/daywalker-sdk
|
|
6
6
|
|
|
7
|
+
[](https://www.npmjs.com/package/@shelbyb/daywalker-sdk)
|
|
8
|
+
[](https://github.com/Shelbyb/daywalker-sdk/actions/workflows/ci.yml)
|
|
9
|
+
[](./LICENSE)
|
|
10
|
+
|
|
7
11
|
Runtime-agnostic client library for [Daywalker Auth](https://auth.daywalker.dev). Supports **RFC 9421 (HTTP Message Signatures)**, **RFC 9530 (Digest Fields)**, **Web Crypto standards**, **Push Architecture key registration**, and **hardened web & Next.js middleware**.
|
|
8
12
|
|
|
9
13
|
Runs on **Node.js 20.19+**, **Bun**, **Deno**, **Cloudflare Workers**, **Next.js Edge**, and browsers. ESM only; `require()` works on Node 20.19+ and 22.12+.
|
|
@@ -44,7 +48,7 @@ DAYWALKER_API_KEY=srv_live_... npx daywalker init --service my-service-slug --do
|
|
|
44
48
|
|
|
45
49
|
`daywalker init`:
|
|
46
50
|
- Generates an Ed25519 keypair locally using WebCrypto (`crypto.subtle`)
|
|
47
|
-
- Registers the public JWK directly with `POST https://auth.daywalker.dev/api/v1/keys/register` (`Authorization: Bearer <API key>`, body `{ keyId, domain, publicKey }`; responds `201`)
|
|
51
|
+
- Registers the public JWK directly with `POST https://auth.daywalker.dev/api/v1/keys/register` (`Authorization: Bearer <API key>`, body `{ keyId, domain, publicKey, serviceSlug }`; responds `201`, or `403 service_not_covered` when the API key doesn't cover the service)
|
|
48
52
|
- Writes `DAYWALKER_SERVICE_SLUG`, `DAYWALKER_KEY_ID`, `DAYWALKER_SIGNING_KEY`, and `DAYWALKER_AUTH_URL` to `.env.local` (atomically, mode `0600`), and only after registration succeeds. `--skip-register` writes without registering.
|
|
49
53
|
- `--force` replaces an existing key and keeps the old one as `DAYWALKER_SIGNING_KEY_PREVIOUS`
|
|
50
54
|
- No route injectors or public `/.well-known/` endpoints needed
|
|
@@ -58,7 +62,7 @@ The private key is never printed unless you pass `--print-key`.
|
|
|
58
62
|
| Variable | Required | Purpose |
|
|
59
63
|
|---|---|---|
|
|
60
64
|
| `DAYWALKER_API_KEY` | ✅ | The service's Daywalker API token (`srv_live_…`) or JWT lease |
|
|
61
|
-
| `DAYWALKER_SERVICE_SLUG` | ✅ | The service slug to validate against (e.g. `sonalyze`) |
|
|
65
|
+
| `DAYWALKER_SERVICE_SLUG` | ✅ | The service slug to validate against (e.g. `sonalyze`). One API key can cover several services: a comma-separated list (`gigpulse,devpulse`) is accepted and the first is the default. See [One API key, several services](#one-api-key-several-services) |
|
|
62
66
|
| `DAYWALKER_SIGNING_KEY` | for signed requests | Ed25519 private key from `daywalker init` (PKCS#8 Base64 DER or PEM) |
|
|
63
67
|
| `DAYWALKER_KEY_ID` | optional | RFC 7638 JWK thumbprint derived during key generation |
|
|
64
68
|
| `DAYWALKER_SIGNING_KEY_PREVIOUS` | during rotation | Previous private key, maintained during deployment transitions |
|
|
@@ -177,6 +181,62 @@ app.get("/api/me", (c) => {
|
|
|
177
181
|
|
|
178
182
|
---
|
|
179
183
|
|
|
184
|
+
## Express / Connect / Node `http` (`@shelbyb/daywalker-sdk/express`)
|
|
185
|
+
|
|
186
|
+
```ts
|
|
187
|
+
import express from "express";
|
|
188
|
+
import { daywalkerAuth, daywalkerKeys } from "@shelbyb/daywalker-sdk/express";
|
|
189
|
+
|
|
190
|
+
const app = express();
|
|
191
|
+
app.use(daywalkerKeys()); // serves GET /.well-known/daywalker-keys.json
|
|
192
|
+
app.use("/api", daywalkerAuth({ serviceSlug: "sonalyze" }));
|
|
193
|
+
|
|
194
|
+
app.get("/api/me", (req, res) => res.json({ auth: req.auth }));
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
`daywalkerAuth` reads `Authorization: Bearer <token>` or `x-api-token`, answers `401` when neither is present, and otherwise returns the same status codes as the other middleware (`429` with `Retry-After`, `503` when Daywalker is unreachable). On success the validated payload is on `req.auth`. Both functions also work with plain `http.createServer` handlers.
|
|
198
|
+
|
|
199
|
+
---
|
|
200
|
+
|
|
201
|
+
## One API Key, Several Services
|
|
202
|
+
|
|
203
|
+
A Daywalker API token can cover several services (say `gigpulse` and `devpulse`) and carry one allowlist of domains and IPs, so you manage a single set of client credentials instead of one per service. Ask for the extra services when you create the token in the Daywalker dashboard (**Also cover these services**), or with `additionalServiceIds` on `POST /api/v1/tokens` and `PATCH /api/v1/tokens/:id`.
|
|
204
|
+
|
|
205
|
+
**Each app keeps its own service slug, with the same API key.** Every app (domain) generates and registers its own signing key:
|
|
206
|
+
|
|
207
|
+
```bash
|
|
208
|
+
# gigpulse app
|
|
209
|
+
DAYWALKER_API_KEY=srv_live_… npx daywalker init --service gigpulse --domain gigpulse.daywalker.dev
|
|
210
|
+
# devpulse app, same API key
|
|
211
|
+
DAYWALKER_API_KEY=srv_live_… npx daywalker init --service devpulse --domain devpulse.daywalker.dev
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
`init` and `rotate` fail right away with `service_not_covered` if the API key doesn't cover the slug you name. An app that serves several services lists them all, `--service gigpulse,devpulse` (`DAYWALKER_SERVICE_SLUG="gigpulse,devpulse"`); the first is the default for `validateToken()`, and `client.config.serviceSlugs` holds the list.
|
|
215
|
+
|
|
216
|
+
**The JWT lists every service.** Once your backend has authenticated with its signed request, ask for a lease. It lists all the service slugs the token covers (and may use right now):
|
|
217
|
+
|
|
218
|
+
```ts
|
|
219
|
+
const lease = await client.issueLease();
|
|
220
|
+
lease.services; // ["gigpulse", "devpulse"]; also the JWT's `services` and `aud` claims
|
|
221
|
+
|
|
222
|
+
// Least privilege: only what this process needs. A service the key doesn't cover is refused (403 SERVICE_NOT_GRANTED).
|
|
223
|
+
const narrow = await client.issueLease({ services: ["devpulse"], ttlSeconds: 3600 });
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
Services that are inactive, in `DRAFT`, `TESTING` (unless an admin owns the token), or no longer entitled to the token's owner are left out. Check `lease.services` rather than assuming.
|
|
227
|
+
|
|
228
|
+
Whoever receives the lease verifies it for **their own** service. It only passes when that slug is in the lease's `services` and `aud`:
|
|
229
|
+
|
|
230
|
+
```ts
|
|
231
|
+
const verifier = new Daywalker({ serviceSlug: "devpulse", daywalkerPublicKey: process.env.DAYWALKER_PUBLIC_KEY });
|
|
232
|
+
const result = await verifier.validateToken(incomingJwt);
|
|
233
|
+
if (result.ok) result.data.services; // ["gigpulse", "devpulse"]: everything this lease covers
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
Online validation of an `srv_live_…` token reports the same list in `result.data.services` (a server that predates multi-service tokens yields just the validated slug).
|
|
237
|
+
|
|
238
|
+
---
|
|
239
|
+
|
|
180
240
|
## Zero-Egress Offline Verification
|
|
181
241
|
|
|
182
242
|
When high-throughput internal microservices need zero latency and zero external egress, Daywalker Auth can issue asymmetric JWT leases. Configure `DAYWALKER_PUBLIC_KEY` (PEM or standard JWK) and token validation runs entirely in memory without network calls:
|
|
@@ -193,7 +253,7 @@ const client = new Daywalker({
|
|
|
193
253
|
const result = await client.validateToken(incomingJwt);
|
|
194
254
|
```
|
|
195
255
|
|
|
196
|
-
Leases must have a numeric `exp` and an `aud` (or `serviceSlug`)
|
|
256
|
+
Leases must have a numeric `exp` and an `aud` (or `serviceSlug`) containing your service slug, so `serviceSlug` must be configured. A lease that lists `services` must list yours too. `result.data.services` is every service the lease covers (see [One API key, several services](#one-api-key-several-services)). Set `leaseIssuer` (or `DAYWALKER_LEASE_ISSUER`) to also check `iss`.
|
|
197
257
|
|
|
198
258
|
---
|
|
199
259
|
|
|
@@ -201,9 +261,9 @@ Leases must have a numeric `exp` and an `aud` (or `serviceSlug`) equal to your s
|
|
|
201
261
|
|
|
202
262
|
| Command | Description |
|
|
203
263
|
|---|---|
|
|
204
|
-
| `npx daywalker init --service <slug> [--domain <host>] [--api-key-stdin] [--skip-register] [--force] [--json]` | Generate Ed25519 key, register with Daywalker Auth, and write `.env.local` or output JSON |
|
|
264
|
+
| `npx daywalker init --service <slug[,slug…]> [--domain <host>] [--api-key-stdin] [--skip-register] [--force] [--json]` | Generate Ed25519 key, register with Daywalker Auth, and write `.env.local` or output JSON |
|
|
205
265
|
| `npx daywalker check` | Validate configuration, signature readiness, and connection |
|
|
206
|
-
| `npx daywalker rotate [--api-key-stdin] [--skip-register] [--json]` | Generate and register a new key while preserving the previous key in `.env.local` |
|
|
266
|
+
| `npx daywalker rotate [--service <slug[,slug…]>] [--api-key-stdin] [--skip-register] [--json]` | Generate and register a new key while preserving the previous key in `.env.local` |
|
|
207
267
|
|
|
208
268
|
---
|
|
209
269
|
|
|
@@ -212,8 +272,8 @@ Leases must have a numeric `exp` and an `aud` (or `serviceSlug`) equal to your s
|
|
|
212
272
|
| Endpoint | SDK usage | Notes |
|
|
213
273
|
|---|---|---|
|
|
214
274
|
| `POST /api/v1/tokens/validate` | `client.validateToken()` | Body `{ token, serviceSlug }`; signed with `Signature-Input`, `Signature`, `Content-Digest`. Errors: `400` (`DIGEST_MISMATCH`/missing fields), `401` (`INVALID_SIGNATURE`/invalid token), `403`, `429`. Response includes `valid`, `tokenId`, `tokenName`, `userId`, `stage`, `rateLimit`, `rateLimitHourly` and `X-RateLimit-*` / `X-RateLimit-Hourly-*` headers |
|
|
215
|
-
| `POST /api/v1/keys/register` | `npx daywalker init` / `rotate` | Bearer auth; body `{ keyId, domain, publicKey }` |
|
|
216
|
-
| `POST /api/v1/leases/issue` | `client.issueLease(ttlSeconds?)` | Signed request returns `{ lease, tokenType, expiresAt, jwksUri }` (EdDSA JWT) |
|
|
275
|
+
| `POST /api/v1/keys/register` | `npx daywalker init` / `rotate` | Bearer auth; body `{ keyId, domain, publicKey, serviceSlug? }`; `403 service_not_covered` if the key doesn't cover `serviceSlug` |
|
|
276
|
+
| `POST /api/v1/leases/issue` | `client.issueLease(ttlSeconds? \| { ttlSeconds?, services? })` | Signed request returns `{ lease, tokenType, expiresAt, jwksUri, services }` (EdDSA JWT listing every service the key covers; `services` narrows it) |
|
|
217
277
|
| `GET /.well-known/jwks.json` | `client.fetchJwks()` | Daywalker public JWKS for offline lease verification |
|
|
218
278
|
|
|
219
279
|
Inbound tokens may be supplied as `x-api-token: srv_live_...` or `Authorization: Bearer <JWT or API key>`.
|
|
@@ -257,6 +317,18 @@ const analytics = createAnalytics({ writeKey: "dpa_…", host: "https://devpulse
|
|
|
257
317
|
|
|
258
318
|
---
|
|
259
319
|
|
|
320
|
+
## Provenance & Releases
|
|
321
|
+
|
|
322
|
+
Releases are published from GitHub Actions ([`publish.yml`](./.github/workflows/publish.yml)) with [npm trusted publishing](https://docs.npmjs.com/trusted-publishers): npm accepts a short-lived OIDC token from this repository's workflow, so there is no long-lived npm token anywhere. Each version carries a signed [provenance attestation](https://docs.npmjs.com/generating-provenance-statements) linking it to the commit and workflow run that built it. Check it on the npm page, or after installing:
|
|
323
|
+
|
|
324
|
+
```bash
|
|
325
|
+
npm audit signatures
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
To cut a release (maintainers): bump `version` in `package.json`, add a `CHANGELOG.md` entry, merge to `main`, then either push a matching tag (`git tag v0.5.0 && git push origin v0.5.0`) or run the **Publish** workflow from `main` with the version. The workflow refuses tags not on `main`, versions that don't match `package.json`, and versions already on npm. Tests and packing run in a job without the OIDC token; only the final job, which uploads the tested tarball, can publish.
|
|
329
|
+
|
|
330
|
+
---
|
|
331
|
+
|
|
260
332
|
## License
|
|
261
333
|
|
|
262
334
|
MIT
|
package/dist/cli.js
CHANGED
|
@@ -2,13 +2,13 @@
|
|
|
2
2
|
import fs from "node:fs";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { createDaywalker } from "./client.js";
|
|
5
|
-
import { publicHostname } from "./config.js";
|
|
5
|
+
import { parseServiceSlugs, publicHostname } from "./config.js";
|
|
6
6
|
import { generateSigningKey, loadSigningKey, registerPublicKey, redactSecrets } from "./crypto/index.js";
|
|
7
7
|
const HELP = `daywalker: connect a service to Daywalker Auth (RFC 9421 / Push Architecture)
|
|
8
8
|
|
|
9
9
|
Usage:
|
|
10
|
-
daywalker init --service <slug> [--api-key-stdin] [--domain <host>] [--auth-url <url>] [--force] [--skip-register] [--json|--export] [--print-key]
|
|
11
|
-
daywalker rotate [--service <slug>] [--api-key-stdin] [--domain <host>] [--auth-url <url>] [--skip-register] [--json|--export] [--print-key]
|
|
10
|
+
daywalker init --service <slug[,slug…]> [--api-key-stdin] [--domain <host>] [--auth-url <url>] [--force] [--skip-register] [--json|--export] [--print-key]
|
|
11
|
+
daywalker rotate [--service <slug[,slug…]>] [--api-key-stdin] [--domain <host>] [--auth-url <url>] [--skip-register] [--json|--export] [--print-key]
|
|
12
12
|
daywalker check
|
|
13
13
|
|
|
14
14
|
init Generates an Ed25519 keypair, registers the public key directly with Daywalker Auth
|
|
@@ -180,12 +180,11 @@ async function init(opts) {
|
|
|
180
180
|
const dir = process.cwd();
|
|
181
181
|
const envFile = path.join(dir, ".env.local");
|
|
182
182
|
const existing = readEnvFile(envFile);
|
|
183
|
-
const
|
|
184
|
-
|
|
185
|
-
.
|
|
186
|
-
if (!slug || !/^[a-z0-9_-]+$/.test(slug)) {
|
|
187
|
-
fail("--service must be this service's Daywalker slug (e.g. --service sonalyze)");
|
|
183
|
+
const slugs = parseServiceSlugs(String(opts.service ?? existing.get("DAYWALKER_SERVICE_SLUG") ?? process.env.DAYWALKER_SERVICE_SLUG ?? ""));
|
|
184
|
+
if (slugs.length === 0 || !slugs.every((s) => /^[a-z0-9_-]+$/.test(s))) {
|
|
185
|
+
fail("--service must be this service's Daywalker slug (e.g. --service sonalyze), or a comma-separated list when one API key covers several (--service gigpulse,devpulse)");
|
|
188
186
|
}
|
|
187
|
+
const slug = slugs.join(",");
|
|
189
188
|
const rawDomain = opts.domain ? String(opts.domain) : existing.get("DAYWALKER_DOMAIN") ?? process.env.DAYWALKER_DOMAIN ?? "";
|
|
190
189
|
let domain = null;
|
|
191
190
|
if (rawDomain) {
|
|
@@ -268,7 +267,7 @@ async function rotate(opts) {
|
|
|
268
267
|
const current = existing.get("DAYWALKER_SIGNING_KEY") ?? process.env.DAYWALKER_SIGNING_KEY;
|
|
269
268
|
if (!current)
|
|
270
269
|
fail("No DAYWALKER_SIGNING_KEY found; run `daywalker init` first");
|
|
271
|
-
const slug = String(opts.service ?? existing.get("DAYWALKER_SERVICE_SLUG") ?? process.env.DAYWALKER_SERVICE_SLUG ?? "")
|
|
270
|
+
const slug = parseServiceSlugs(String(opts.service ?? existing.get("DAYWALKER_SERVICE_SLUG") ?? process.env.DAYWALKER_SERVICE_SLUG ?? "")).join(",");
|
|
272
271
|
const apiKey = await resolveApiKey(opts, existing);
|
|
273
272
|
const authUrl = String(opts["auth-url"] ?? existing.get("DAYWALKER_AUTH_URL") ?? process.env.DAYWALKER_AUTH_URL ?? "https://auth.daywalker.dev").replace(/\/+$/, "");
|
|
274
273
|
const rotateDomain = publicHostname(String(opts.domain ?? existing.get("DAYWALKER_DOMAIN") ?? process.env.DAYWALKER_DOMAIN ?? ""));
|
|
@@ -317,7 +316,9 @@ async function check() {
|
|
|
317
316
|
const result = await client.selfCheck();
|
|
318
317
|
if (result.ok) {
|
|
319
318
|
console.log(`✔ Daywalker configuration looks good`);
|
|
320
|
-
if (result.
|
|
319
|
+
if (result.serviceSlugs.length > 1)
|
|
320
|
+
console.log(` • Services: ${result.serviceSlugs.join(", ")} (default: ${result.serviceSlug})`);
|
|
321
|
+
else if (result.serviceSlug)
|
|
321
322
|
console.log(` • Service slug: ${result.serviceSlug}`);
|
|
322
323
|
if (result.domain)
|
|
323
324
|
console.log(` • Caller domain: ${result.domain}`);
|
package/dist/client.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type ResolvedConfig } from "./config.js";
|
|
2
|
-
import { type DaywalkerConfig, type PublicJwk, type RetryOptions, type SelfCheckResult, type TokenPayload, type ValidateResult } from "./types.js";
|
|
2
|
+
import { type DaywalkerConfig, type IssueLeaseOptions, type LeaseResponse, type PublicJwk, type RetryOptions, type SelfCheckResult, type TokenPayload, type ValidateResult } from "./types.js";
|
|
3
3
|
/** Validation results kept in memory; the least recently used are evicted past this. */
|
|
4
4
|
export declare const MAX_VALIDATION_CACHE_ENTRIES = 1000;
|
|
5
5
|
/** Longest 429 `Retry-After` waited out by default (`retry.maxRetryAfterMs`); longer ones return the 429. */
|
|
@@ -85,13 +85,10 @@ export declare class Daywalker {
|
|
|
85
85
|
}): Promise<TokenPayload>;
|
|
86
86
|
/**
|
|
87
87
|
* Exchanges an RFC 9421 signed request for a short-lived EdDSA JWT lease (POST /api/v1/leases/issue).
|
|
88
|
+
* The lease lists every service slug the API key covers (`services`, also the JWT's `services` and `aud`
|
|
89
|
+
* claims), so one set of credentials serves all of them. Pass `{ services: [...] }` to narrow it.
|
|
88
90
|
*/
|
|
89
|
-
issueLease(
|
|
90
|
-
lease: string;
|
|
91
|
-
tokenType?: string;
|
|
92
|
-
expiresAt?: string;
|
|
93
|
-
jwksUri?: string;
|
|
94
|
-
}>;
|
|
91
|
+
issueLease(options?: number | IssueLeaseOptions): Promise<LeaseResponse>;
|
|
95
92
|
/**
|
|
96
93
|
* Fetches Daywalker's public JWKS (GET /.well-known/jwks.json) for offline lease verification.
|
|
97
94
|
* Uses the configured `fetch`, sends no credentials, never follows redirects and times out after `timeoutMs`.
|
package/dist/client.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { resolveConfig } from "./config.js";
|
|
2
|
-
import { loadSigningKey, signRequest, verifyJwtLease, maskSecret, redactSecrets, sha256Base64Url, } from "./crypto/index.js";
|
|
1
|
+
import { parseServiceSlugs, resolveConfig } from "./config.js";
|
|
2
|
+
import { loadSigningKey, signRequest, verifyJwtLease, leaseServices, maskSecret, redactSecrets, sha256Base64Url, } from "./crypto/index.js";
|
|
3
3
|
import { DaywalkerAuthError, DaywalkerError, DaywalkerRateLimitError, } from "./types.js";
|
|
4
4
|
function parseRetryAfter(headerValue) {
|
|
5
5
|
if (!headerValue)
|
|
@@ -159,6 +159,9 @@ function malformedValidation(json, payloadData) {
|
|
|
159
159
|
if (src[k] !== undefined && src[k] !== null && typeof src[k] !== "string")
|
|
160
160
|
return `${k} is not a string`;
|
|
161
161
|
}
|
|
162
|
+
if (src.services !== undefined && src.services !== null && !(Array.isArray(src.services) && src.services.every((s) => typeof s === "string"))) {
|
|
163
|
+
return "services is not a list of strings";
|
|
164
|
+
}
|
|
162
165
|
for (const k of ["rateLimit", "rateLimitHourly"]) {
|
|
163
166
|
if (src[k] !== undefined && src[k] !== null && !isRateLimitWindow(src[k]))
|
|
164
167
|
return `${k} is not a rate limit window`;
|
|
@@ -416,7 +419,7 @@ export class Daywalker {
|
|
|
416
419
|
const result = {
|
|
417
420
|
ok: true,
|
|
418
421
|
data: {
|
|
419
|
-
serviceSlug: serviceSlug || (payload.serviceSlug || payload.
|
|
422
|
+
serviceSlug: serviceSlug || (payload.serviceSlug || payload.service || ""),
|
|
420
423
|
tenantId: (payload.tenantId || payload.sub || ""),
|
|
421
424
|
rateLimit: payload.rateLimit ?? {
|
|
422
425
|
limit: 1000,
|
|
@@ -430,6 +433,8 @@ export class Daywalker {
|
|
|
430
433
|
stage: payload.stage,
|
|
431
434
|
offlineVerified: true,
|
|
432
435
|
...payload,
|
|
436
|
+
// Every service the lease covers: its `services` claim, else its audience.
|
|
437
|
+
services: leaseServices(payload),
|
|
433
438
|
},
|
|
434
439
|
};
|
|
435
440
|
if (this._config.cacheTtlMs > 0) {
|
|
@@ -535,6 +540,7 @@ export class Daywalker {
|
|
|
535
540
|
userId: (payloadData.userId || json.userId),
|
|
536
541
|
stage: (payloadData.stage || json.stage),
|
|
537
542
|
...payloadData,
|
|
543
|
+
services: (payloadData.services ?? json.services) ?? [json.serviceSlug || serviceSlug],
|
|
538
544
|
};
|
|
539
545
|
const result = {
|
|
540
546
|
ok: true,
|
|
@@ -604,17 +610,29 @@ export class Daywalker {
|
|
|
604
610
|
}
|
|
605
611
|
/**
|
|
606
612
|
* Exchanges an RFC 9421 signed request for a short-lived EdDSA JWT lease (POST /api/v1/leases/issue).
|
|
613
|
+
* The lease lists every service slug the API key covers (`services`, also the JWT's `services` and `aud`
|
|
614
|
+
* claims), so one set of credentials serves all of them. Pass `{ services: [...] }` to narrow it.
|
|
607
615
|
*/
|
|
608
|
-
async issueLease(
|
|
616
|
+
async issueLease(options) {
|
|
617
|
+
const opts = typeof options === "number" ? { ttlSeconds: options } : (options ?? {});
|
|
618
|
+
const services = opts.services ? parseServiceSlugs(opts.services) : undefined;
|
|
619
|
+
if (opts.services && (!services || services.length === 0)) {
|
|
620
|
+
throw new DaywalkerAuthError("issueLease: services must list at least one service slug", 400, "INVALID_REQUEST");
|
|
621
|
+
}
|
|
609
622
|
const res = await this.fetch("/api/v1/leases/issue", {
|
|
610
623
|
method: "POST",
|
|
611
|
-
body: ttlSeconds ? { ttlSeconds } : {},
|
|
624
|
+
body: { ...(opts.ttlSeconds ? { ttlSeconds: opts.ttlSeconds } : {}), ...(services ? { services } : {}) },
|
|
612
625
|
});
|
|
613
626
|
const json = (await res.json().catch(() => ({})));
|
|
614
627
|
if (res.status !== 201 && res.status !== 200) {
|
|
615
628
|
throw new DaywalkerAuthError(redactSecrets(String(json.message || json.error || `Lease issue failed with HTTP ${res.status}`)), res.status, json.error);
|
|
616
629
|
}
|
|
617
|
-
|
|
630
|
+
if (typeof json.lease !== "string") {
|
|
631
|
+
throw new DaywalkerAuthError("Daywalker Auth returned a malformed lease response (no lease)", res.status, "SERVICE_UNAVAILABLE");
|
|
632
|
+
}
|
|
633
|
+
// Servers from before multi-service tokens don't list services; their lease covers the one configured slug.
|
|
634
|
+
const listed = Array.isArray(json.services) ? json.services.filter((s) => typeof s === "string") : [];
|
|
635
|
+
return { ...json, lease: json.lease, services: listed.length > 0 ? listed : this._config.serviceSlug ? [this._config.serviceSlug] : [] };
|
|
618
636
|
}
|
|
619
637
|
/**
|
|
620
638
|
* Fetches Daywalker's public JWKS (GET /.well-known/jwks.json) for offline lease verification.
|
|
@@ -685,6 +703,7 @@ export class Daywalker {
|
|
|
685
703
|
return {
|
|
686
704
|
ok: problems.length === 0,
|
|
687
705
|
serviceSlug: this._config.serviceSlug,
|
|
706
|
+
serviceSlugs: this._config.serviceSlugs,
|
|
688
707
|
domain: this._config.domain,
|
|
689
708
|
signing: Boolean(signingKey),
|
|
690
709
|
keyId,
|
package/dist/config.d.ts
CHANGED
|
@@ -5,7 +5,10 @@ export interface ResolvedConfig {
|
|
|
5
5
|
/** Never signs; `keys()` publishes its public JWK alongside the current key during rotation. */
|
|
6
6
|
previousSigningKey: string | SigningKey | null;
|
|
7
7
|
keyId: string | null;
|
|
8
|
+
/** The slug used when a call doesn't name one: the first of `serviceSlugs`. */
|
|
8
9
|
serviceSlug: string | null;
|
|
10
|
+
/** Every service this deployment is configured for (one API key can cover several). */
|
|
11
|
+
serviceSlugs: string[];
|
|
9
12
|
domain: string | null;
|
|
10
13
|
domainSource: string | null;
|
|
11
14
|
daywalkerPublicKey: string | PublicJwk | PublicJwks | null;
|
|
@@ -31,4 +34,9 @@ export declare function publicHostname(value: string | undefined | null): string
|
|
|
31
34
|
* so API keys, SDK keys and write keys never travel in cleartext.
|
|
32
35
|
*/
|
|
33
36
|
export declare function assertSecureUrl(name: string, value: string): void;
|
|
37
|
+
/**
|
|
38
|
+
* Service slugs from a comma- or whitespace-separated string (`DAYWALKER_SERVICE_SLUG="gigpulse,devpulse"`)
|
|
39
|
+
* or an array: lower-cased, de-duplicated, in the order given (the first is the default service).
|
|
40
|
+
*/
|
|
41
|
+
export declare function parseServiceSlugs(value: string | readonly string[] | null | undefined): string[];
|
|
34
42
|
export declare function resolveConfig(options?: DaywalkerConfig): ResolvedConfig;
|
package/dist/config.js
CHANGED
|
@@ -66,11 +66,20 @@ export function assertSecureUrl(name, value) {
|
|
|
66
66
|
return;
|
|
67
67
|
throw new Error(`${name} must use https:// (http:// is only allowed for localhost); got "${value}"`);
|
|
68
68
|
}
|
|
69
|
+
/**
|
|
70
|
+
* Service slugs from a comma- or whitespace-separated string (`DAYWALKER_SERVICE_SLUG="gigpulse,devpulse"`)
|
|
71
|
+
* or an array: lower-cased, de-duplicated, in the order given (the first is the default service).
|
|
72
|
+
*/
|
|
73
|
+
export function parseServiceSlugs(value) {
|
|
74
|
+
const parts = typeof value === "string" ? value.split(/[\s,]+/) : Array.isArray(value) ? value.flatMap((v) => String(v).split(/[\s,]+/)) : [];
|
|
75
|
+
return Array.from(new Set(parts.map((v) => v.trim().toLowerCase()).filter(Boolean)));
|
|
76
|
+
}
|
|
69
77
|
export function resolveConfig(options = {}) {
|
|
70
78
|
const env = options.env ?? (typeof process !== "undefined" && process.env ? process.env : {});
|
|
71
79
|
const problems = [];
|
|
72
80
|
const apiKey = (options.apiKey ?? env.DAYWALKER_API_KEY ?? "").trim() || null;
|
|
73
|
-
const
|
|
81
|
+
const serviceSlugs = parseServiceSlugs(options.serviceSlugs ?? options.serviceSlug ?? env.DAYWALKER_SERVICE_SLUG);
|
|
82
|
+
const serviceSlug = serviceSlugs[0] ?? null;
|
|
74
83
|
if (!apiKey)
|
|
75
84
|
problems.push("DAYWALKER_API_KEY is not set");
|
|
76
85
|
if (!serviceSlug)
|
|
@@ -123,6 +132,7 @@ export function resolveConfig(options = {}) {
|
|
|
123
132
|
previousSigningKey,
|
|
124
133
|
keyId,
|
|
125
134
|
serviceSlug,
|
|
135
|
+
serviceSlugs,
|
|
126
136
|
domain,
|
|
127
137
|
domainSource,
|
|
128
138
|
daywalkerPublicKey,
|
package/dist/crypto/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export { stringToUtf8, utf8ToString, bytesToBase64, base64ToBytes, bytesToBase64Url, base64UrlToBytes, base64UrlToBytesStrict, bytesToHex, hexToBytes, timingSafeEqual, timingSafeEqualString, maskSecret, redactSecrets, } from "./encoding.js";
|
|
2
2
|
export { DEFAULT_MAX_BODY_SIZE_BYTES, computeContentDigest, sha256Base64, sha256Base64Url, } from "./digest.js";
|
|
3
3
|
export { calculateJwkThumbprint, generateSigningKey, loadSigningKey, loadPublicKey, } from "./keys.js";
|
|
4
|
-
export { SIGNATURE_VERSION, SIGNATURE_LABEL, SIGNATURE_ALG, MAX_CLOCK_SKEW_SECONDS, REPLAY_CACHE_MAX_ENTRIES, SignatureReplayCache, formatSignatureParams, createSignatureBase, signRequest, verifyRequestSignature, verifyJwtLease, registerPublicKey, } from "./signatures.js";
|
|
4
|
+
export { SIGNATURE_VERSION, SIGNATURE_LABEL, SIGNATURE_ALG, MAX_CLOCK_SKEW_SECONDS, REPLAY_CACHE_MAX_ENTRIES, SignatureReplayCache, formatSignatureParams, createSignatureBase, signRequest, verifyRequestSignature, verifyJwtLease, leaseServices, registerPublicKey, } from "./signatures.js";
|
|
5
5
|
export type { SignedHeaders, SignRequestParams, VerifySignatureParams, VerifySignatureResult, ReplayStore, } from "./signatures.js";
|
package/dist/crypto/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
export { stringToUtf8, utf8ToString, bytesToBase64, base64ToBytes, bytesToBase64Url, base64UrlToBytes, base64UrlToBytesStrict, bytesToHex, hexToBytes, timingSafeEqual, timingSafeEqualString, maskSecret, redactSecrets, } from "./encoding.js";
|
|
2
2
|
export { DEFAULT_MAX_BODY_SIZE_BYTES, computeContentDigest, sha256Base64, sha256Base64Url, } from "./digest.js";
|
|
3
3
|
export { calculateJwkThumbprint, generateSigningKey, loadSigningKey, loadPublicKey, } from "./keys.js";
|
|
4
|
-
export { SIGNATURE_VERSION, SIGNATURE_LABEL, SIGNATURE_ALG, MAX_CLOCK_SKEW_SECONDS, REPLAY_CACHE_MAX_ENTRIES, SignatureReplayCache, formatSignatureParams, createSignatureBase, signRequest, verifyRequestSignature, verifyJwtLease, registerPublicKey, } from "./signatures.js";
|
|
4
|
+
export { SIGNATURE_VERSION, SIGNATURE_LABEL, SIGNATURE_ALG, MAX_CLOCK_SKEW_SECONDS, REPLAY_CACHE_MAX_ENTRIES, SignatureReplayCache, formatSignatureParams, createSignatureBase, signRequest, verifyRequestSignature, verifyJwtLease, leaseServices, registerPublicKey, } from "./signatures.js";
|
|
@@ -88,6 +88,11 @@ export declare function signRequest(params: SignRequestParams): Promise<SignedHe
|
|
|
88
88
|
* Replay protection uses `replayStore` (default: per-process memory; use a shared store across instances).
|
|
89
89
|
*/
|
|
90
90
|
export declare function verifyRequestSignature(params: VerifySignatureParams): Promise<VerifySignatureResult>;
|
|
91
|
+
/**
|
|
92
|
+
* Every service slug a (verified) JWT lease covers: its `services` claim, else its audience, else the
|
|
93
|
+
* single `serviceSlug` / `service` claim of leases issued before multi-service tokens.
|
|
94
|
+
*/
|
|
95
|
+
export declare function leaseServices(payload: Record<string, unknown>): string[];
|
|
91
96
|
/** Clock skew tolerated on JWT lease `exp` / `nbf`. */
|
|
92
97
|
export declare const JWT_CLOCK_SKEW_SECONDS = 60;
|
|
93
98
|
/**
|
|
@@ -405,6 +405,16 @@ export async function verifyRequestSignature(params) {
|
|
|
405
405
|
return { valid: false, error: `Verification error: ${err.message}` };
|
|
406
406
|
}
|
|
407
407
|
}
|
|
408
|
+
/**
|
|
409
|
+
* Every service slug a (verified) JWT lease covers: its `services` claim, else its audience, else the
|
|
410
|
+
* single `serviceSlug` / `service` claim of leases issued before multi-service tokens.
|
|
411
|
+
*/
|
|
412
|
+
export function leaseServices(payload) {
|
|
413
|
+
const strings = (v) => (Array.isArray(v) ? v : v === undefined || v === null ? [] : [v]).filter((s) => typeof s === "string" && s !== "");
|
|
414
|
+
const listed = strings(payload.services);
|
|
415
|
+
const fallback = listed.length > 0 ? listed : strings(payload.aud).length > 0 ? strings(payload.aud) : strings(payload.serviceSlug ?? payload.service);
|
|
416
|
+
return Array.from(new Set(fallback.map((s) => s.toLowerCase())));
|
|
417
|
+
}
|
|
408
418
|
/** Clock skew tolerated on JWT lease `exp` / `nbf`. */
|
|
409
419
|
export const JWT_CLOCK_SKEW_SECONDS = 60;
|
|
410
420
|
/**
|
|
@@ -490,6 +500,16 @@ export async function verifyJwtLease(params) {
|
|
|
490
500
|
error: `JWT audience mismatch: expected ${expected}, got ${audiences.join(", ").toLowerCase()}`,
|
|
491
501
|
};
|
|
492
502
|
}
|
|
503
|
+
// A lease that lists its services must list this one (belt and braces next to `aud`).
|
|
504
|
+
if (payload.services !== undefined) {
|
|
505
|
+
const listed = Array.isArray(payload.services) ? payload.services : null;
|
|
506
|
+
if (!listed || !listed.some((s) => typeof s === "string" && s.toLowerCase() === expected)) {
|
|
507
|
+
return {
|
|
508
|
+
valid: false,
|
|
509
|
+
error: `JWT services mismatch: expected ${expected}, got ${listed ? listed.map(String).join(", ").toLowerCase() : "(not a list)"}`,
|
|
510
|
+
};
|
|
511
|
+
}
|
|
512
|
+
}
|
|
493
513
|
if (params.expectedIssuer && payload.iss !== params.expectedIssuer) {
|
|
494
514
|
return { valid: false, error: `JWT issuer mismatch: expected ${params.expectedIssuer}, got ${payload.iss ?? "(none)"}` };
|
|
495
515
|
}
|
|
@@ -527,6 +547,8 @@ export async function registerPublicKey(params) {
|
|
|
527
547
|
keyId: params.publicJwk.kid,
|
|
528
548
|
domain: params.domain,
|
|
529
549
|
publicKey: params.publicJwk,
|
|
550
|
+
// Lets Daywalker refuse right away when the API key doesn't cover the service(s); a comma-separated list is fine
|
|
551
|
+
...(params.serviceSlug ? { serviceSlug: params.serviceSlug } : {}),
|
|
530
552
|
}),
|
|
531
553
|
});
|
|
532
554
|
if (res.ok) {
|
package/dist/index.d.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
export { Daywalker, createDaywalker, daywalker, MAX_RETRY_AFTER_MS, MAX_VALIDATION_CACHE_ENTRIES, } from "./client.js";
|
|
2
2
|
export type { DaywalkerFetchInit, } from "./client.js";
|
|
3
3
|
export { DaywalkerError, DaywalkerAuthError, DaywalkerRateLimitError, UNAVAILABLE_ERROR_CODES, } from "./types.js";
|
|
4
|
-
export type { DaywalkerConfig, RateLimitWindow, TokenPayload, ValidateErrorCode, ValidateResult, RetryOptions, SelfCheckResult, PublicJwk, PublicJwks, SigningKey, } from "./types.js";
|
|
5
|
-
export { resolveConfig, publicHostname, assertSecureUrl, DEFAULT_TIMEOUT_MS, } from "./config.js";
|
|
4
|
+
export type { DaywalkerConfig, RateLimitWindow, TokenPayload, ValidateErrorCode, ValidateResult, RetryOptions, SelfCheckResult, LeaseResponse, IssueLeaseOptions, PublicJwk, PublicJwks, SigningKey, } from "./types.js";
|
|
5
|
+
export { resolveConfig, parseServiceSlugs, publicHostname, assertSecureUrl, DEFAULT_TIMEOUT_MS, } from "./config.js";
|
|
6
6
|
export type { ResolvedConfig, } from "./config.js";
|
|
7
|
-
export { SIGNATURE_VERSION, SIGNATURE_LABEL, SIGNATURE_ALG, MAX_CLOCK_SKEW_SECONDS, REPLAY_CACHE_MAX_ENTRIES, SignatureReplayCache, computeContentDigest, sha256Base64, sha256Base64Url, calculateJwkThumbprint, generateSigningKey, loadSigningKey, loadPublicKey, formatSignatureParams, createSignatureBase, signRequest, verifyRequestSignature, verifyJwtLease, registerPublicKey, stringToUtf8, utf8ToString, bytesToBase64, base64ToBytes, bytesToBase64Url, base64UrlToBytes, base64UrlToBytesStrict, bytesToHex, hexToBytes, maskSecret, redactSecrets, } from "./crypto/index.js";
|
|
7
|
+
export { SIGNATURE_VERSION, SIGNATURE_LABEL, SIGNATURE_ALG, MAX_CLOCK_SKEW_SECONDS, REPLAY_CACHE_MAX_ENTRIES, SignatureReplayCache, computeContentDigest, sha256Base64, sha256Base64Url, calculateJwkThumbprint, generateSigningKey, loadSigningKey, loadPublicKey, formatSignatureParams, createSignatureBase, signRequest, verifyRequestSignature, verifyJwtLease, leaseServices, registerPublicKey, stringToUtf8, utf8ToString, bytesToBase64, base64ToBytes, bytesToBase64Url, base64UrlToBytes, base64UrlToBytesStrict, bytesToHex, hexToBytes, maskSecret, redactSecrets, } from "./crypto/index.js";
|
|
8
8
|
export type { SignedHeaders, SignRequestParams, VerifySignatureParams, VerifySignatureResult, ReplayStore, } from "./crypto/index.js";
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
export { Daywalker, createDaywalker, daywalker, MAX_RETRY_AFTER_MS, MAX_VALIDATION_CACHE_ENTRIES, } from "./client.js";
|
|
2
2
|
export { DaywalkerError, DaywalkerAuthError, DaywalkerRateLimitError, UNAVAILABLE_ERROR_CODES, } from "./types.js";
|
|
3
|
-
export { resolveConfig, publicHostname, assertSecureUrl, DEFAULT_TIMEOUT_MS, } from "./config.js";
|
|
4
|
-
export { SIGNATURE_VERSION, SIGNATURE_LABEL, SIGNATURE_ALG, MAX_CLOCK_SKEW_SECONDS, REPLAY_CACHE_MAX_ENTRIES, SignatureReplayCache, computeContentDigest, sha256Base64, sha256Base64Url, calculateJwkThumbprint, generateSigningKey, loadSigningKey, loadPublicKey, formatSignatureParams, createSignatureBase, signRequest, verifyRequestSignature, verifyJwtLease, registerPublicKey, stringToUtf8, utf8ToString, bytesToBase64, base64ToBytes, bytesToBase64Url, base64UrlToBytes, base64UrlToBytesStrict, bytesToHex, hexToBytes, maskSecret, redactSecrets, } from "./crypto/index.js";
|
|
3
|
+
export { resolveConfig, parseServiceSlugs, publicHostname, assertSecureUrl, DEFAULT_TIMEOUT_MS, } from "./config.js";
|
|
4
|
+
export { SIGNATURE_VERSION, SIGNATURE_LABEL, SIGNATURE_ALG, MAX_CLOCK_SKEW_SECONDS, REPLAY_CACHE_MAX_ENTRIES, SignatureReplayCache, computeContentDigest, sha256Base64, sha256Base64Url, calculateJwkThumbprint, generateSigningKey, loadSigningKey, loadPublicKey, formatSignatureParams, createSignatureBase, signRequest, verifyRequestSignature, verifyJwtLease, leaseServices, registerPublicKey, stringToUtf8, utf8ToString, bytesToBase64, base64ToBytes, bytesToBase64Url, base64UrlToBytes, base64UrlToBytesStrict, bytesToHex, hexToBytes, maskSecret, redactSecrets, } from "./crypto/index.js";
|
package/dist/types.d.ts
CHANGED
|
@@ -12,6 +12,11 @@ export interface TokenPayload {
|
|
|
12
12
|
tokenName?: string;
|
|
13
13
|
userId?: string;
|
|
14
14
|
stage?: string;
|
|
15
|
+
/**
|
|
16
|
+
* Every service slug the token (or JWT lease) covers. One API key can cover several services, so this
|
|
17
|
+
* can hold more than `serviceSlug`, which is the service this call validated for.
|
|
18
|
+
*/
|
|
19
|
+
services?: string[];
|
|
15
20
|
offlineVerified?: boolean;
|
|
16
21
|
[key: string]: unknown;
|
|
17
22
|
}
|
|
@@ -76,7 +81,13 @@ export interface DaywalkerConfig {
|
|
|
76
81
|
*/
|
|
77
82
|
previousSigningKey?: string | SigningKey;
|
|
78
83
|
keyId?: string;
|
|
84
|
+
/**
|
|
85
|
+
* The service this deployment validates tokens for (env DAYWALKER_SERVICE_SLUG). One API key can cover
|
|
86
|
+
* several services: a comma-separated list ("gigpulse,devpulse") is accepted, and the first is the default.
|
|
87
|
+
*/
|
|
79
88
|
serviceSlug?: string;
|
|
89
|
+
/** Same as a comma-separated `serviceSlug`, as an array. Takes precedence over `serviceSlug`. */
|
|
90
|
+
serviceSlugs?: string[];
|
|
80
91
|
domain?: string;
|
|
81
92
|
/** A JWK, a JWKS (`{ keys: [...] }`, selected by the lease's `kid`), PEM, or Base64 SPKI / raw key. */
|
|
82
93
|
daywalkerPublicKey?: string | PublicJwk | PublicJwks;
|
|
@@ -98,9 +109,30 @@ export interface DaywalkerConfig {
|
|
|
98
109
|
fetch?: typeof fetch;
|
|
99
110
|
env?: Record<string, string | undefined>;
|
|
100
111
|
}
|
|
112
|
+
/** Response of `client.issueLease()` (POST /api/v1/leases/issue). */
|
|
113
|
+
export interface LeaseResponse {
|
|
114
|
+
/** The signed EdDSA JWT. Verify it with Daywalker's public JWKS. */
|
|
115
|
+
lease: string;
|
|
116
|
+
tokenType?: string;
|
|
117
|
+
/** Unix seconds. */
|
|
118
|
+
expiresAt?: number | string;
|
|
119
|
+
jwksUri?: string;
|
|
120
|
+
/** Every service slug the lease covers (also its `services` and `aud` claims). */
|
|
121
|
+
services: string[];
|
|
122
|
+
}
|
|
123
|
+
export interface IssueLeaseOptions {
|
|
124
|
+
/** Lease lifetime in seconds (Daywalker clamps it to 1h-24h). */
|
|
125
|
+
ttlSeconds?: number;
|
|
126
|
+
/**
|
|
127
|
+
* Narrow the lease to these service slugs (least privilege). Each must be one the API key covers;
|
|
128
|
+
* otherwise Daywalker answers 403 `SERVICE_NOT_GRANTED`. Default: every service the key covers.
|
|
129
|
+
*/
|
|
130
|
+
services?: string[];
|
|
131
|
+
}
|
|
101
132
|
export interface SelfCheckResult {
|
|
102
133
|
ok: boolean;
|
|
103
134
|
serviceSlug: string | null;
|
|
135
|
+
serviceSlugs: string[];
|
|
104
136
|
domain: string | null;
|
|
105
137
|
signing: boolean;
|
|
106
138
|
keyId: string | null;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@shelbyb/daywalker-sdk",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Daywalker Auth service validation plus the DevPulse client SDKs (feature flags, OpenFeature, product analytics) in one
|
|
3
|
+
"version": "0.5.0",
|
|
4
|
+
"description": "Daywalker Auth service validation plus the DevPulse client SDKs (feature flags, OpenFeature, product analytics) in one package with a single runtime dependency (re2js).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
7
7
|
"types": "./dist/index.d.ts",
|
|
@@ -68,6 +68,7 @@
|
|
|
68
68
|
"files": [
|
|
69
69
|
"dist",
|
|
70
70
|
"docs",
|
|
71
|
+
"!docs/maintenance",
|
|
71
72
|
"README.md",
|
|
72
73
|
"LICENSE"
|
|
73
74
|
],
|