@archlast/shared 0.1.0 → 0.1.2
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 +40 -233
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,270 +1,77 @@
|
|
|
1
1
|
# @archlast/shared
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
> ⚠️ **Internal Package**: Most applications should use `@archlast/client`, `@archlast/server`, or `@archlast/cli` instead. This package is primarily for internal use by other Archlast packages.
|
|
6
|
-
|
|
7
|
-
## Table of Contents
|
|
8
|
-
|
|
9
|
-
- [Installation](#installation)
|
|
10
|
-
- [Features](#features)
|
|
11
|
-
- [Token Obfuscation](#token-obfuscation)
|
|
12
|
-
- [Token Format](#token-format)
|
|
13
|
-
- [API Reference](#api-reference)
|
|
14
|
-
- [Configuration](#configuration)
|
|
15
|
-
- [Security Considerations](#security-considerations)
|
|
3
|
+
Shared utilities for Archlast packages. This package is mainly internal; most
|
|
4
|
+
applications should use `@archlast/client`, `@archlast/server`, or `@archlast/cli`.
|
|
16
5
|
|
|
17
6
|
## Installation
|
|
18
7
|
|
|
19
8
|
```bash
|
|
20
9
|
npm install @archlast/shared
|
|
21
|
-
|
|
22
|
-
# Or with other package managers
|
|
23
|
-
pnpm add @archlast/shared
|
|
24
|
-
bun add @archlast/shared
|
|
25
10
|
```
|
|
26
11
|
|
|
27
|
-
##
|
|
28
|
-
|
|
29
|
-
- **Token Obfuscation**: Secure token wrapping with HMAC signatures
|
|
30
|
-
- **Token Validation**: Verify and extract obfuscated tokens
|
|
31
|
-
- **Path-Bound Tokens**: Tokens are bound to specific API paths
|
|
32
|
-
- **Timestamp Validation**: Short validity window prevents replay attacks
|
|
33
|
-
- **Shared Constants**: Common constants used across packages
|
|
34
|
-
|
|
35
|
-
## Token Obfuscation
|
|
36
|
-
|
|
37
|
-
Token obfuscation protects sensitive tokens (API keys, session tokens) in transit by:
|
|
38
|
-
|
|
39
|
-
1. Adding a timestamp for time-limited validity
|
|
40
|
-
2. Including a random nonce for uniqueness
|
|
41
|
-
3. Computing an HMAC signature bound to the request path
|
|
42
|
-
4. Encoding the result in a structured format
|
|
43
|
-
|
|
44
|
-
### Why Obfuscate?
|
|
45
|
-
|
|
46
|
-
- **Path Binding**: A token obfuscated for `/api/users` cannot be reused for `/api/admin`
|
|
47
|
-
- **Time Limiting**: Tokens expire after a short window (default: 5 minutes)
|
|
48
|
-
- **Replay Prevention**: Random nonce ensures each request is unique
|
|
49
|
-
- **Signature Verification**: HMAC ensures token integrity
|
|
12
|
+
## Token obfuscation
|
|
50
13
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
Obfuscated tokens follow this structure:
|
|
14
|
+
Token obfuscation protects sensitive tokens by binding them to a request path
|
|
15
|
+
and a short validity window. The format is:
|
|
54
16
|
|
|
55
17
|
```
|
|
56
18
|
v1:<timestamp>:<nonce>:<signature>:<token>
|
|
57
19
|
```
|
|
58
20
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
| `timestamp` | Unix milliseconds when token was obfuscated |
|
|
63
|
-
| `nonce` | Random 16-character string |
|
|
64
|
-
| `signature` | HMAC-SHA256 of `token:timestamp:nonce:path` |
|
|
65
|
-
| `token` | The original raw token |
|
|
66
|
-
|
|
67
|
-
### Example
|
|
68
|
-
|
|
69
|
-
```
|
|
70
|
-
v1:1704067200000:a1b2c3d4e5f6g7h8:8f4e2c1a9b7d5e3f1a2b4c6d8e0f2a4b6:arch_sk_live_xxx
|
|
71
|
-
```
|
|
72
|
-
|
|
73
|
-
## API Reference
|
|
74
|
-
|
|
75
|
-
### `obfuscateToken(token, path, options?)`
|
|
76
|
-
|
|
77
|
-
Obfuscate a raw token for a specific API path.
|
|
78
|
-
|
|
79
|
-
```ts
|
|
80
|
-
import { obfuscateToken } from "@archlast/shared";
|
|
81
|
-
|
|
82
|
-
const obfuscated = obfuscateToken(
|
|
83
|
-
"arch_sk_live_abc123",
|
|
84
|
-
"/_archlast/admin/auth/me"
|
|
85
|
-
);
|
|
86
|
-
|
|
87
|
-
// Result: "v1:1704067200000:a1b2c3d4e5f6g7h8:..."
|
|
88
|
-
```
|
|
89
|
-
|
|
90
|
-
**Parameters:**
|
|
21
|
+
- `timestamp` is unix milliseconds
|
|
22
|
+
- `nonce` is a random 16-character string
|
|
23
|
+
- `signature` is an HMAC of `token:timestamp:nonce:path`
|
|
91
24
|
|
|
92
|
-
|
|
93
|
-
|-----------|------|-------------|
|
|
94
|
-
| `token` | `string` | Raw token to obfuscate |
|
|
95
|
-
| `path` | `string` | API path the token is bound to |
|
|
96
|
-
| `options.secret` | `string?` | Custom HMAC secret (default: env var) |
|
|
97
|
-
| `options.timestamp` | `number?` | Custom timestamp (default: now) |
|
|
25
|
+
The current implementation uses a five minute window.
|
|
98
26
|
|
|
99
|
-
|
|
27
|
+
## API
|
|
100
28
|
|
|
101
|
-
|
|
29
|
+
### `obfuscateToken(token, path)`
|
|
102
30
|
|
|
103
|
-
|
|
104
|
-
import { deobfuscateToken } from "@archlast/shared";
|
|
31
|
+
Returns an obfuscated token string.
|
|
105
32
|
|
|
106
|
-
|
|
107
|
-
"v1:1704067200000:...",
|
|
108
|
-
"/_archlast/admin/auth/me"
|
|
109
|
-
);
|
|
33
|
+
### `deobfuscateToken(obfuscated, path)`
|
|
110
34
|
|
|
111
|
-
|
|
112
|
-
console.log("Token:", result.token);
|
|
113
|
-
} else {
|
|
114
|
-
console.error("Invalid:", result.error);
|
|
115
|
-
}
|
|
116
|
-
```
|
|
35
|
+
Returns the raw token when valid, otherwise `null`.
|
|
117
36
|
|
|
118
|
-
|
|
37
|
+
### `isObfuscatedToken(token)`
|
|
119
38
|
|
|
120
|
-
|
|
121
|
-
// Success
|
|
122
|
-
{ valid: true, token: string }
|
|
39
|
+
Returns `true` when the token uses the `v1:` prefix.
|
|
123
40
|
|
|
124
|
-
|
|
125
|
-
{ valid: false, error: string }
|
|
126
|
-
```
|
|
41
|
+
### `extractRawToken(token, path)`
|
|
127
42
|
|
|
128
|
-
|
|
43
|
+
Accepts either a raw or obfuscated token and returns the raw token or `null`.
|
|
129
44
|
|
|
130
|
-
|
|
131
|
-
|-------|-------------|
|
|
132
|
-
| `"Invalid format"` | Token doesn't match expected structure |
|
|
133
|
-
| `"Unsupported version"` | Version prefix is not recognized |
|
|
134
|
-
| `"Token expired"` | Timestamp is outside validity window |
|
|
135
|
-
| `"Invalid signature"` | HMAC verification failed |
|
|
136
|
-
| `"Path mismatch"` | Token was created for a different path |
|
|
137
|
-
|
|
138
|
-
### `extractRawToken(obfuscated, path, options?)`
|
|
139
|
-
|
|
140
|
-
Convenience wrapper that returns the raw token or throws.
|
|
45
|
+
## Usage
|
|
141
46
|
|
|
142
47
|
```ts
|
|
143
|
-
import {
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
}
|
|
149
|
-
|
|
48
|
+
import {
|
|
49
|
+
obfuscateToken,
|
|
50
|
+
deobfuscateToken,
|
|
51
|
+
extractRawToken,
|
|
52
|
+
isObfuscatedToken
|
|
53
|
+
} from "@archlast/shared";
|
|
54
|
+
|
|
55
|
+
const obfuscated = obfuscateToken(rawToken, "/_archlast/admin/auth/me");
|
|
56
|
+
const raw = deobfuscateToken(obfuscated, "/_archlast/admin/auth/me");
|
|
57
|
+
const rawEither = extractRawToken(obfuscated, "/_archlast/admin/auth/me");
|
|
58
|
+
|
|
59
|
+
if (isObfuscatedToken(obfuscated)) {
|
|
60
|
+
// token was wrapped
|
|
150
61
|
}
|
|
151
62
|
```
|
|
152
63
|
|
|
153
|
-
### `isObfuscatedToken(value)`
|
|
154
|
-
|
|
155
|
-
Check if a string looks like an obfuscated token.
|
|
156
|
-
|
|
157
|
-
```ts
|
|
158
|
-
import { isObfuscatedToken } from "@archlast/shared";
|
|
159
|
-
|
|
160
|
-
isObfuscatedToken("v1:123:abc:def:token"); // true
|
|
161
|
-
isObfuscatedToken("arch_sk_live_xxx"); // false
|
|
162
|
-
```
|
|
163
|
-
|
|
164
64
|
## Configuration
|
|
165
65
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
| Variable | Default | Description |
|
|
169
|
-
|----------|---------|-------------|
|
|
170
|
-
| `ARCHLAST_TOKEN_OBFUSCATION_SECRET` | - | **Required in production.** HMAC secret for signatures. |
|
|
171
|
-
| `ARCHLAST_TOKEN_VALIDITY_MS` | `300000` | Token validity window (5 minutes) |
|
|
172
|
-
|
|
173
|
-
### Setting the Secret
|
|
174
|
-
|
|
175
|
-
```bash
|
|
176
|
-
# Generate a strong secret
|
|
177
|
-
openssl rand -base64 32
|
|
178
|
-
|
|
179
|
-
# Set in environment
|
|
180
|
-
export ARCHLAST_TOKEN_OBFUSCATION_SECRET="your-32-char-random-secret-here"
|
|
181
|
-
```
|
|
182
|
-
|
|
183
|
-
### Custom Options
|
|
184
|
-
|
|
185
|
-
```ts
|
|
186
|
-
const obfuscated = obfuscateToken(token, path, {
|
|
187
|
-
secret: "custom-secret",
|
|
188
|
-
timestamp: Date.now(),
|
|
189
|
-
});
|
|
190
|
-
|
|
191
|
-
const result = deobfuscateToken(obfuscated, path, {
|
|
192
|
-
secret: "custom-secret",
|
|
193
|
-
validityMs: 60000, // 1 minute
|
|
194
|
-
});
|
|
195
|
-
```
|
|
196
|
-
|
|
197
|
-
## Security Considerations
|
|
198
|
-
|
|
199
|
-
### Production Requirements
|
|
200
|
-
|
|
201
|
-
1. **Set a Strong Secret**: Always set `ARCHLAST_TOKEN_OBFUSCATION_SECRET` in production
|
|
202
|
-
2. **Keep Clocks Synced**: Use NTP to keep server clocks synchronized
|
|
203
|
-
3. **Use HTTPS**: Always transmit tokens over HTTPS
|
|
204
|
-
4. **Short Validity**: Keep the validity window short (default: 5 minutes)
|
|
205
|
-
|
|
206
|
-
### Best Practices
|
|
207
|
-
|
|
208
|
-
```ts
|
|
209
|
-
// ✅ Good: Obfuscate before sending
|
|
210
|
-
const headers = {
|
|
211
|
-
Authorization: `Bearer ${obfuscateToken(apiKey, requestPath)}`
|
|
212
|
-
};
|
|
213
|
-
|
|
214
|
-
// ❌ Bad: Sending raw tokens in headers
|
|
215
|
-
const headers = {
|
|
216
|
-
Authorization: `Bearer ${apiKey}`
|
|
217
|
-
};
|
|
218
|
-
```
|
|
219
|
-
|
|
220
|
-
### Clock Skew
|
|
221
|
-
|
|
222
|
-
The validation allows for some clock skew between client and server:
|
|
223
|
-
|
|
224
|
-
```ts
|
|
225
|
-
// Token is valid if:
|
|
226
|
-
// (serverTime - validityMs) < tokenTimestamp < (serverTime + clockSkew)
|
|
227
|
-
```
|
|
228
|
-
|
|
229
|
-
Default clock skew allowance: 30 seconds.
|
|
230
|
-
|
|
231
|
-
## Internal Usage
|
|
232
|
-
|
|
233
|
-
This package is used internally by:
|
|
234
|
-
|
|
235
|
-
- **@archlast/server**: Validating incoming requests
|
|
236
|
-
- **@archlast/client**: Preparing authenticated requests
|
|
237
|
-
- **@archlast/cli**: API key handling for deployments
|
|
238
|
-
|
|
239
|
-
### Example: Server Middleware
|
|
240
|
-
|
|
241
|
-
```ts
|
|
242
|
-
// Internal usage in @archlast/server
|
|
243
|
-
import { deobfuscateToken } from "@archlast/shared";
|
|
244
|
-
|
|
245
|
-
function authMiddleware(req, res, next) {
|
|
246
|
-
const authHeader = req.headers.authorization;
|
|
247
|
-
const token = authHeader?.replace("Bearer ", "");
|
|
248
|
-
|
|
249
|
-
if (!token) {
|
|
250
|
-
return res.status(401).json({ error: "No token" });
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
const result = deobfuscateToken(token, req.path);
|
|
254
|
-
|
|
255
|
-
if (!result.valid) {
|
|
256
|
-
return res.status(401).json({ error: result.error });
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
req.apiKey = result.token;
|
|
260
|
-
next();
|
|
261
|
-
}
|
|
262
|
-
```
|
|
66
|
+
Set `ARCHLAST_TOKEN_OBFUSCATION_SECRET` in production. If it is not set, a
|
|
67
|
+
development default is used.
|
|
263
68
|
|
|
264
|
-
##
|
|
69
|
+
## Security notes
|
|
265
70
|
|
|
266
|
-
|
|
71
|
+
- Keep clocks in sync between clients and servers.
|
|
72
|
+
- Rotate the secret if you suspect exposure.
|
|
73
|
+
- Bind tokens to the exact path you expect to validate.
|
|
267
74
|
|
|
268
|
-
##
|
|
75
|
+
## Publishing (maintainers)
|
|
269
76
|
|
|
270
|
-
|
|
77
|
+
See `docs/npm-publishing.md` for release and publish steps.
|