@shelbyb/daywalker-sdk 0.0.0-stage → 0.1.1
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/LICENSE +21 -0
- package/README.md +209 -2
- package/assets/logo-512.png +0 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +254 -0
- package/dist/client.d.ts +60 -0
- package/dist/client.js +376 -0
- package/dist/config.d.ts +21 -0
- package/dist/config.js +100 -0
- package/dist/crypto/digest.d.ts +17 -0
- package/dist/crypto/digest.js +36 -0
- package/dist/crypto/encoding.d.ts +29 -0
- package/dist/crypto/encoding.js +117 -0
- package/dist/crypto/index.d.ts +5 -0
- package/dist/crypto/index.js +4 -0
- package/dist/crypto/keys.d.ts +25 -0
- package/dist/crypto/keys.js +236 -0
- package/dist/crypto/signatures.d.ts +83 -0
- package/dist/crypto/signatures.js +331 -0
- package/dist/express.d.ts +25 -0
- package/dist/express.js +68 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +4 -0
- package/dist/middleware.d.ts +20 -0
- package/dist/middleware.js +72 -0
- package/dist/next.d.ts +63 -0
- package/dist/next.js +189 -0
- package/dist/signing.d.ts +3 -0
- package/dist/signing.js +2 -0
- package/dist/types.d.ts +122 -0
- package/dist/types.js +46 -0
- package/package.json +68 -4
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Shelby Brayton
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,210 @@
|
|
|
1
|
-
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src="./assets/logo-512.png" alt="Daywalker" width="160" />
|
|
3
|
+
</p>
|
|
2
4
|
|
|
3
|
-
|
|
5
|
+
# @shelbyb/daywalker-sdk
|
|
6
|
+
|
|
7
|
+
Zero-dependency, 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
|
+
|
|
9
|
+
Runs seamlessly across **Node.js 18+**, **Bun**, **Deno**, **Cloudflare Workers**, **Next.js Edge**, and standard browser runtimes.
|
|
10
|
+
|
|
11
|
+
- **Zero-Dependency Web Standards Core**: 100% standard `crypto.subtle`, `Uint8Array`, `fetch`, `Request`, and `Response`.
|
|
12
|
+
- **RFC 9421 (HTTP Message Signatures)**: Cryptographically binds `@method`, `@path`, `@authority`, and `content-digest` using Ed25519 signatures (`Signature-Input`, `Signature`).
|
|
13
|
+
- **RFC 9530 (Digest Fields)**: Computes standard `Content-Digest: sha-256=:<base64>:` headers for payload integrity.
|
|
14
|
+
- **Class-Based Client Architecture**: Instantiable `Daywalker` client supporting dependency injection, with a lazy default singleton for plug-and-play simplicity.
|
|
15
|
+
- **Resilience & Rate Limiting**: Automatic exponential backoff with jitter and `Retry-After` header parsing directly inside client fetch requests.
|
|
16
|
+
- **Deterministic Type-Safe Results**: Discriminated union returns (`Result<T, E>`) for clean handling without try/catch boilerplate.
|
|
17
|
+
- **Security-Hardened Next.js Middleware**: Built-in Server Action probe mitigation (CVE-2025-55182 defense), health check bypass, and self-healing diagnostic error routing.
|
|
18
|
+
- **Zero-Egress Inbound Verification**: In-memory caching (60–300s TTL) and offline JWT token lease validation using Daywalker's asymmetric public key.
|
|
19
|
+
- **Decoupled Push Architecture CLI**: `npx daywalker init --json` registers keys directly via `POST /api/v1/keys/register` with zero runtime reverse-HTTP calls.
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## Installation
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
npm install @shelbyb/daywalker-sdk
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
## Setup (once per service)
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
# Generate Ed25519 keypair and write .env.local
|
|
35
|
+
npx daywalker init --service my-service-slug
|
|
36
|
+
|
|
37
|
+
# Or register the public key immediately with Daywalker Auth:
|
|
38
|
+
npx daywalker init --service my-service-slug --api-key srv_live_...
|
|
39
|
+
|
|
40
|
+
# Pipe JSON secrets directly into Vercel, Fly.io, DigitalOcean, or AWS SSM:
|
|
41
|
+
npx daywalker init --service my-service-slug --api-key srv_live_... --json
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
`daywalker init`:
|
|
45
|
+
- Generates an Ed25519 keypair locally using WebCrypto (`crypto.subtle`)
|
|
46
|
+
- Registers the public JWK directly with `POST https://auth.daywalker.dev/api/v1/keys/register`
|
|
47
|
+
- Writes `DAYWALKER_SERVICE_SLUG`, `DAYWALKER_KEY_ID`, `DAYWALKER_SIGNING_KEY`, and `DAYWALKER_AUTH_URL` to `.env.local`
|
|
48
|
+
- No route injectors or public `/.well-known/` endpoints needed
|
|
49
|
+
|
|
50
|
+
The private key is never printed unless you pass `--print-key`.
|
|
51
|
+
|
|
52
|
+
---
|
|
53
|
+
|
|
54
|
+
## Environment
|
|
55
|
+
|
|
56
|
+
| Variable | Required | Purpose |
|
|
57
|
+
|---|---|---|
|
|
58
|
+
| `DAYWALKER_API_KEY` | ✅ | The service's Daywalker API token (`srv_live_…`) or JWT lease |
|
|
59
|
+
| `DAYWALKER_SERVICE_SLUG` | ✅ | The service slug to validate against (e.g. `sonalyze`) |
|
|
60
|
+
| `DAYWALKER_SIGNING_KEY` | for signed requests | Ed25519 private key from `daywalker init` (PKCS#8 Base64 DER or PEM) |
|
|
61
|
+
| `DAYWALKER_KEY_ID` | optional | RFC 7638 JWK thumbprint derived during key generation |
|
|
62
|
+
| `DAYWALKER_SIGNING_KEY_PREVIOUS` | during rotation | Previous private key, maintained during deployment transitions |
|
|
63
|
+
| `DAYWALKER_DOMAIN` | optional | Optional caller identifier or host domain name |
|
|
64
|
+
| `DAYWALKER_PUBLIC_KEY` | optional | Daywalker Auth public key for zero-egress offline JWT lease verification |
|
|
65
|
+
| `DAYWALKER_AUTH_URL` | optional | Defaults to `https://auth.daywalker.dev` |
|
|
66
|
+
|
|
67
|
+
---
|
|
68
|
+
|
|
69
|
+
## Usage
|
|
70
|
+
|
|
71
|
+
### Client Class & Instantiation
|
|
72
|
+
|
|
73
|
+
```ts
|
|
74
|
+
import { Daywalker, daywalker } from "@shelbyb/daywalker-sdk";
|
|
75
|
+
|
|
76
|
+
// Option 1: Instantiable client with custom options / dependency injection
|
|
77
|
+
const client = new Daywalker({
|
|
78
|
+
apiKey: process.env.DAYWALKER_API_KEY,
|
|
79
|
+
serviceSlug: "sonalyze",
|
|
80
|
+
maxRetries: 3,
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
// Option 2: Default lazy singleton (reads process.env automatically)
|
|
84
|
+
const result = await daywalker.validateToken();
|
|
85
|
+
|
|
86
|
+
if (result.ok) {
|
|
87
|
+
console.log("Validated tenant:", result.data.tenantId);
|
|
88
|
+
console.log("Rate limit remaining:", result.data.rateLimit.remaining);
|
|
89
|
+
} else {
|
|
90
|
+
console.error(`Validation failed (${result.status} ${result.error}): ${result.message}`);
|
|
91
|
+
}
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
### Type-Safe Verification & Error Handling
|
|
95
|
+
|
|
96
|
+
```ts
|
|
97
|
+
// requireValidToken returns data or throws DaywalkerAuthError / DaywalkerRateLimitError
|
|
98
|
+
try {
|
|
99
|
+
const auth = await client.requireValidToken(req.headers.get("x-api-token"));
|
|
100
|
+
console.log("Authenticated service:", auth.serviceSlug);
|
|
101
|
+
} catch (err) {
|
|
102
|
+
if (err instanceof DaywalkerRateLimitError) {
|
|
103
|
+
console.warn(`Rate limited. Retry after ${err.retryAfter}s`);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
### RFC 9421 Signed Requests with Automatic Backoff
|
|
109
|
+
|
|
110
|
+
```ts
|
|
111
|
+
// Outbound request automatically computes Content-Digest (RFC 9530)
|
|
112
|
+
// and attaches RFC 9421 signature headers. Retries 429 rate limits automatically.
|
|
113
|
+
const res = await client.fetch("/api/v1/tokens/validate", {
|
|
114
|
+
method: "POST",
|
|
115
|
+
body: { token: "user_token", serviceSlug: "other-service" },
|
|
116
|
+
});
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
---
|
|
120
|
+
|
|
121
|
+
## Hardened Next.js Middleware (`@shelbyb/daywalker-sdk/next`)
|
|
122
|
+
|
|
123
|
+
```ts
|
|
124
|
+
// middleware.ts
|
|
125
|
+
import { createDaywalkerMiddleware } from "@shelbyb/daywalker-sdk/next";
|
|
126
|
+
|
|
127
|
+
export const middleware = createDaywalkerMiddleware({
|
|
128
|
+
serviceSlug: "sonalyze",
|
|
129
|
+
errorPath: "/service-error", // redirect unauthorized web visitors
|
|
130
|
+
validateServerActions: true, // drops malformed CVE-2025-55182 scanner probes
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
export const config = {
|
|
134
|
+
matcher: ["/((?!_next/static|_next/image|favicon.ico).*)"],
|
|
135
|
+
};
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
### Route Handler Wrapper
|
|
139
|
+
|
|
140
|
+
```ts
|
|
141
|
+
// app/api/protected/route.ts
|
|
142
|
+
import { withDaywalkerAuth } from "@shelbyb/daywalker-sdk/next";
|
|
143
|
+
|
|
144
|
+
export const POST = withDaywalkerAuth(async (req, { auth }) => {
|
|
145
|
+
return Response.json({ message: "Hello " + auth.serviceSlug, auth });
|
|
146
|
+
});
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
---
|
|
150
|
+
|
|
151
|
+
## Web Standard & Hono Middleware (`@shelbyb/daywalker-sdk/middleware`)
|
|
152
|
+
|
|
153
|
+
```ts
|
|
154
|
+
import { Hono } from "hono";
|
|
155
|
+
import { daywalkerAuth } from "@shelbyb/daywalker-sdk/middleware";
|
|
156
|
+
|
|
157
|
+
const app = new Hono();
|
|
158
|
+
app.use("/api/*", daywalkerAuth());
|
|
159
|
+
|
|
160
|
+
app.get("/api/me", (c) => {
|
|
161
|
+
const auth = c.get("auth");
|
|
162
|
+
return c.json({ auth });
|
|
163
|
+
});
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
---
|
|
167
|
+
|
|
168
|
+
## Zero-Egress Offline Verification
|
|
169
|
+
|
|
170
|
+
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:
|
|
171
|
+
|
|
172
|
+
```ts
|
|
173
|
+
import { Daywalker } from "@shelbyb/daywalker-sdk";
|
|
174
|
+
|
|
175
|
+
const client = new Daywalker({
|
|
176
|
+
serviceSlug: "myservice",
|
|
177
|
+
daywalkerPublicKey: process.env.DAYWALKER_PUBLIC_KEY,
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
// Verifies JWT signature, expiration, and service claims locally
|
|
181
|
+
const result = await client.validateToken(incomingJwt);
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
---
|
|
185
|
+
|
|
186
|
+
## CLI Reference
|
|
187
|
+
|
|
188
|
+
| Command | Description |
|
|
189
|
+
|---|---|
|
|
190
|
+
| `npx daywalker init --service <slug> [--api-key <key>] [--json]` | Generate Ed25519 key, register with Daywalker Auth, and write `.env.local` or output JSON |
|
|
191
|
+
| `npx daywalker check` | Validate configuration, signature readiness, and connection |
|
|
192
|
+
| `npx daywalker rotate [--api-key <key>] [--json]` | Generate and register a new key while preserving the previous key in `.env.local` |
|
|
193
|
+
|
|
194
|
+
---
|
|
195
|
+
|
|
196
|
+
## Request Signing Specification (RFC 9421 & RFC 9530)
|
|
197
|
+
|
|
198
|
+
Each outbound request signed by `@shelbyb/daywalker-sdk` includes:
|
|
199
|
+
- `Content-Digest: sha-256=:<base64-encoded-hash>:`
|
|
200
|
+
- `Signature-Input: sig1=("@method" "@path" "@authority" "content-digest");created=<unix_seconds>;keyid="<kid>";alg="ed25519"`
|
|
201
|
+
- `Signature: sig1=:<base64-encoded-signature>:`
|
|
202
|
+
- `Authorization: Bearer <DAYWALKER_API_KEY>`
|
|
203
|
+
|
|
204
|
+
The exact signature base and test cases are verified against [`test-vectors/signing-rfc9421.json`](./test-vectors/signing-rfc9421.json).
|
|
205
|
+
|
|
206
|
+
---
|
|
207
|
+
|
|
208
|
+
## License
|
|
209
|
+
|
|
210
|
+
MIT
|
|
Binary file
|
package/dist/cli.d.ts
ADDED
package/dist/cli.js
ADDED
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { createDaywalker } from "./client.js";
|
|
5
|
+
import { publicHostname } from "./config.js";
|
|
6
|
+
import { generateSigningKey, loadSigningKey, registerPublicKey, redactSecrets } from "./crypto/index.js";
|
|
7
|
+
const HELP = `daywalker: connect a service to Daywalker Auth (RFC 9421 / Push Architecture)
|
|
8
|
+
|
|
9
|
+
Usage:
|
|
10
|
+
daywalker init --service <slug> [--api-key <key>] [--domain <host>] [--auth-url <url>] [--json|--export] [--print-key]
|
|
11
|
+
daywalker rotate [--service <slug>] [--api-key <key>] [--auth-url <url>] [--json|--export] [--print-key]
|
|
12
|
+
daywalker check
|
|
13
|
+
|
|
14
|
+
init Generates an Ed25519 keypair, registers the public key directly with Daywalker Auth
|
|
15
|
+
(POST /api/v1/keys/register), and writes DAYWALKER_* settings to .env.local (or outputs JSON with --json).
|
|
16
|
+
No public ingress or /.well-known route handlers are required.
|
|
17
|
+
rotate Generates a new Ed25519 key, registers it with Daywalker Auth, and keeps the old one
|
|
18
|
+
in .env.local as DAYWALKER_SIGNING_KEY_PREVIOUS during deployment transition.
|
|
19
|
+
check Validates configuration and confirms Daywalker Auth connection and signature readiness.
|
|
20
|
+
|
|
21
|
+
Options:
|
|
22
|
+
--json, --export Output configuration as JSON for piping into Vercel, DigitalOcean, Fly.io, AWS SSM, etc.
|
|
23
|
+
--print-key Print the private key to stdout (disabled by default for security).
|
|
24
|
+
`;
|
|
25
|
+
function args(argv) {
|
|
26
|
+
const out = {};
|
|
27
|
+
for (let i = 0; i < argv.length; i++) {
|
|
28
|
+
const a = argv[i];
|
|
29
|
+
if (!a.startsWith("--"))
|
|
30
|
+
continue;
|
|
31
|
+
const next = argv[i + 1];
|
|
32
|
+
if (next && !next.startsWith("--")) {
|
|
33
|
+
out[a.slice(2)] = next;
|
|
34
|
+
i++;
|
|
35
|
+
}
|
|
36
|
+
else
|
|
37
|
+
out[a.slice(2)] = true;
|
|
38
|
+
}
|
|
39
|
+
return out;
|
|
40
|
+
}
|
|
41
|
+
function fail(message) {
|
|
42
|
+
console.error(redactSecrets(`✖ ${message}`));
|
|
43
|
+
process.exit(1);
|
|
44
|
+
}
|
|
45
|
+
function readEnvFile(file) {
|
|
46
|
+
const map = new Map();
|
|
47
|
+
if (!fs.existsSync(file))
|
|
48
|
+
return map;
|
|
49
|
+
for (const line of fs.readFileSync(file, "utf8").split("\n")) {
|
|
50
|
+
const m = line.match(/^\s*([A-Z0-9_]+)\s*=\s*(.*)$/);
|
|
51
|
+
if (m)
|
|
52
|
+
map.set(m[1], m[2].replace(/^"(.*)"$/, "$1"));
|
|
53
|
+
}
|
|
54
|
+
return map;
|
|
55
|
+
}
|
|
56
|
+
function writeEnvFile(file, values) {
|
|
57
|
+
const lines = fs.existsSync(file) ? fs.readFileSync(file, "utf8").split("\n") : [];
|
|
58
|
+
const seen = new Set();
|
|
59
|
+
const out = [];
|
|
60
|
+
for (const line of lines) {
|
|
61
|
+
const key = line.match(/^\s*([A-Z0-9_]+)\s*=/)?.[1];
|
|
62
|
+
if (key && key in values) {
|
|
63
|
+
seen.add(key);
|
|
64
|
+
if (values[key] !== null)
|
|
65
|
+
out.push(`${key}="${values[key]}"`);
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
out.push(line);
|
|
69
|
+
}
|
|
70
|
+
while (out.length && out[out.length - 1] === "")
|
|
71
|
+
out.pop();
|
|
72
|
+
const added = Object.entries(values).filter(([k, v]) => !seen.has(k) && v !== null);
|
|
73
|
+
if (added.length) {
|
|
74
|
+
if (out.length)
|
|
75
|
+
out.push("");
|
|
76
|
+
out.push("# Daywalker Auth (managed by `daywalker init`)", ...added.map(([k, v]) => `${k}="${v}"`));
|
|
77
|
+
}
|
|
78
|
+
fs.writeFileSync(file, out.join("\n") + "\n", { mode: 0o600 });
|
|
79
|
+
}
|
|
80
|
+
function ensureGitignored(dir, entry) {
|
|
81
|
+
const file = path.join(dir, ".gitignore");
|
|
82
|
+
const content = fs.existsSync(file) ? fs.readFileSync(file, "utf8") : "";
|
|
83
|
+
const covered = content.split("\n").some((l) => [entry, ".env*", ".env.*", ".env*.local", "*.local"].includes(l.trim()));
|
|
84
|
+
if (!covered)
|
|
85
|
+
fs.appendFileSync(file, `${content.endsWith("\n") || !content ? "" : "\n"}${entry}\n`);
|
|
86
|
+
}
|
|
87
|
+
async function init(opts) {
|
|
88
|
+
const isJson = Boolean(opts.json || opts.export);
|
|
89
|
+
const dir = process.cwd();
|
|
90
|
+
const envFile = path.join(dir, ".env.local");
|
|
91
|
+
const existing = readEnvFile(envFile);
|
|
92
|
+
const slug = String(opts.service ?? existing.get("DAYWALKER_SERVICE_SLUG") ?? process.env.DAYWALKER_SERVICE_SLUG ?? "")
|
|
93
|
+
.trim()
|
|
94
|
+
.toLowerCase();
|
|
95
|
+
if (!slug || !/^[a-z0-9_-]+$/.test(slug)) {
|
|
96
|
+
fail("--service must be this service's Daywalker slug (e.g. --service sonalyze)");
|
|
97
|
+
}
|
|
98
|
+
const rawDomain = opts.domain ? String(opts.domain) : existing.get("DAYWALKER_DOMAIN") ?? process.env.DAYWALKER_DOMAIN ?? "";
|
|
99
|
+
let domain = null;
|
|
100
|
+
if (rawDomain) {
|
|
101
|
+
domain = publicHostname(rawDomain);
|
|
102
|
+
if (!domain)
|
|
103
|
+
fail(`--domain (${rawDomain}) is not a valid public hostname`);
|
|
104
|
+
}
|
|
105
|
+
const apiKey = String(opts["api-key"] ?? existing.get("DAYWALKER_API_KEY") ?? process.env.DAYWALKER_API_KEY ?? "").trim() || null;
|
|
106
|
+
const authUrl = String(opts["auth-url"] ?? existing.get("DAYWALKER_AUTH_URL") ?? process.env.DAYWALKER_AUTH_URL ?? "https://auth.daywalker.dev").replace(/\/+$/, "");
|
|
107
|
+
const keyExists = Boolean(existing.get("DAYWALKER_SIGNING_KEY")) && !opts.force;
|
|
108
|
+
const key = keyExists ? null : await generateSigningKey();
|
|
109
|
+
const activeKey = key ?? (existing.get("DAYWALKER_SIGNING_KEY") ? await loadSigningKey(existing.get("DAYWALKER_SIGNING_KEY")) : null);
|
|
110
|
+
const kid = activeKey?.publicJwk.kid ?? String(opts["key-id"] ?? "");
|
|
111
|
+
// Register public key directly with Daywalker Auth if API key is provided
|
|
112
|
+
let registered = false;
|
|
113
|
+
if (apiKey && activeKey && !opts["skip-register"]) {
|
|
114
|
+
if (!isJson)
|
|
115
|
+
console.log(`• Registering public key (${activeKey.publicJwk.kid}) with Daywalker Auth...`);
|
|
116
|
+
const regResult = await registerPublicKey({
|
|
117
|
+
authUrl,
|
|
118
|
+
apiKey,
|
|
119
|
+
serviceSlug: slug,
|
|
120
|
+
domain,
|
|
121
|
+
publicJwk: activeKey.publicJwk,
|
|
122
|
+
});
|
|
123
|
+
if (regResult.ok) {
|
|
124
|
+
registered = true;
|
|
125
|
+
if (!isJson)
|
|
126
|
+
console.log(`✔ Public key registered with Daywalker Auth (Push Architecture)`);
|
|
127
|
+
}
|
|
128
|
+
else {
|
|
129
|
+
if (!isJson) {
|
|
130
|
+
console.warn(redactSecrets(`! Key registration warning (${regResult.status}): ${regResult.error}`));
|
|
131
|
+
console.warn(` You can register the key later by re-running init with your DAYWALKER_API_KEY.`);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
else if (!apiKey && !opts["skip-register"] && !isJson) {
|
|
136
|
+
console.log(`• Note: Set DAYWALKER_API_KEY in .env.local to automatically register public keys.`);
|
|
137
|
+
}
|
|
138
|
+
const signingKeyStr = key?.privateKeyPkcs8Base64 ?? existing.get("DAYWALKER_SIGNING_KEY") ?? "";
|
|
139
|
+
const envValues = {
|
|
140
|
+
DAYWALKER_SERVICE_SLUG: slug,
|
|
141
|
+
DAYWALKER_KEY_ID: kid,
|
|
142
|
+
DAYWALKER_AUTH_URL: authUrl,
|
|
143
|
+
...(domain ? { DAYWALKER_DOMAIN: domain } : {}),
|
|
144
|
+
...(apiKey ? { DAYWALKER_API_KEY: apiKey } : {}),
|
|
145
|
+
...(signingKeyStr ? { DAYWALKER_SIGNING_KEY: signingKeyStr } : {}),
|
|
146
|
+
};
|
|
147
|
+
if (isJson) {
|
|
148
|
+
console.log(JSON.stringify(envValues, null, 2));
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
writeEnvFile(envFile, envValues);
|
|
152
|
+
ensureGitignored(dir, ".env.local");
|
|
153
|
+
console.log(`✔ Daywalker configured for service "${slug}"${domain ? ` (${domain})` : ""}`);
|
|
154
|
+
console.log(` • RFC 9421 / RFC 9530 HTTP Message Signing active`);
|
|
155
|
+
console.log(` • .env.local: DAYWALKER_SERVICE_SLUG, DAYWALKER_KEY_ID, DAYWALKER_AUTH_URL${domain ? ", DAYWALKER_DOMAIN" : ""}${apiKey ? ", DAYWALKER_API_KEY" : ""}${signingKeyStr ? ", DAYWALKER_SIGNING_KEY" : ""}`);
|
|
156
|
+
if (kid)
|
|
157
|
+
console.log(` • Key ID (RFC 7638 thumbprint): ${kid}`);
|
|
158
|
+
if (registered)
|
|
159
|
+
console.log(` • Zero-ingress push architecture: public key registered directly`);
|
|
160
|
+
if (opts["print-key"] && signingKeyStr)
|
|
161
|
+
console.log(`\nDAYWALKER_SIGNING_KEY=${signingKeyStr}`);
|
|
162
|
+
}
|
|
163
|
+
async function rotate(opts) {
|
|
164
|
+
const isJson = Boolean(opts.json || opts.export);
|
|
165
|
+
const dir = process.cwd();
|
|
166
|
+
const envFile = path.join(dir, ".env.local");
|
|
167
|
+
const existing = readEnvFile(envFile);
|
|
168
|
+
const current = existing.get("DAYWALKER_SIGNING_KEY") ?? process.env.DAYWALKER_SIGNING_KEY;
|
|
169
|
+
if (!current)
|
|
170
|
+
fail("No DAYWALKER_SIGNING_KEY found; run `daywalker init` first");
|
|
171
|
+
const slug = String(opts.service ?? existing.get("DAYWALKER_SERVICE_SLUG") ?? process.env.DAYWALKER_SERVICE_SLUG ?? "").trim().toLowerCase();
|
|
172
|
+
const apiKey = String(opts["api-key"] ?? existing.get("DAYWALKER_API_KEY") ?? process.env.DAYWALKER_API_KEY ?? "").trim() || null;
|
|
173
|
+
const authUrl = String(opts["auth-url"] ?? existing.get("DAYWALKER_AUTH_URL") ?? process.env.DAYWALKER_AUTH_URL ?? "https://auth.daywalker.dev").replace(/\/+$/, "");
|
|
174
|
+
const key = await generateSigningKey();
|
|
175
|
+
if (apiKey && slug) {
|
|
176
|
+
if (!isJson)
|
|
177
|
+
console.log(`• Registering new public key (${key.publicJwk.kid}) with Daywalker Auth...`);
|
|
178
|
+
const regResult = await registerPublicKey({
|
|
179
|
+
authUrl,
|
|
180
|
+
apiKey,
|
|
181
|
+
serviceSlug: slug,
|
|
182
|
+
publicJwk: key.publicJwk,
|
|
183
|
+
});
|
|
184
|
+
if (regResult.ok) {
|
|
185
|
+
if (!isJson)
|
|
186
|
+
console.log(`✔ New public key registered with Daywalker Auth`);
|
|
187
|
+
}
|
|
188
|
+
else {
|
|
189
|
+
if (!isJson)
|
|
190
|
+
console.warn(redactSecrets(`! Key registration warning: ${regResult.error}`));
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
const envValues = {
|
|
194
|
+
DAYWALKER_KEY_ID: key.publicJwk.kid,
|
|
195
|
+
DAYWALKER_SIGNING_KEY: key.privateKeyPkcs8Base64 ?? "",
|
|
196
|
+
DAYWALKER_SIGNING_KEY_PREVIOUS: current,
|
|
197
|
+
};
|
|
198
|
+
if (isJson) {
|
|
199
|
+
console.log(JSON.stringify(envValues, null, 2));
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
writeEnvFile(envFile, envValues);
|
|
203
|
+
console.log(`✔ New key ${key.publicJwk.kid} written; previous key preserved as DAYWALKER_SIGNING_KEY_PREVIOUS.`);
|
|
204
|
+
console.log(` After deployment rollout is complete, you may remove DAYWALKER_SIGNING_KEY_PREVIOUS.`);
|
|
205
|
+
if (opts["print-key"])
|
|
206
|
+
console.log(`\nDAYWALKER_SIGNING_KEY=${key.privateKeyPkcs8Base64}`);
|
|
207
|
+
}
|
|
208
|
+
async function check() {
|
|
209
|
+
const env = { ...process.env, ...Object.fromEntries(readEnvFile(path.join(process.cwd(), ".env.local"))) };
|
|
210
|
+
const client = createDaywalker({ env });
|
|
211
|
+
const result = await client.selfCheck();
|
|
212
|
+
if (result.ok) {
|
|
213
|
+
console.log(`✔ Daywalker configuration looks good`);
|
|
214
|
+
if (result.serviceSlug)
|
|
215
|
+
console.log(` • Service slug: ${result.serviceSlug}`);
|
|
216
|
+
if (result.domain)
|
|
217
|
+
console.log(` • Caller domain: ${result.domain}`);
|
|
218
|
+
if (result.keyId)
|
|
219
|
+
console.log(` • Key ID: ${result.keyId}`);
|
|
220
|
+
if (result.signing)
|
|
221
|
+
console.log(` • RFC 9421 signature signing: ready`);
|
|
222
|
+
if (result.offlineVerificationReady)
|
|
223
|
+
console.log(` • Zero-egress offline JWT lease verification: ready`);
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
console.error(`✖ Daywalker self-check found problems:`);
|
|
227
|
+
for (const p of result.problems)
|
|
228
|
+
console.error(redactSecrets(` - ${p}`));
|
|
229
|
+
process.exit(1);
|
|
230
|
+
}
|
|
231
|
+
async function main() {
|
|
232
|
+
const [cmd, ...rest] = process.argv.slice(2);
|
|
233
|
+
const parsedArgs = args(rest);
|
|
234
|
+
if (!cmd || cmd === "--help" || cmd === "-h" || cmd === "help") {
|
|
235
|
+
console.log(HELP);
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
if (cmd === "init") {
|
|
239
|
+
await init(parsedArgs);
|
|
240
|
+
}
|
|
241
|
+
else if (cmd === "rotate") {
|
|
242
|
+
await rotate(parsedArgs);
|
|
243
|
+
}
|
|
244
|
+
else if (cmd === "check") {
|
|
245
|
+
await check();
|
|
246
|
+
}
|
|
247
|
+
else {
|
|
248
|
+
fail(`Unknown command "${cmd}". Run \`daywalker --help\` for usage.`);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
main().catch((err) => {
|
|
252
|
+
console.error(redactSecrets(String(err?.stack || err)));
|
|
253
|
+
process.exit(1);
|
|
254
|
+
});
|
package/dist/client.d.ts
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { type ResolvedConfig } from "./config.js";
|
|
2
|
+
import { type DaywalkerConfig, type PublicJwk, type RetryOptions, type SelfCheckResult, type TokenPayload, type ValidateResult } from "./types.js";
|
|
3
|
+
export type DaywalkerFetchInit = Omit<RequestInit, "body"> & {
|
|
4
|
+
body?: BodyInit | object | null;
|
|
5
|
+
retry?: RetryOptions;
|
|
6
|
+
};
|
|
7
|
+
export declare class Daywalker {
|
|
8
|
+
private readonly _config;
|
|
9
|
+
private readonly _rawConfig;
|
|
10
|
+
private _signingKeyPromise?;
|
|
11
|
+
private readonly _cache;
|
|
12
|
+
constructor(config?: DaywalkerConfig);
|
|
13
|
+
get config(): ResolvedConfig;
|
|
14
|
+
toJSON(): Record<string, unknown>;
|
|
15
|
+
private getLoadedSigningKey;
|
|
16
|
+
/**
|
|
17
|
+
* Signs and executes requests with automatic retry and exponential backoff on 429 responses.
|
|
18
|
+
*/
|
|
19
|
+
fetch(path: string, init?: DaywalkerFetchInit): Promise<Response>;
|
|
20
|
+
/**
|
|
21
|
+
* Validates incoming bearer tokens with memory caching and discriminated union return.
|
|
22
|
+
* Supports zero-egress offline verification for JWT token leases when Daywalker's public key is configured.
|
|
23
|
+
*/
|
|
24
|
+
validateToken(tokenOrOptions?: string | {
|
|
25
|
+
token?: string;
|
|
26
|
+
serviceSlug?: string;
|
|
27
|
+
forceRefresh?: boolean;
|
|
28
|
+
offlineOnly?: boolean;
|
|
29
|
+
}, options?: {
|
|
30
|
+
serviceSlug?: string;
|
|
31
|
+
forceRefresh?: boolean;
|
|
32
|
+
offlineOnly?: boolean;
|
|
33
|
+
}): Promise<ValidateResult>;
|
|
34
|
+
/**
|
|
35
|
+
* Throws DaywalkerAuthError or DaywalkerRateLimitError if token is invalid or rate limited.
|
|
36
|
+
*/
|
|
37
|
+
requireValidToken(tokenOrOptions?: string | {
|
|
38
|
+
token?: string;
|
|
39
|
+
serviceSlug?: string;
|
|
40
|
+
offlineOnly?: boolean;
|
|
41
|
+
}, options?: {
|
|
42
|
+
serviceSlug?: string;
|
|
43
|
+
offlineOnly?: boolean;
|
|
44
|
+
}): Promise<TokenPayload>;
|
|
45
|
+
/**
|
|
46
|
+
* The public JWK(s) configured for this service.
|
|
47
|
+
*/
|
|
48
|
+
keys(): Promise<{
|
|
49
|
+
keys: PublicJwk[];
|
|
50
|
+
}>;
|
|
51
|
+
/**
|
|
52
|
+
* Check configuration and verify signature readiness.
|
|
53
|
+
*/
|
|
54
|
+
selfCheck(): Promise<SelfCheckResult>;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Factory function for creating instantiable Daywalker client instances.
|
|
58
|
+
*/
|
|
59
|
+
export declare function createDaywalker(config?: DaywalkerConfig): Daywalker;
|
|
60
|
+
export declare const daywalker: Daywalker & (() => Daywalker);
|