@spfn/auth 0.3.0-beta.1 → 0.3.0-beta.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 +74 -3
- package/dist/{authenticate-DlTGaBT8.d.ts → authenticate-55LeXHqZ.d.ts} +44 -0
- package/dist/crypto.d.ts +61 -0
- package/dist/crypto.js +108 -0
- package/dist/crypto.js.map +1 -0
- package/dist/index.d.ts +46 -2
- package/dist/index.js +4 -1
- package/dist/index.js.map +1 -1
- package/dist/server.d.ts +254 -65
- package/dist/server.js +341 -101
- package/dist/server.js.map +1 -1
- package/migrations/20260807145911_mixed_invisible_woman/migration.sql +11 -0
- package/migrations/20260807145911_mixed_invisible_woman/snapshot.json +3334 -0
- package/package.json +6 -1
package/README.md
CHANGED
|
@@ -720,9 +720,18 @@ Programmatic checks (server): `hasPermission`, `hasAnyPermission`, `hasAllPermis
|
|
|
720
720
|
|
|
721
721
|
Yes, and that is the point of the operator half of this package. The day after you deploy,
|
|
722
722
|
someone has to refund an order, look up a user, publish a change, retry a failed job. The
|
|
723
|
-
usual answer is to build screens for each of those.
|
|
724
|
-
|
|
725
|
-
|
|
723
|
+
usual answer is to build screens for each of those. SPFN's answer is to expose those
|
|
724
|
+
operations to an agent instead, and there are two transports for that:
|
|
725
|
+
|
|
726
|
+
- **CLI-first (the default)**: develop ops as routes with
|
|
727
|
+
[`createOpsRouter`](../core/README.md#how-do-i-operate-the-app-from-the-terminal),
|
|
728
|
+
authenticate them with [ops tokens](#ops-tokens-spfn-ops), and drive them with
|
|
729
|
+
`spfn ops` from the same terminal the app was built in.
|
|
730
|
+
- **MCP**: [`@spfn/mcp`](../mcp/README.md) turns operations into tools a chat client's
|
|
731
|
+
agent can run — the fit when operators work outside a terminal.
|
|
732
|
+
|
|
733
|
+
`@spfn/auth` already knows who your operators are and which of them may do what; the MCP
|
|
734
|
+
wiring below shows how those answers reach `@spfn/mcp`.
|
|
726
735
|
|
|
727
736
|
The connection is app code, deliberately. `@spfn/mcp` does not read this package's RBAC on
|
|
728
737
|
its own — it asks you for a `validateToken` and a `listTools`, and those are where auth's
|
|
@@ -832,6 +841,68 @@ For short-lived authenticated handshakes (e.g. SSE) where a `Bearer` header is a
|
|
|
832
841
|
with `authApi.issueOneTimeToken`, protect the consuming route with the `oneTimeTokenAuth`
|
|
833
842
|
middleware. Call `initOneTimeTokenManager({ ttl, store })` during setup for a custom TTL/store.
|
|
834
843
|
|
|
844
|
+
## Ops tokens (`spfn ops`)
|
|
845
|
+
|
|
846
|
+
The machine credential behind the CLI-first ops surface
|
|
847
|
+
([`@spfn/core/ops`](../core/README.md#how-do-i-operate-the-app-from-the-terminal)). An ops
|
|
848
|
+
token is not a user session: it carries a label and a scope list, only its SHA-256 hash is
|
|
849
|
+
stored, and the secret is shown exactly once at issuance.
|
|
850
|
+
|
|
851
|
+
```typescript
|
|
852
|
+
// src/server/ops.ts — the app develops its own ops as routes
|
|
853
|
+
import { createOpsRouter, opsRoute } from '@spfn/core/ops';
|
|
854
|
+
import { opsTokenAuth, requireOpsScope } from '@spfn/auth/server';
|
|
855
|
+
|
|
856
|
+
export const opsRouter = createOpsRouter({
|
|
857
|
+
listSignups: opsRoute.get('/signups')
|
|
858
|
+
.use([requireOpsScope('waitlist:read')])
|
|
859
|
+
.handler(async () => signupsRepository.list()),
|
|
860
|
+
}, { auth: opsTokenAuth });
|
|
861
|
+
```
|
|
862
|
+
|
|
863
|
+
`opsRoute` comes from `@spfn/core` **0.3.0-beta.2** onwards; before that release an ops
|
|
864
|
+
route spelled its own `/_ops/` prefix with `route`.
|
|
865
|
+
|
|
866
|
+
Issue and manage tokens against the running app, signed in as an administrator. The CLI
|
|
867
|
+
prompts for the administrator's email and password, so nothing here needs database access:
|
|
868
|
+
|
|
869
|
+
```bash
|
|
870
|
+
spfn ops token issue --name laptop --scopes 'waitlist:read' --app https://api.example.com
|
|
871
|
+
spfn ops token issue --name laptop --scopes '*' --to-keychain --app https://api.example.com
|
|
872
|
+
spfn ops token list --app https://api.example.com
|
|
873
|
+
spfn ops token revoke 3 --app https://api.example.com
|
|
874
|
+
```
|
|
875
|
+
|
|
876
|
+
Behind those commands are three admin-only routes, mounted with the rest of the auth
|
|
877
|
+
router:
|
|
878
|
+
|
|
879
|
+
| Route | What it does |
|
|
880
|
+
| --- | --- |
|
|
881
|
+
| `POST /_auth/ops-tokens` | Issue. The secret is in this answer and nowhere else. |
|
|
882
|
+
| `GET /_auth/ops-tokens` | List. Only hashes were stored, so no secret can be returned. |
|
|
883
|
+
| `DELETE /_auth/ops-tokens/:id` | Revoke. Permanent, and effective immediately. |
|
|
884
|
+
|
|
885
|
+
Each requires `authenticate` plus `requireRole('admin', 'superadmin')`. The administrator
|
|
886
|
+
seeded from `SPFN_AUTH_ADMIN_*` (see [Admin seeding](#admin-seeding))
|
|
887
|
+
signs in with a password, so this works in an app whose end users only sign in socially.
|
|
888
|
+
|
|
889
|
+
Issuance takes `expiresInDays` from 1 to 36500 (about a century), or `null` for a token that
|
|
890
|
+
never expires. There is an upper bound because a day count becomes a date by arithmetic, and
|
|
891
|
+
a big enough count produces an invalid date rather than a distant one — a refusal the route
|
|
892
|
+
should answer with a message, not with whatever the driver says about a value it cannot store.
|
|
893
|
+
|
|
894
|
+
SPFN authenticates a request with a JWT the client signs itself, so the CLI generates a key
|
|
895
|
+
pair, hands the public half over at login, signs the one call it needs, and revokes the key
|
|
896
|
+
before the command ends — on the failing path as much as the succeeding one.
|
|
897
|
+
`@spfn/auth/crypto` exports the two functions that take part (`generateKeyPair`,
|
|
898
|
+
`generateClientToken`) without pulling in the auth server; it exists from **0.3.0-beta.2**,
|
|
899
|
+
which is the floor the `spfn` CLI declares for this package.
|
|
900
|
+
|
|
901
|
+
Verification refuses uniformly: an expired, revoked, or never-issued token all answer the
|
|
902
|
+
same 401, so whether a presented secret ever existed is not inferable. A valid token
|
|
903
|
+
missing a route's scope answers 403 naming only the missing scope. `'*'` grants every
|
|
904
|
+
scope.
|
|
905
|
+
|
|
835
906
|
## Mobile clientProofV1 (`@spfn/auth/client-proof`)
|
|
836
907
|
|
|
837
908
|
Server side of the spfn-mobile native SDK auth profile (issue #46; asymmetric revision in
|
|
@@ -1252,6 +1252,50 @@ declare const mainAuthRouter: _spfn_core_route.Router<{
|
|
|
1252
1252
|
userId: number;
|
|
1253
1253
|
roleId: number;
|
|
1254
1254
|
}>;
|
|
1255
|
+
issueOpsToken: _spfn_core_route.RouteDef<{
|
|
1256
|
+
body: _sinclair_typebox.TObject<{
|
|
1257
|
+
name: _sinclair_typebox.TString;
|
|
1258
|
+
scopes: _sinclair_typebox.TArray<_sinclair_typebox.TString>;
|
|
1259
|
+
expiresInDays: _sinclair_typebox.TOptional<_sinclair_typebox.TUnion<[_sinclair_typebox.TNumber, _sinclair_typebox.TNull]>>;
|
|
1260
|
+
}>;
|
|
1261
|
+
}, {}, {
|
|
1262
|
+
token: string;
|
|
1263
|
+
opsToken: {
|
|
1264
|
+
id: number;
|
|
1265
|
+
name: string;
|
|
1266
|
+
scopes: string[];
|
|
1267
|
+
expiresAt: string | null;
|
|
1268
|
+
revokedAt: string | null;
|
|
1269
|
+
lastUsedAt: string | null;
|
|
1270
|
+
createdAt: string | null;
|
|
1271
|
+
};
|
|
1272
|
+
}>;
|
|
1273
|
+
listOpsTokens: _spfn_core_route.RouteDef<{}, {}, {
|
|
1274
|
+
opsTokens: {
|
|
1275
|
+
id: number;
|
|
1276
|
+
name: string;
|
|
1277
|
+
scopes: string[];
|
|
1278
|
+
expiresAt: string | null;
|
|
1279
|
+
revokedAt: string | null;
|
|
1280
|
+
lastUsedAt: string | null;
|
|
1281
|
+
createdAt: string | null;
|
|
1282
|
+
}[];
|
|
1283
|
+
}>;
|
|
1284
|
+
revokeOpsToken: _spfn_core_route.RouteDef<{
|
|
1285
|
+
params: _sinclair_typebox.TObject<{
|
|
1286
|
+
id: _sinclair_typebox.TNumber;
|
|
1287
|
+
}>;
|
|
1288
|
+
}, {}, {
|
|
1289
|
+
opsToken: {
|
|
1290
|
+
id: number;
|
|
1291
|
+
name: string;
|
|
1292
|
+
scopes: string[];
|
|
1293
|
+
expiresAt: string | null;
|
|
1294
|
+
revokedAt: string | null;
|
|
1295
|
+
lastUsedAt: string | null;
|
|
1296
|
+
createdAt: string | null;
|
|
1297
|
+
};
|
|
1298
|
+
}>;
|
|
1255
1299
|
}>;
|
|
1256
1300
|
|
|
1257
1301
|
/**
|
package/dist/crypto.d.ts
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { K as KeyAlgorithmType } from './types-DYyhze28.js';
|
|
2
|
+
import { Algorithm } from 'jsonwebtoken';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* @spfn/auth - Client Crypto Helpers
|
|
6
|
+
*
|
|
7
|
+
* ES256 (ECDSA P-256) key generation and JWT signing for Next.js
|
|
8
|
+
* Keys are stored in DER format (Base64 encoded) for efficiency
|
|
9
|
+
*
|
|
10
|
+
* Key Sizes:
|
|
11
|
+
* - ES256 (ECDSA P-256): ~91 bytes (Base64: ~120 chars)
|
|
12
|
+
* - RS256 (RSA 2048): ~294 bytes (Base64: ~392 chars)
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
type Unit = 'Years' | 'Year' | 'Yrs' | 'Yr' | 'Y' | 'Weeks' | 'Week' | 'W' | 'Days' | 'Day' | 'D' | 'Hours' | 'Hour' | 'Hrs' | 'Hr' | 'H' | 'Minutes' | 'Minute' | 'Mins' | 'Min' | 'M' | 'Seconds' | 'Second' | 'Secs' | 'Sec' | 's' | 'Milliseconds' | 'Millisecond' | 'Msecs' | 'Msec' | 'Ms';
|
|
16
|
+
type UnitAnyCase = Unit | Uppercase<Unit> | Lowercase<Unit>;
|
|
17
|
+
type StringValue = `${number}` | `${number}${UnitAnyCase}` | `${number} ${UnitAnyCase}`;
|
|
18
|
+
interface KeyPair {
|
|
19
|
+
privateKey: string;
|
|
20
|
+
publicKey: string;
|
|
21
|
+
keyId: string;
|
|
22
|
+
fingerprint: string;
|
|
23
|
+
algorithm: KeyAlgorithmType;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Generate ECDSA P-256 key pair (ES256)
|
|
27
|
+
* Recommended for optimal size and performance
|
|
28
|
+
*/
|
|
29
|
+
declare function generateKeyPairES256(): KeyPair;
|
|
30
|
+
/**
|
|
31
|
+
* Generate RSA 2048 key pair (RS256)
|
|
32
|
+
* Fallback option, larger size but wider compatibility
|
|
33
|
+
*/
|
|
34
|
+
declare function generateKeyPairRS256(): KeyPair;
|
|
35
|
+
/**
|
|
36
|
+
* Generate key pair (defaults to ES256)
|
|
37
|
+
*/
|
|
38
|
+
declare function generateKeyPair(algorithm?: KeyAlgorithmType): KeyPair;
|
|
39
|
+
/**
|
|
40
|
+
* Generate JWT signed with client private key (DER format)
|
|
41
|
+
*/
|
|
42
|
+
declare function generateClientToken(payload: Record<string, any>, privateKeyB64: string, algorithm: Algorithm, options?: {
|
|
43
|
+
expiresIn?: StringValue | number;
|
|
44
|
+
issuer?: string;
|
|
45
|
+
}): string;
|
|
46
|
+
/**
|
|
47
|
+
* Get key size information
|
|
48
|
+
*/
|
|
49
|
+
declare function getKeySize(publicKeyB64: string): {
|
|
50
|
+
bytes: number;
|
|
51
|
+
base64Length: number;
|
|
52
|
+
};
|
|
53
|
+
/**
|
|
54
|
+
* Check if key should be rotated based on creation date
|
|
55
|
+
*/
|
|
56
|
+
declare function shouldRotateKey(createdAt: Date, rotationDays?: number): {
|
|
57
|
+
shouldRotate: boolean;
|
|
58
|
+
daysRemaining: number;
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
export { KeyAlgorithmType, type KeyPair, generateClientToken, generateKeyPair, generateKeyPairES256, generateKeyPairRS256, getKeySize, shouldRotateKey };
|
package/dist/crypto.js
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
// src/server/lib/crypto.ts
|
|
2
|
+
import crypto from "crypto";
|
|
3
|
+
import jwt from "jsonwebtoken";
|
|
4
|
+
function generateKeyPairES256() {
|
|
5
|
+
const keyId = crypto.randomUUID();
|
|
6
|
+
const { privateKey, publicKey } = crypto.generateKeyPairSync("ec", {
|
|
7
|
+
namedCurve: "P-256",
|
|
8
|
+
// ES256
|
|
9
|
+
publicKeyEncoding: {
|
|
10
|
+
type: "spki",
|
|
11
|
+
format: "der"
|
|
12
|
+
},
|
|
13
|
+
privateKeyEncoding: {
|
|
14
|
+
type: "pkcs8",
|
|
15
|
+
format: "der"
|
|
16
|
+
}
|
|
17
|
+
});
|
|
18
|
+
const privateKeyB64 = privateKey.toString("base64");
|
|
19
|
+
const publicKeyB64 = publicKey.toString("base64");
|
|
20
|
+
const fingerprint = crypto.createHash("sha256").update(publicKey).digest("hex");
|
|
21
|
+
return {
|
|
22
|
+
privateKey: privateKeyB64,
|
|
23
|
+
publicKey: publicKeyB64,
|
|
24
|
+
keyId,
|
|
25
|
+
fingerprint,
|
|
26
|
+
algorithm: "ES256"
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
function generateKeyPairRS256() {
|
|
30
|
+
const keyId = crypto.randomUUID();
|
|
31
|
+
const { privateKey, publicKey } = crypto.generateKeyPairSync("rsa", {
|
|
32
|
+
modulusLength: 2048,
|
|
33
|
+
publicKeyEncoding: {
|
|
34
|
+
type: "spki",
|
|
35
|
+
format: "der"
|
|
36
|
+
},
|
|
37
|
+
privateKeyEncoding: {
|
|
38
|
+
type: "pkcs8",
|
|
39
|
+
format: "der"
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
const privateKeyB64 = privateKey.toString("base64");
|
|
43
|
+
const publicKeyB64 = publicKey.toString("base64");
|
|
44
|
+
const fingerprint = crypto.createHash("sha256").update(publicKey).digest("hex");
|
|
45
|
+
return {
|
|
46
|
+
privateKey: privateKeyB64,
|
|
47
|
+
publicKey: publicKeyB64,
|
|
48
|
+
keyId,
|
|
49
|
+
fingerprint,
|
|
50
|
+
algorithm: "RS256"
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
function generateKeyPair(algorithm = "ES256") {
|
|
54
|
+
return algorithm === "ES256" ? generateKeyPairES256() : generateKeyPairRS256();
|
|
55
|
+
}
|
|
56
|
+
function generateClientToken(payload, privateKeyB64, algorithm, options) {
|
|
57
|
+
try {
|
|
58
|
+
const privateKeyDER = Buffer.from(privateKeyB64, "base64");
|
|
59
|
+
const privateKeyObject = crypto.createPrivateKey({
|
|
60
|
+
key: privateKeyDER,
|
|
61
|
+
format: "der",
|
|
62
|
+
type: "pkcs8"
|
|
63
|
+
});
|
|
64
|
+
const privateKeyPEM = privateKeyObject.export({
|
|
65
|
+
type: "pkcs8",
|
|
66
|
+
format: "pem"
|
|
67
|
+
});
|
|
68
|
+
const signOptions = {
|
|
69
|
+
algorithm,
|
|
70
|
+
issuer: options?.issuer || "spfn-client",
|
|
71
|
+
expiresIn: options?.expiresIn ?? "15m"
|
|
72
|
+
// Default to 15 minutes
|
|
73
|
+
};
|
|
74
|
+
return jwt.sign(payload, privateKeyPEM, signOptions);
|
|
75
|
+
} catch (error) {
|
|
76
|
+
throw new Error(
|
|
77
|
+
`Failed to generate client token: ${error instanceof Error ? error.message : "Unknown error"}`
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
function getKeySize(publicKeyB64) {
|
|
82
|
+
const keyDER = Buffer.from(publicKeyB64, "base64");
|
|
83
|
+
return {
|
|
84
|
+
bytes: keyDER.length,
|
|
85
|
+
base64Length: publicKeyB64.length
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
function shouldRotateKey(createdAt, rotationDays = 90) {
|
|
89
|
+
const now = /* @__PURE__ */ new Date();
|
|
90
|
+
const ageInDays = Math.floor(
|
|
91
|
+
(now.getTime() - createdAt.getTime()) / (1e3 * 60 * 60 * 24)
|
|
92
|
+
);
|
|
93
|
+
const daysRemaining = Math.max(0, rotationDays - ageInDays);
|
|
94
|
+
return {
|
|
95
|
+
shouldRotate: daysRemaining <= 7,
|
|
96
|
+
// Warn 7 days before expiry
|
|
97
|
+
daysRemaining
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
export {
|
|
101
|
+
generateClientToken,
|
|
102
|
+
generateKeyPair,
|
|
103
|
+
generateKeyPairES256,
|
|
104
|
+
generateKeyPairRS256,
|
|
105
|
+
getKeySize,
|
|
106
|
+
shouldRotateKey
|
|
107
|
+
};
|
|
108
|
+
//# sourceMappingURL=crypto.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/server/lib/crypto.ts"],"sourcesContent":["/**\n * @spfn/auth - Client Crypto Helpers\n *\n * ES256 (ECDSA P-256) key generation and JWT signing for Next.js\n * Keys are stored in DER format (Base64 encoded) for efficiency\n *\n * Key Sizes:\n * - ES256 (ECDSA P-256): ~91 bytes (Base64: ~120 chars)\n * - RS256 (RSA 2048): ~294 bytes (Base64: ~392 chars)\n */\n\nimport { type KeyAlgorithmType } from '../types';\nimport crypto from 'crypto';\nimport jwt, { type Algorithm, type SignOptions } from 'jsonwebtoken';\n\ntype Unit =\n | 'Years'\n | 'Year'\n | 'Yrs'\n | 'Yr'\n | 'Y'\n | 'Weeks'\n | 'Week'\n | 'W'\n | 'Days'\n | 'Day'\n | 'D'\n | 'Hours'\n | 'Hour'\n | 'Hrs'\n | 'Hr'\n | 'H'\n | 'Minutes'\n | 'Minute'\n | 'Mins'\n | 'Min'\n | 'M'\n | 'Seconds'\n | 'Second'\n | 'Secs'\n | 'Sec'\n | 's'\n | 'Milliseconds'\n | 'Millisecond'\n | 'Msecs'\n | 'Msec'\n | 'Ms';\n\ntype UnitAnyCase = Unit | Uppercase<Unit> | Lowercase<Unit>;\n\ntype StringValue =\n | `${number}`\n | `${number}${UnitAnyCase}`\n | `${number} ${UnitAnyCase}`;\n\nexport interface KeyPair\n{\n privateKey: string; // Base64 encoded DER\n publicKey: string; // Base64 encoded DER\n keyId: string; // UUID\n fingerprint: string; // SHA-256 hash\n algorithm: KeyAlgorithmType;\n}\n\n/**\n * Generate ECDSA P-256 key pair (ES256)\n * Recommended for optimal size and performance\n */\nexport function generateKeyPairES256(): KeyPair\n{\n const keyId = crypto.randomUUID();\n\n const { privateKey, publicKey } = crypto.generateKeyPairSync('ec', {\n namedCurve: 'P-256', // ES256\n publicKeyEncoding: {\n type: 'spki',\n format: 'der',\n },\n privateKeyEncoding: {\n type: 'pkcs8',\n format: 'der',\n },\n });\n\n // Convert Buffer to Base64\n const privateKeyB64 = privateKey.toString('base64');\n const publicKeyB64 = publicKey.toString('base64');\n\n // Generate fingerprint (SHA-256 of public key)\n const fingerprint = crypto\n .createHash('sha256')\n .update(publicKey)\n .digest('hex');\n\n return {\n privateKey: privateKeyB64,\n publicKey: publicKeyB64,\n keyId,\n fingerprint,\n algorithm: 'ES256',\n };\n}\n\n/**\n * Generate RSA 2048 key pair (RS256)\n * Fallback option, larger size but wider compatibility\n */\nexport function generateKeyPairRS256(): KeyPair\n{\n const keyId = crypto.randomUUID();\n\n const { privateKey, publicKey } = crypto.generateKeyPairSync('rsa', {\n modulusLength: 2048,\n publicKeyEncoding: {\n type: 'spki',\n format: 'der',\n },\n privateKeyEncoding: {\n type: 'pkcs8',\n format: 'der',\n },\n });\n\n const privateKeyB64 = privateKey.toString('base64');\n const publicKeyB64 = publicKey.toString('base64');\n\n const fingerprint = crypto\n .createHash('sha256')\n .update(publicKey)\n .digest('hex');\n\n return {\n privateKey: privateKeyB64,\n publicKey: publicKeyB64,\n keyId,\n fingerprint,\n algorithm: 'RS256',\n };\n}\n\n/**\n * Generate key pair (defaults to ES256)\n */\nexport function generateKeyPair(\n algorithm: KeyAlgorithmType = 'ES256',\n): KeyPair\n{\n return algorithm === 'ES256'\n ? generateKeyPairES256()\n : generateKeyPairRS256();\n}\n\n/**\n * Generate JWT signed with client private key (DER format)\n */\nexport function generateClientToken(\n payload: Record<string, any>,\n privateKeyB64: string,\n algorithm: Algorithm,\n options?: {\n expiresIn?: StringValue | number;\n issuer?: string;\n },\n): string\n{\n try\n {\n // Convert Base64 back to Buffer\n const privateKeyDER = Buffer.from(privateKeyB64, 'base64');\n\n // Create key object for signing\n const privateKeyObject = crypto.createPrivateKey({\n key: privateKeyDER,\n format: 'der',\n type: 'pkcs8',\n });\n\n // Export as PEM for jwt.sign\n const privateKeyPEM = privateKeyObject.export({\n type: 'pkcs8',\n format: 'pem',\n });\n\n const signOptions: SignOptions = {\n algorithm,\n issuer: options?.issuer || 'spfn-client',\n expiresIn: options?.expiresIn ?? '15m', // Default to 15 minutes\n };\n\n return jwt.sign(payload, privateKeyPEM, signOptions);\n }\n catch (error)\n {\n throw new Error(\n `Failed to generate client token: ${error instanceof Error ? error.message : 'Unknown error'}`,\n );\n }\n}\n\n/**\n * Get key size information\n */\nexport function getKeySize(publicKeyB64: string): {\n bytes: number;\n base64Length: number;\n}\n{\n const keyDER = Buffer.from(publicKeyB64, 'base64');\n\n return {\n bytes: keyDER.length,\n base64Length: publicKeyB64.length,\n };\n}\n\n/**\n * Check if key should be rotated based on creation date\n */\nexport function shouldRotateKey(\n createdAt: Date,\n rotationDays: number = 90,\n): {\n shouldRotate: boolean;\n daysRemaining: number;\n}\n{\n const now = new Date();\n const ageInDays = Math.floor(\n (now.getTime() - createdAt.getTime()) / (1000 * 60 * 60 * 24),\n );\n const daysRemaining = Math.max(0, rotationDays - ageInDays);\n\n return {\n shouldRotate: daysRemaining <= 7, // Warn 7 days before expiry\n daysRemaining,\n };\n}\n"],"mappings":";AAYA,OAAO,YAAY;AACnB,OAAO,SAA+C;AAuD/C,SAAS,uBAChB;AACI,QAAM,QAAQ,OAAO,WAAW;AAEhC,QAAM,EAAE,YAAY,UAAU,IAAI,OAAO,oBAAoB,MAAM;AAAA,IAC/D,YAAY;AAAA;AAAA,IACZ,mBAAmB;AAAA,MACf,MAAM;AAAA,MACN,QAAQ;AAAA,IACZ;AAAA,IACA,oBAAoB;AAAA,MAChB,MAAM;AAAA,MACN,QAAQ;AAAA,IACZ;AAAA,EACJ,CAAC;AAGD,QAAM,gBAAgB,WAAW,SAAS,QAAQ;AAClD,QAAM,eAAe,UAAU,SAAS,QAAQ;AAGhD,QAAM,cAAc,OACf,WAAW,QAAQ,EACnB,OAAO,SAAS,EAChB,OAAO,KAAK;AAEjB,SAAO;AAAA,IACH,YAAY;AAAA,IACZ,WAAW;AAAA,IACX;AAAA,IACA;AAAA,IACA,WAAW;AAAA,EACf;AACJ;AAMO,SAAS,uBAChB;AACI,QAAM,QAAQ,OAAO,WAAW;AAEhC,QAAM,EAAE,YAAY,UAAU,IAAI,OAAO,oBAAoB,OAAO;AAAA,IAChE,eAAe;AAAA,IACf,mBAAmB;AAAA,MACf,MAAM;AAAA,MACN,QAAQ;AAAA,IACZ;AAAA,IACA,oBAAoB;AAAA,MAChB,MAAM;AAAA,MACN,QAAQ;AAAA,IACZ;AAAA,EACJ,CAAC;AAED,QAAM,gBAAgB,WAAW,SAAS,QAAQ;AAClD,QAAM,eAAe,UAAU,SAAS,QAAQ;AAEhD,QAAM,cAAc,OACf,WAAW,QAAQ,EACnB,OAAO,SAAS,EAChB,OAAO,KAAK;AAEjB,SAAO;AAAA,IACH,YAAY;AAAA,IACZ,WAAW;AAAA,IACX;AAAA,IACA;AAAA,IACA,WAAW;AAAA,EACf;AACJ;AAKO,SAAS,gBACZ,YAA8B,SAElC;AACI,SAAO,cAAc,UACf,qBAAqB,IACrB,qBAAqB;AAC/B;AAKO,SAAS,oBACZ,SACA,eACA,WACA,SAKJ;AACI,MACA;AAEI,UAAM,gBAAgB,OAAO,KAAK,eAAe,QAAQ;AAGzD,UAAM,mBAAmB,OAAO,iBAAiB;AAAA,MAC7C,KAAK;AAAA,MACL,QAAQ;AAAA,MACR,MAAM;AAAA,IACV,CAAC;AAGD,UAAM,gBAAgB,iBAAiB,OAAO;AAAA,MAC1C,MAAM;AAAA,MACN,QAAQ;AAAA,IACZ,CAAC;AAED,UAAM,cAA2B;AAAA,MAC7B;AAAA,MACA,QAAQ,SAAS,UAAU;AAAA,MAC3B,WAAW,SAAS,aAAa;AAAA;AAAA,IACrC;AAEA,WAAO,IAAI,KAAK,SAAS,eAAe,WAAW;AAAA,EACvD,SACO,OACP;AACI,UAAM,IAAI;AAAA,MACN,oCAAoC,iBAAiB,QAAQ,MAAM,UAAU,eAAe;AAAA,IAChG;AAAA,EACJ;AACJ;AAKO,SAAS,WAAW,cAI3B;AACI,QAAM,SAAS,OAAO,KAAK,cAAc,QAAQ;AAEjD,SAAO;AAAA,IACH,OAAO,OAAO;AAAA,IACd,cAAc,aAAa;AAAA,EAC/B;AACJ;AAKO,SAAS,gBACZ,WACA,eAAuB,IAK3B;AACI,QAAM,MAAM,oBAAI,KAAK;AACrB,QAAM,YAAY,KAAK;AAAA,KAClB,IAAI,QAAQ,IAAI,UAAU,QAAQ,MAAM,MAAO,KAAK,KAAK;AAAA,EAC9D;AACA,QAAM,gBAAgB,KAAK,IAAI,GAAG,eAAe,SAAS;AAE1D,SAAO;AAAA,IACH,cAAc,iBAAiB;AAAA;AAAA,IAC/B;AAAA,EACJ;AACJ;","names":[]}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as _spfn_core_nextjs from '@spfn/core/nextjs';
|
|
2
|
-
import { P as PermissionConfig, R as RoleConfig, m as mainAuthRouter, S as SendVerificationCodeResult, a as RegisterResult, L as LoginResult, b as RotateKeyResult, K as KeySummary, c as RevokeAllKeysResult, I as IssueOneTimeTokenResult, O as OAuthStartResult, d as OAuthNativeResult, U as UserProfile, e as ProfileInfo } from './authenticate-
|
|
3
|
-
export { A as AuthInitOptions, f as AuthSession, g as PERMISSION_CATEGORIES, h as PermissionCategory, V as VERIFICATION_PURPOSES, i as VERIFICATION_TARGET_TYPES, j as VerificationPurpose, k as VerificationTargetType } from './authenticate-
|
|
2
|
+
import { P as PermissionConfig, R as RoleConfig, m as mainAuthRouter, S as SendVerificationCodeResult, a as RegisterResult, L as LoginResult, b as RotateKeyResult, K as KeySummary, c as RevokeAllKeysResult, I as IssueOneTimeTokenResult, O as OAuthStartResult, d as OAuthNativeResult, U as UserProfile, e as ProfileInfo } from './authenticate-55LeXHqZ.js';
|
|
3
|
+
export { A as AuthInitOptions, f as AuthSession, g as PERMISSION_CATEGORIES, h as PermissionCategory, V as VERIFICATION_PURPOSES, i as VERIFICATION_TARGET_TYPES, j as VerificationPurpose, k as VerificationTargetType } from './authenticate-55LeXHqZ.js';
|
|
4
4
|
import * as _spfn_core_route from '@spfn/core/route';
|
|
5
5
|
import { HttpMethod } from '@spfn/core/route';
|
|
6
6
|
export { A as ACCOUNT_DELETION_REQUESTED_BY, a as ACCOUNT_DELETION_REQUEST_STATUSES, b as AccountDeletionRequestStatus, c as AccountDeletionRequestedBy, I as INVITATION_STATUSES, d as InvitationStatus, e as KEY_ALGORITHM, f as KEY_DEVICE_NAME_MAX_LENGTH, g as KEY_PLATFORM, K as KeyAlgorithmType, h as KeyPlatformType, P as PURGE_STRATEGIES, i as PurgeStrategy, S as SOCIAL_PROVIDERS, j as SocialProvider, U as USER_STATUSES, k as UserStatus } from './types-DYyhze28.js';
|
|
@@ -563,6 +563,50 @@ declare const authApi: _spfn_core_nextjs.Client<_spfn_core_route.Router<{
|
|
|
563
563
|
userId: number;
|
|
564
564
|
roleId: number;
|
|
565
565
|
}>;
|
|
566
|
+
issueOpsToken: _spfn_core_route.RouteDef<{
|
|
567
|
+
body: _sinclair_typebox.TObject<{
|
|
568
|
+
name: _sinclair_typebox.TString;
|
|
569
|
+
scopes: _sinclair_typebox.TArray<_sinclair_typebox.TString>;
|
|
570
|
+
expiresInDays: _sinclair_typebox.TOptional<_sinclair_typebox.TUnion<[_sinclair_typebox.TNumber, _sinclair_typebox.TNull]>>;
|
|
571
|
+
}>;
|
|
572
|
+
}, {}, {
|
|
573
|
+
token: string;
|
|
574
|
+
opsToken: {
|
|
575
|
+
id: number;
|
|
576
|
+
name: string;
|
|
577
|
+
scopes: string[];
|
|
578
|
+
expiresAt: string | null;
|
|
579
|
+
revokedAt: string | null;
|
|
580
|
+
lastUsedAt: string | null;
|
|
581
|
+
createdAt: string | null;
|
|
582
|
+
};
|
|
583
|
+
}>;
|
|
584
|
+
listOpsTokens: _spfn_core_route.RouteDef<{}, {}, {
|
|
585
|
+
opsTokens: {
|
|
586
|
+
id: number;
|
|
587
|
+
name: string;
|
|
588
|
+
scopes: string[];
|
|
589
|
+
expiresAt: string | null;
|
|
590
|
+
revokedAt: string | null;
|
|
591
|
+
lastUsedAt: string | null;
|
|
592
|
+
createdAt: string | null;
|
|
593
|
+
}[];
|
|
594
|
+
}>;
|
|
595
|
+
revokeOpsToken: _spfn_core_route.RouteDef<{
|
|
596
|
+
params: _sinclair_typebox.TObject<{
|
|
597
|
+
id: _sinclair_typebox.TNumber;
|
|
598
|
+
}>;
|
|
599
|
+
}, {}, {
|
|
600
|
+
opsToken: {
|
|
601
|
+
id: number;
|
|
602
|
+
name: string;
|
|
603
|
+
scopes: string[];
|
|
604
|
+
expiresAt: string | null;
|
|
605
|
+
revokedAt: string | null;
|
|
606
|
+
lastUsedAt: string | null;
|
|
607
|
+
createdAt: string | null;
|
|
608
|
+
};
|
|
609
|
+
}>;
|
|
566
610
|
}>>;
|
|
567
611
|
type AuthRouter = typeof mainAuthRouter;
|
|
568
612
|
|
package/dist/index.js
CHANGED
|
@@ -298,7 +298,10 @@ var routeMap = {
|
|
|
298
298
|
deleteAdminRole: { method: "DELETE", path: "/_auth/admin/roles/:id" },
|
|
299
299
|
updateUserRole: { method: "PATCH", path: "/_auth/admin/users/:userId/role" },
|
|
300
300
|
requestAccountDeletion: { method: "POST", path: "/_auth/deletion/request" },
|
|
301
|
-
cancelAccountDeletion: { method: "POST", path: "/_auth/deletion/cancel" }
|
|
301
|
+
cancelAccountDeletion: { method: "POST", path: "/_auth/deletion/cancel" },
|
|
302
|
+
issueOpsToken: { method: "POST", path: "/_auth/ops-tokens" },
|
|
303
|
+
listOpsTokens: { method: "GET", path: "/_auth/ops-tokens" },
|
|
304
|
+
revokeOpsToken: { method: "DELETE", path: "/_auth/ops-tokens/:id" }
|
|
302
305
|
};
|
|
303
306
|
|
|
304
307
|
// src/lib/types.ts
|