@nevermined-io/ui-widgets-server 0.4.5 → 0.4.6
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 +14 -6
- package/dist/create-init-token.d.ts.map +1 -1
- package/dist/create-init-token.js +8 -5
- package/dist/normalize-email.d.ts +14 -0
- package/dist/normalize-email.d.ts.map +1 -0
- package/dist/normalize-email.js +31 -0
- package/dist/types.d.ts +19 -1
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -25,11 +25,14 @@ import { createInitToken } from '@nevermined-io/ui-widgets-server'
|
|
|
25
25
|
const app = express()
|
|
26
26
|
|
|
27
27
|
app.get('/api/widget-init-token', async (req, res) => {
|
|
28
|
-
// Authenticate the request your normal way and resolve the user.
|
|
29
|
-
|
|
28
|
+
// Authenticate the request your normal way and resolve the user's email.
|
|
29
|
+
// The email is the canonical Nevermined identity — a user that later logs
|
|
30
|
+
// into nevermined.dev directly with the same email is resolved to the
|
|
31
|
+
// same Privy user, the same wallet, and the same widget profile.
|
|
32
|
+
const email = req.user.email
|
|
30
33
|
|
|
31
34
|
const initToken = await createInitToken({
|
|
32
|
-
|
|
35
|
+
email,
|
|
33
36
|
orgId: process.env.NVM_ORG_ID!,
|
|
34
37
|
secretKey: process.env.NVM_WIDGET_KEY_SECRET!, // never expose to the browser
|
|
35
38
|
})
|
|
@@ -55,13 +58,13 @@ const nvm = await NeverminedWidgets.initialize({
|
|
|
55
58
|
|
|
56
59
|
### `createInitToken(options): Promise<string>`
|
|
57
60
|
|
|
58
|
-
Returns a signed JWT (HS256) that the browser SDK passes to `POST /api/v1/widgets/session`. The token's audience is `nvm:widgets` and it carries the user
|
|
61
|
+
Returns a signed JWT (HS256) that the browser SDK passes to `POST /api/v1/widgets/session`. The token's audience is `nvm:widgets` and it carries the user's email (`email`, the canonical Nevermined identity) and organization id (`orgId`) as claims.
|
|
59
62
|
|
|
60
63
|
#### Options
|
|
61
64
|
|
|
62
65
|
| Field | Type | Required | Default | Description |
|
|
63
66
|
| ------------ | -------- | -------- | ------- | ----------- |
|
|
64
|
-
| `
|
|
67
|
+
| `email` | `string` | yes | — | The end user's email. Becomes the JWT `email` claim and the canonical Nevermined identity — a user that logs into nevermined.dev directly with the same email is resolved to the same Privy user, wallet, and profile. |
|
|
65
68
|
| `orgId` | `string` | yes | — | Your Nevermined organization id. |
|
|
66
69
|
| `secretKey` | `string` | yes | — | The widget key secret (`wk_...`) issued in the Nevermined dashboard. **Server-side only.** |
|
|
67
70
|
| `expiresIn` | `string` | no | `'5m'` | JWT lifetime — any [`jose`-compatible](https://github.com/panva/jose) duration string. The default is intentionally short: the browser exchanges the token immediately for a long-lived session. |
|
|
@@ -70,7 +73,8 @@ Returns a signed JWT (HS256) that the browser SDK passes to `POST /api/v1/widget
|
|
|
70
73
|
|
|
71
74
|
```ts
|
|
72
75
|
interface InitTokenPayload {
|
|
73
|
-
sub: string //
|
|
76
|
+
sub: string // mirrors `email` (legacy claim, kept for compatibility)
|
|
77
|
+
email: string // canonical Nevermined identity
|
|
74
78
|
orgId: string
|
|
75
79
|
aud: 'nvm:widgets'
|
|
76
80
|
iat: number
|
|
@@ -78,6 +82,10 @@ interface InitTokenPayload {
|
|
|
78
82
|
}
|
|
79
83
|
```
|
|
80
84
|
|
|
85
|
+
#### Breaking change vs `< 0.5.0`
|
|
86
|
+
|
|
87
|
+
The `userId` parameter was renamed to `email` and is now required to be a real email address. The widget session API resolves the Privy user via `getByEmailAddress` and only falls back to creating a new user if no match exists; an arbitrary `userId` no longer maps to a deterministic Privy user. Update your backend integration to forward the user's email to `createInitToken` instead of an internal id.
|
|
88
|
+
|
|
81
89
|
## Security notes
|
|
82
90
|
|
|
83
91
|
- Treat `secretKey` like a database password: never inline it in client code, commit it, or send it through an unencrypted channel. If it leaks, rotate the widget key in the dashboard immediately.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"create-init-token.d.ts","sourceRoot":"","sources":["../src/create-init-token.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"create-init-token.d.ts","sourceRoot":"","sources":["../src/create-init-token.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,YAAY,CAAA;AAKxD,wBAAsB,eAAe,CAAC,OAAO,EAAE,sBAAsB,GAAG,OAAO,CAAC,MAAM,CAAC,CAoBtF"}
|
|
@@ -1,18 +1,21 @@
|
|
|
1
1
|
import { SignJWT } from 'jose';
|
|
2
|
+
import { normalizeEmail } from './normalize-email.js';
|
|
2
3
|
const WIDGET_AUDIENCE = 'nvm:widgets';
|
|
3
4
|
const DEFAULT_EXPIRES_IN = '5m';
|
|
4
5
|
export async function createInitToken(options) {
|
|
5
|
-
const {
|
|
6
|
+
const { email, orgId, secretKey, expiresIn = DEFAULT_EXPIRES_IN } = options;
|
|
6
7
|
if (!secretKey?.trim())
|
|
7
8
|
throw new Error('secretKey is required');
|
|
8
|
-
if (!userId?.trim())
|
|
9
|
-
throw new Error('userId is required');
|
|
10
9
|
if (!orgId?.trim())
|
|
11
10
|
throw new Error('orgId is required');
|
|
11
|
+
const normalizedEmail = normalizeEmail(email);
|
|
12
12
|
const secret = new TextEncoder().encode(secretKey);
|
|
13
|
-
|
|
13
|
+
// `sub` is set to the email so the API's existence check on the claim
|
|
14
|
+
// continues to pass without a parallel API change. The downstream
|
|
15
|
+
// identity resolution uses the explicit `email` claim, not `sub`.
|
|
16
|
+
return new SignJWT({ orgId, email: normalizedEmail })
|
|
14
17
|
.setProtectedHeader({ alg: 'HS256' })
|
|
15
|
-
.setSubject(
|
|
18
|
+
.setSubject(normalizedEmail)
|
|
16
19
|
.setAudience(WIDGET_AUDIENCE)
|
|
17
20
|
.setIssuedAt()
|
|
18
21
|
.setExpirationTime(expiresIn)
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Validates and normalizes an email to a canonical form: trimmed and lowercased,
|
|
3
|
+
* capped at 254 chars (RFC 5321), and matched against a permissive format regex.
|
|
4
|
+
*
|
|
5
|
+
* Normalization is implicit so `Alice@Example.COM` and `alice@example.com` map
|
|
6
|
+
* to the same Nevermined identity downstream (#1520 — "one email = one user,
|
|
7
|
+
* one wallet, one widget profile"). Callers should not rely on the original
|
|
8
|
+
* case being preserved.
|
|
9
|
+
*
|
|
10
|
+
* Throws with caller-friendly messages — the SDK is the first boundary that
|
|
11
|
+
* knows the value is supposed to be an email.
|
|
12
|
+
*/
|
|
13
|
+
export declare function normalizeEmail(email: string | undefined): string;
|
|
14
|
+
//# sourceMappingURL=normalize-email.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"normalize-email.d.ts","sourceRoot":"","sources":["../src/normalize-email.ts"],"names":[],"mappings":"AAQA;;;;;;;;;;;GAWG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,CAahE"}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
// Matches the regex behind <input type="email"> and what RFC 5322 implementations
|
|
2
|
+
// accept in practice. Intentionally permissive — strict RFC validation belongs in
|
|
3
|
+
// the integrator's auth layer, not here.
|
|
4
|
+
const EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
5
|
+
// RFC 5321 caps email addresses at 254 chars. Enforcing here prevents a broken
|
|
6
|
+
// upstream integrator from ballooning the JWT and the server-side decode path.
|
|
7
|
+
const EMAIL_MAX_LENGTH = 254;
|
|
8
|
+
/**
|
|
9
|
+
* Validates and normalizes an email to a canonical form: trimmed and lowercased,
|
|
10
|
+
* capped at 254 chars (RFC 5321), and matched against a permissive format regex.
|
|
11
|
+
*
|
|
12
|
+
* Normalization is implicit so `Alice@Example.COM` and `alice@example.com` map
|
|
13
|
+
* to the same Nevermined identity downstream (#1520 — "one email = one user,
|
|
14
|
+
* one wallet, one widget profile"). Callers should not rely on the original
|
|
15
|
+
* case being preserved.
|
|
16
|
+
*
|
|
17
|
+
* Throws with caller-friendly messages — the SDK is the first boundary that
|
|
18
|
+
* knows the value is supposed to be an email.
|
|
19
|
+
*/
|
|
20
|
+
export function normalizeEmail(email) {
|
|
21
|
+
if (!email?.trim())
|
|
22
|
+
throw new Error('email is required');
|
|
23
|
+
const normalized = email.trim().toLowerCase();
|
|
24
|
+
if (normalized.length > EMAIL_MAX_LENGTH) {
|
|
25
|
+
throw new Error(`email is too long (max ${EMAIL_MAX_LENGTH} chars per RFC 5321)`);
|
|
26
|
+
}
|
|
27
|
+
if (!EMAIL_RE.test(normalized)) {
|
|
28
|
+
throw new Error('email must be a valid email address');
|
|
29
|
+
}
|
|
30
|
+
return normalized;
|
|
31
|
+
}
|
package/dist/types.d.ts
CHANGED
|
@@ -1,11 +1,29 @@
|
|
|
1
1
|
export interface CreateInitTokenOptions {
|
|
2
|
-
|
|
2
|
+
/**
|
|
3
|
+
* The end user's email — the canonical Nevermined identity (#1520). The
|
|
4
|
+
* widget session API uses this to resolve or provision the Privy user.
|
|
5
|
+
*
|
|
6
|
+
* Validated and normalized at the SDK boundary: trimmed, lowercased, capped
|
|
7
|
+
* at 254 chars (RFC 5321), and matched against a basic format regex. The
|
|
8
|
+
* normalized value is what ends up in the JWT — callers should not rely on
|
|
9
|
+
* case being preserved.
|
|
10
|
+
*/
|
|
11
|
+
email: string;
|
|
3
12
|
orgId: string;
|
|
4
13
|
secretKey: string;
|
|
5
14
|
expiresIn?: string;
|
|
6
15
|
}
|
|
7
16
|
export interface InitTokenPayload {
|
|
17
|
+
/**
|
|
18
|
+
* Mirrors `email` so the widget session API's non-empty existence check on
|
|
19
|
+
* `sub` passes without a parallel API change. Do NOT read `sub` for identity
|
|
20
|
+
* downstream — read `email` instead. This claim will be dropped once the
|
|
21
|
+
* API stops reading `sub` (follow-up to #1520).
|
|
22
|
+
*
|
|
23
|
+
* @deprecated Read `email` instead. Kept temporarily for API compatibility.
|
|
24
|
+
*/
|
|
8
25
|
sub: string;
|
|
26
|
+
email: string;
|
|
9
27
|
orgId: string;
|
|
10
28
|
aud: string;
|
|
11
29
|
iat: number;
|
package/dist/types.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,sBAAsB;IACrC,
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,sBAAsB;IACrC;;;;;;;;OAQG;IACH,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,EAAE,MAAM,CAAA;IACb,SAAS,EAAE,MAAM,CAAA;IACjB,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB;AAED,MAAM,WAAW,gBAAgB;IAC/B;;;;;;;OAOG;IACH,GAAG,EAAE,MAAM,CAAA;IACX,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,EAAE,MAAM,CAAA;IACb,GAAG,EAAE,MAAM,CAAA;IACX,GAAG,EAAE,MAAM,CAAA;IACX,GAAG,EAAE,MAAM,CAAA;CACZ"}
|