@me2em/react 0.1.0-alpha.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.
@@ -0,0 +1,157 @@
1
+ // packages/react/src/react/useSession.ts
2
+ import { useState, useEffect, useCallback, useRef } from 'react';
3
+ import type { Handle } from '@me2em/core';
4
+ import { Session, type SessionOptions, type SessionPayload } from '@me2em/core';
5
+ import { useMe2emContext } from './context.js';
6
+
7
+ /**
8
+ * Options for useSession, memoized via deep comparison of
9
+ * `handle` and `options` refs on each render.
10
+ *
11
+ * @category Types
12
+ */
13
+ export interface UseSessionOptions {
14
+ /** Intended audience (recipient) of this session. */
15
+ audience: string;
16
+ /** Permission scopes granted in this session. */
17
+ scopes: string[];
18
+ /** Time-to-live in seconds. */
19
+ ttl: number;
20
+ }
21
+
22
+ /**
23
+ * Result of the useSession hook.
24
+ *
25
+ * @category Types
26
+ */
27
+ export interface UseSessionResult {
28
+ /** The active session, or null if no handle or session creation failed. */
29
+ session: Session | null;
30
+ /** Whether the session (or its absence) has been determined. */
31
+ isLoaded: boolean;
32
+ /** Error string if session creation or renewal failed. */
33
+ error: string | null;
34
+ /** Whether the current session has expired. */
35
+ isExpired: boolean;
36
+ /** Manually renew the session (creates a new one with a fresh jti). */
37
+ renew: () => void;
38
+ }
39
+
40
+ /**
41
+ * Hook that creates and manages a signed session token for a Handle.
42
+ *
43
+ * When `handle` is provided, creates a Session with the given options.
44
+ * Supports auto-renewal: when `autoRenew` is true (default), a timer
45
+ * fires at half the session TTL to create a fresh session with a new
46
+ * jti (the previous token remains valid until its own expiry, but the
47
+ * lineage identity is preserved through `handle`).
48
+ *
49
+ * On unmount, all timers are cleaned up to prevent state updates after
50
+ * the component is gone.
51
+ *
52
+ * Dependencies: `[handle, renew]` — the options object is read through
53
+ * a ref, so passing a fresh object literal on every render does NOT
54
+ * re-trigger session creation. Only changes to `handle` (or to the
55
+ * renewal function identity, which follows `handle`) do.
56
+ *
57
+ * @param handle - The Handle to sign the session, or null.
58
+ * @param options - Session options (audience, scopes, ttl).
59
+ * @param autoRenew - Whether to auto-renew at half-life (default: true).
60
+ * @returns Session state with manual `renew` control.
61
+ *
62
+ * @example
63
+ * ```ts
64
+ * const handle = useHandle('my-handle');
65
+ * const { session, isExpired, renew } = useSession(handle, {
66
+ * audience: 'app.example.com',
67
+ * scopes: ['read', 'write'],
68
+ * ttl: 3600,
69
+ * });
70
+ * ```
71
+ */
72
+ export function useSession(
73
+ handle: Handle | null,
74
+ options: UseSessionOptions,
75
+ autoRenew: boolean = true
76
+ ): UseSessionResult {
77
+ const { identity } = useMe2emContext();
78
+ const [session, setSession] = useState<Session | null>(null);
79
+ const [isLoaded, setIsLoaded] = useState(false);
80
+ const [error, setError] = useState<string | null>(null);
81
+ const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
82
+ const optionsRef = useRef(options);
83
+ const mountedRef = useRef(true);
84
+
85
+ // Update options ref without triggering re-renders
86
+ optionsRef.current = options;
87
+
88
+ const clearTimer = useCallback(() => {
89
+ if (timerRef.current) {
90
+ clearTimeout(timerRef.current);
91
+ timerRef.current = null;
92
+ }
93
+ }, []);
94
+
95
+ // Cleanup on unmount
96
+ useEffect(() => {
97
+ mountedRef.current = true;
98
+ return () => {
99
+ mountedRef.current = false;
100
+ clearTimer();
101
+ };
102
+ }, [clearTimer]);
103
+
104
+ const renew = useCallback(() => {
105
+ clearTimer();
106
+ const opts = optionsRef.current;
107
+
108
+ if (!handle) {
109
+ if (!mountedRef.current) return;
110
+ setSession(null);
111
+ setIsLoaded(true);
112
+ setError(null);
113
+ return;
114
+ }
115
+
116
+ Session.create(handle, {
117
+ audience: opts.audience,
118
+ scopes: opts.scopes,
119
+ ttl: opts.ttl,
120
+ }).then((s) => {
121
+ if (!mountedRef.current) return;
122
+ setSession(s);
123
+ setIsLoaded(true);
124
+ setError(null);
125
+
126
+ // Schedule auto-renewal at half-life
127
+ if (autoRenew) {
128
+ const halfLife = Math.floor((s.expiresAt * 1000 - Date.now()) / 2);
129
+ if (halfLife > 0) {
130
+ timerRef.current = setTimeout(() => {
131
+ renew();
132
+ }, Math.max(halfLife, 1000));
133
+ }
134
+ }
135
+ }).catch((err) => {
136
+ if (!mountedRef.current) return;
137
+ setSession(null);
138
+ setIsLoaded(true);
139
+ setError(err instanceof Error ? err.message : String(err));
140
+ });
141
+ }, [handle, autoRenew, clearTimer]);
142
+
143
+ // Create session when handle or options change
144
+ useEffect(() => {
145
+ if (mountedRef.current) {
146
+ renew();
147
+ }
148
+ }, [handle, renew]);
149
+
150
+ return {
151
+ session,
152
+ isLoaded,
153
+ error,
154
+ isExpired: session ? session.isExpired() : false,
155
+ renew,
156
+ };
157
+ }
@@ -0,0 +1,30 @@
1
+ import { useState, useEffect } from 'react';
2
+ import { type SubHandle } from '@me2em/core';
3
+ import { useMe2emContext } from './context.js';
4
+
5
+ export function useSubHandle(
6
+ handleName: string,
7
+ subName: string
8
+ ): SubHandle | null {
9
+ const { identity } = useMe2emContext();
10
+ const [subHandle, setSubHandle] = useState<SubHandle | null>(null);
11
+
12
+ useEffect(() => {
13
+ let cancelled = false;
14
+ if (!identity) {
15
+ setSubHandle(null);
16
+ return;
17
+ }
18
+ identity
19
+ .deriveSubHandle(handleName, subName)
20
+ .then((h) => {
21
+ if (!cancelled) setSubHandle(h);
22
+ })
23
+ .catch((err) => {
24
+ if (!cancelled) console.error('useSubHandle derivation failed', err);
25
+ });
26
+ return () => { cancelled = true; };
27
+ }, [identity, handleName, subName]);
28
+
29
+ return subHandle;
30
+ }