@workos-inc/authkit-nextjs 4.0.1 → 4.1.0
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 -2
- package/dist/esm/feature-flags.js +11 -0
- package/dist/esm/feature-flags.js.map +1 -0
- package/dist/esm/index.js +2 -1
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/types/feature-flags.d.ts +9 -0
- package/dist/esm/types/index.d.ts +2 -1
- package/dist/esm/types/utils.d.ts +1 -1
- package/dist/esm/utils.js +2 -2
- package/dist/esm/utils.js.map +1 -1
- package/package.json +1 -1
- package/src/feature-flags.spec.ts +28 -0
- package/src/feature-flags.ts +14 -0
- package/src/index.ts +2 -0
- package/src/utils.ts +4 -4
package/README.md
CHANGED
|
@@ -452,14 +452,52 @@ export default function MyComponent() {
|
|
|
452
452
|
}
|
|
453
453
|
```
|
|
454
454
|
|
|
455
|
-
###
|
|
455
|
+
### Evaluate feature flags
|
|
456
456
|
|
|
457
|
-
|
|
457
|
+
There are two ways to evaluate feature flags with AuthKit Next.js:
|
|
458
|
+
|
|
459
|
+
- Use the `feature_flags` access token claim when you want a simple session-scoped list of enabled flags for the signed-in user.
|
|
460
|
+
- Use the Feature Flags runtime client when you want server-side evaluation without storing every active flag in the user's session cookie.
|
|
461
|
+
|
|
462
|
+
#### Option 1: Use the `feature_flags` claim
|
|
463
|
+
|
|
464
|
+
For situations where you need access to the authenticated user's currently active feature flags and your environment includes the `feature_flags` access token claim, use `withAuth` to retrieve the flags from the WorkOS session.
|
|
458
465
|
|
|
459
466
|
```jsx
|
|
460
467
|
const { featureFlags } = await withAuth();
|
|
461
468
|
```
|
|
462
469
|
|
|
470
|
+
This is convenient for small flag sets because the flags are available with the user's session. Flag changes appear the next time the user logs in or the session is refreshed.
|
|
471
|
+
|
|
472
|
+
#### Option 2: Use the runtime client
|
|
473
|
+
|
|
474
|
+
Use the runtime client when your application has many feature flags, when the `feature_flags` claim makes the access token too large, or when you need server-side flag evaluation that stays in sync independently of the user's session. The runtime client keeps flag configuration in memory and syncs changes in the background, so create one shared instance per server process rather than one client per request.
|
|
475
|
+
|
|
476
|
+
```tsx
|
|
477
|
+
import { getFeatureFlagsRuntimeClient, withAuth } from '@workos-inc/authkit-nextjs';
|
|
478
|
+
|
|
479
|
+
const featureFlags = getFeatureFlagsRuntimeClient();
|
|
480
|
+
|
|
481
|
+
export default async function DashboardPage() {
|
|
482
|
+
const { user, organizationId } = await withAuth({ ensureSignedIn: true });
|
|
483
|
+
|
|
484
|
+
try {
|
|
485
|
+
await featureFlags.waitUntilReady({ timeoutMs: 5000 });
|
|
486
|
+
} catch (error) {
|
|
487
|
+
console.error('Feature flags client failed to initialize:', error);
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
const enabled = featureFlags.isEnabled('advanced-analytics', {
|
|
491
|
+
userId: user.id,
|
|
492
|
+
organizationId,
|
|
493
|
+
});
|
|
494
|
+
|
|
495
|
+
return enabled ? <AdvancedAnalytics /> : <BasicAnalytics />;
|
|
496
|
+
}
|
|
497
|
+
```
|
|
498
|
+
|
|
499
|
+
The `getFeatureFlagsRuntimeClient` helper returns the same runtime client for every call in the current server process. Options passed to `getFeatureFlagsRuntimeClient(options)` are only used when the client is created for the first time.
|
|
500
|
+
|
|
463
501
|
### Requiring auth
|
|
464
502
|
|
|
465
503
|
For pages where a signed-in user is mandatory, you can use the `ensureSignedIn` option:
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { lazy } from './utils.js';
|
|
2
|
+
import { getWorkOS } from './workos.js';
|
|
3
|
+
/**
|
|
4
|
+
* Returns a shared WorkOS Feature Flags runtime client.
|
|
5
|
+
*
|
|
6
|
+
* The runtime client keeps feature flag state in sync in the background, so it
|
|
7
|
+
* should be created once per server process instead of once per request.
|
|
8
|
+
* Options are only used when the client is created for the first time.
|
|
9
|
+
*/
|
|
10
|
+
export const getFeatureFlagsRuntimeClient = lazy((options) => getWorkOS().featureFlags.createRuntimeClient(options));
|
|
11
|
+
//# sourceMappingURL=feature-flags.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"feature-flags.js","sourceRoot":"","sources":["../../src/feature-flags.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,IAAI,EAAE,MAAM,YAAY,CAAC;AAClC,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAExC;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,4BAA4B,GAAG,IAAI,CAC9C,CAAC,OAA8B,EAA6B,EAAE,CAAC,SAAS,EAAE,CAAC,YAAY,CAAC,mBAAmB,CAAC,OAAO,CAAC,CACrH,CAAC"}
|
package/dist/esm/index.js
CHANGED
|
@@ -5,7 +5,8 @@ import { authkit, authkitMiddleware, authkitProxy } from './middleware.js';
|
|
|
5
5
|
export { applyResponseHeaders, handleAuthkitHeaders, handleAuthkitProxy, partitionAuthkitHeaders, isAuthkitRequestHeader, AUTHKIT_REQUEST_HEADERS, } from './middleware-helpers.js';
|
|
6
6
|
import { getTokenClaims, refreshSession, saveSession, withAuth } from './session.js';
|
|
7
7
|
import { validateApiKey } from './validate-api-key.js';
|
|
8
|
+
import { getFeatureFlagsRuntimeClient } from './feature-flags.js';
|
|
8
9
|
import { getWorkOS } from './workos.js';
|
|
9
10
|
export * from './interfaces.js';
|
|
10
|
-
export { AuthKitError, TokenRefreshError, authkit, authkitMiddleware, authkitProxy, getSignInUrl, getSignUpUrl, getTokenClaims, getWorkOS, handleAuth, refreshSession, saveSession, signOut, switchToOrganization, validateApiKey, withAuth, };
|
|
11
|
+
export { AuthKitError, TokenRefreshError, authkit, authkitMiddleware, authkitProxy, getSignInUrl, getSignUpUrl, getFeatureFlagsRuntimeClient, getTokenClaims, getWorkOS, handleAuth, refreshSession, saveSession, signOut, switchToOrganization, validateApiKey, withAuth, };
|
|
11
12
|
//# sourceMappingURL=index.js.map
|
package/dist/esm/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,YAAY,EAAE,OAAO,EAAE,oBAAoB,EAAE,MAAM,WAAW,CAAC;AACtF,OAAO,EAAE,UAAU,EAAE,MAAM,6BAA6B,CAAC;AACzD,OAAO,EAAE,YAAY,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAC9D,OAAO,EAAE,OAAO,EAAE,iBAAiB,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAC3E,OAAO,EACL,oBAAoB,EACpB,oBAAoB,EACpB,kBAAkB,EAClB,uBAAuB,EACvB,sBAAsB,EACtB,uBAAuB,GAKxB,MAAM,yBAAyB,CAAC;AACjC,OAAO,EAAE,cAAc,EAAE,cAAc,EAAE,WAAW,EAAE,QAAQ,EAAE,MAAM,cAAc,CAAC;AACrF,OAAO,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AACvD,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAExC,cAAc,iBAAiB,CAAC;AAEhC,OAAO,EACL,YAAY,EACZ,iBAAiB,EACjB,OAAO,EACP,iBAAiB,EACjB,YAAY,EACZ,YAAY,EACZ,YAAY,EACZ,cAAc,EACd,SAAS,EACT,UAAU,EACV,cAAc,EACd,WAAW,EACX,OAAO,EACP,oBAAoB,EACpB,cAAc,EACd,QAAQ,GACT,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,YAAY,EAAE,OAAO,EAAE,oBAAoB,EAAE,MAAM,WAAW,CAAC;AACtF,OAAO,EAAE,UAAU,EAAE,MAAM,6BAA6B,CAAC;AACzD,OAAO,EAAE,YAAY,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAC9D,OAAO,EAAE,OAAO,EAAE,iBAAiB,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAC3E,OAAO,EACL,oBAAoB,EACpB,oBAAoB,EACpB,kBAAkB,EAClB,uBAAuB,EACvB,sBAAsB,EACtB,uBAAuB,GAKxB,MAAM,yBAAyB,CAAC;AACjC,OAAO,EAAE,cAAc,EAAE,cAAc,EAAE,WAAW,EAAE,QAAQ,EAAE,MAAM,cAAc,CAAC;AACrF,OAAO,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AACvD,OAAO,EAAE,4BAA4B,EAAE,MAAM,oBAAoB,CAAC;AAClE,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAExC,cAAc,iBAAiB,CAAC;AAEhC,OAAO,EACL,YAAY,EACZ,iBAAiB,EACjB,OAAO,EACP,iBAAiB,EACjB,YAAY,EACZ,YAAY,EACZ,YAAY,EACZ,4BAA4B,EAC5B,cAAc,EACd,SAAS,EACT,UAAU,EACV,cAAc,EACd,WAAW,EACX,OAAO,EACP,oBAAoB,EACpB,cAAc,EACd,QAAQ,GACT,CAAC"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { FeatureFlagsRuntimeClient, RuntimeClientOptions } from '@workos-inc/node';
|
|
2
|
+
/**
|
|
3
|
+
* Returns a shared WorkOS Feature Flags runtime client.
|
|
4
|
+
*
|
|
5
|
+
* The runtime client keeps feature flag state in sync in the background, so it
|
|
6
|
+
* should be created once per server process instead of once per request.
|
|
7
|
+
* Options are only used when the client is created for the first time.
|
|
8
|
+
*/
|
|
9
|
+
export declare const getFeatureFlagsRuntimeClient: (options?: RuntimeClientOptions | undefined) => FeatureFlagsRuntimeClient;
|
|
@@ -5,6 +5,7 @@ import { authkit, authkitMiddleware, authkitProxy } from './middleware.js';
|
|
|
5
5
|
export { applyResponseHeaders, handleAuthkitHeaders, handleAuthkitProxy, partitionAuthkitHeaders, isAuthkitRequestHeader, AUTHKIT_REQUEST_HEADERS, type AuthkitHeadersResult, type AuthkitRedirectStatus, type AuthkitRequestHeader, type HandleAuthkitHeadersOptions, } from './middleware-helpers.js';
|
|
6
6
|
import { getTokenClaims, refreshSession, saveSession, withAuth } from './session.js';
|
|
7
7
|
import { validateApiKey } from './validate-api-key.js';
|
|
8
|
+
import { getFeatureFlagsRuntimeClient } from './feature-flags.js';
|
|
8
9
|
import { getWorkOS } from './workos.js';
|
|
9
10
|
export * from './interfaces.js';
|
|
10
|
-
export { AuthKitError, TokenRefreshError, authkit, authkitMiddleware, authkitProxy, getSignInUrl, getSignUpUrl, getTokenClaims, getWorkOS, handleAuth, refreshSession, saveSession, signOut, switchToOrganization, validateApiKey, withAuth, };
|
|
11
|
+
export { AuthKitError, TokenRefreshError, authkit, authkitMiddleware, authkitProxy, getSignInUrl, getSignUpUrl, getFeatureFlagsRuntimeClient, getTokenClaims, getWorkOS, handleAuth, refreshSession, saveSession, signOut, switchToOrganization, validateApiKey, withAuth, };
|
|
@@ -17,4 +17,4 @@ export declare function errorResponseWithFallback(errorBody: {
|
|
|
17
17
|
* @param fn - The function to be called once.
|
|
18
18
|
* @returns A function that can only be called once.
|
|
19
19
|
*/
|
|
20
|
-
export declare function lazy<
|
|
20
|
+
export declare function lazy<TArgs extends unknown[], TResult>(fn: (...args: TArgs) => TResult): (...args: TArgs) => TResult;
|
package/dist/esm/utils.js
CHANGED
|
@@ -36,9 +36,9 @@ export function errorResponseWithFallback(errorBody) {
|
|
|
36
36
|
export function lazy(fn) {
|
|
37
37
|
let called = false;
|
|
38
38
|
let result;
|
|
39
|
-
return () => {
|
|
39
|
+
return (...args) => {
|
|
40
40
|
if (!called) {
|
|
41
|
-
result = fn();
|
|
41
|
+
result = fn(...args);
|
|
42
42
|
called = true;
|
|
43
43
|
}
|
|
44
44
|
return result;
|
package/dist/esm/utils.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"utils.js","sourceRoot":"","sources":["../../src/utils.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAE3C;;;GAGG;AACH,MAAM,UAAU,yBAAyB,CAAC,OAAgB;IACxD,OAAO,CAAC,GAAG,CAAC,eAAe,EAAE,yDAAyD,CAAC,CAAC;IACxF,OAAO,CAAC,GAAG,CAAC,oBAAoB,EAAE,UAAU,CAAC,CAAC;AAChD,CAAC;AAED,MAAM,UAAU,oBAAoB,CAAC,WAAmB,EAAE,OAAiB;IACzE,MAAM,UAAU,GAAG,OAAO,CAAC,CAAC,CAAC,IAAI,OAAO,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,IAAI,OAAO,EAAE,CAAC;IAClE,UAAU,CAAC,GAAG,CAAC,UAAU,EAAE,WAAW,CAAC,CAAC;IAExC,mEAAmE;IACnE,iCAAiC;IACjC,OAAO,YAAY,EAAE,QAAQ;QAC3B,CAAC,CAAC,YAAY,CAAC,QAAQ,CAAC,WAAW,EAAE,EAAE,OAAO,EAAE,CAAC;QACjD,CAAC,CAAC,IAAI,QAAQ,CAAC,IAAI,EAAE,EAAE,MAAM,EAAE,GAAG,EAAE,OAAO,EAAE,UAAU,EAAE,CAAC,CAAC;AAC/D,CAAC;AAED,MAAM,UAAU,yBAAyB,CAAC,SAA8D;IACtG,mEAAmE;IACnE,iCAAiC;IACjC,OAAO,YAAY,EAAE,IAAI;QACvB,CAAC,CAAC,YAAY,CAAC,IAAI,CAAC,SAAS,EAAE,EAAE,MAAM,EAAE,GAAG,EAAE,CAAC;QAC/C,CAAC,CAAC,IAAI,QAAQ,CAAC,IAAI,CAAC,SAAS,CAAC,SAAS,CAAC,EAAE;YACtC,MAAM,EAAE,GAAG;YACX,OAAO,EAAE,EAAE,cAAc,EAAE,kBAAkB,EAAE;SAChD,CAAC,CAAC;AACT,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,IAAI,
|
|
1
|
+
{"version":3,"file":"utils.js","sourceRoot":"","sources":["../../src/utils.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAE3C;;;GAGG;AACH,MAAM,UAAU,yBAAyB,CAAC,OAAgB;IACxD,OAAO,CAAC,GAAG,CAAC,eAAe,EAAE,yDAAyD,CAAC,CAAC;IACxF,OAAO,CAAC,GAAG,CAAC,oBAAoB,EAAE,UAAU,CAAC,CAAC;AAChD,CAAC;AAED,MAAM,UAAU,oBAAoB,CAAC,WAAmB,EAAE,OAAiB;IACzE,MAAM,UAAU,GAAG,OAAO,CAAC,CAAC,CAAC,IAAI,OAAO,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,IAAI,OAAO,EAAE,CAAC;IAClE,UAAU,CAAC,GAAG,CAAC,UAAU,EAAE,WAAW,CAAC,CAAC;IAExC,mEAAmE;IACnE,iCAAiC;IACjC,OAAO,YAAY,EAAE,QAAQ;QAC3B,CAAC,CAAC,YAAY,CAAC,QAAQ,CAAC,WAAW,EAAE,EAAE,OAAO,EAAE,CAAC;QACjD,CAAC,CAAC,IAAI,QAAQ,CAAC,IAAI,EAAE,EAAE,MAAM,EAAE,GAAG,EAAE,OAAO,EAAE,UAAU,EAAE,CAAC,CAAC;AAC/D,CAAC;AAED,MAAM,UAAU,yBAAyB,CAAC,SAA8D;IACtG,mEAAmE;IACnE,iCAAiC;IACjC,OAAO,YAAY,EAAE,IAAI;QACvB,CAAC,CAAC,YAAY,CAAC,IAAI,CAAC,SAAS,EAAE,EAAE,MAAM,EAAE,GAAG,EAAE,CAAC;QAC/C,CAAC,CAAC,IAAI,QAAQ,CAAC,IAAI,CAAC,SAAS,CAAC,SAAS,CAAC,EAAE;YACtC,MAAM,EAAE,GAAG;YACX,OAAO,EAAE,EAAE,cAAc,EAAE,kBAAkB,EAAE;SAChD,CAAC,CAAC;AACT,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,IAAI,CAAmC,EAA+B;IACpF,IAAI,MAAM,GAAG,KAAK,CAAC;IACnB,IAAI,MAAe,CAAC;IACpB,OAAO,CAAC,GAAG,IAAW,EAAE,EAAE;QACxB,IAAI,CAAC,MAAM,EAAE,CAAC;YACZ,MAAM,GAAG,EAAE,CAAC,GAAG,IAAI,CAAC,CAAC;YACrB,MAAM,GAAG,IAAI,CAAC;QAChB,CAAC;QACD,OAAO,MAAM,CAAC;IAChB,CAAC,CAAC;AACJ,CAAC"}
|
package/package.json
CHANGED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { getWorkOS } from './workos.js';
|
|
2
|
+
|
|
3
|
+
describe('feature flags', () => {
|
|
4
|
+
afterEach(() => {
|
|
5
|
+
vi.restoreAllMocks();
|
|
6
|
+
vi.resetModules();
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
it('memoizes the feature flags runtime client', async () => {
|
|
10
|
+
const runtimeClient = {
|
|
11
|
+
close: vi.fn(),
|
|
12
|
+
getAllFlags: vi.fn(),
|
|
13
|
+
getFlag: vi.fn(),
|
|
14
|
+
getStats: vi.fn(),
|
|
15
|
+
isEnabled: vi.fn(),
|
|
16
|
+
waitUntilReady: vi.fn(),
|
|
17
|
+
};
|
|
18
|
+
const createRuntimeClient = vi
|
|
19
|
+
.spyOn(getWorkOS().featureFlags, 'createRuntimeClient')
|
|
20
|
+
.mockReturnValue(runtimeClient as never);
|
|
21
|
+
const { getFeatureFlagsRuntimeClient } = await import('./feature-flags.js');
|
|
22
|
+
|
|
23
|
+
expect(getFeatureFlagsRuntimeClient({ pollingIntervalMs: 5000 })).toBe(runtimeClient);
|
|
24
|
+
expect(getFeatureFlagsRuntimeClient({ pollingIntervalMs: 30000 })).toBe(runtimeClient);
|
|
25
|
+
expect(createRuntimeClient).toHaveBeenCalledTimes(1);
|
|
26
|
+
expect(createRuntimeClient).toHaveBeenCalledWith({ pollingIntervalMs: 5000 });
|
|
27
|
+
});
|
|
28
|
+
});
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { FeatureFlagsRuntimeClient, RuntimeClientOptions } from '@workos-inc/node';
|
|
2
|
+
import { lazy } from './utils.js';
|
|
3
|
+
import { getWorkOS } from './workos.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Returns a shared WorkOS Feature Flags runtime client.
|
|
7
|
+
*
|
|
8
|
+
* The runtime client keeps feature flag state in sync in the background, so it
|
|
9
|
+
* should be created once per server process instead of once per request.
|
|
10
|
+
* Options are only used when the client is created for the first time.
|
|
11
|
+
*/
|
|
12
|
+
export const getFeatureFlagsRuntimeClient = lazy(
|
|
13
|
+
(options?: RuntimeClientOptions): FeatureFlagsRuntimeClient => getWorkOS().featureFlags.createRuntimeClient(options),
|
|
14
|
+
);
|
package/src/index.ts
CHANGED
|
@@ -16,6 +16,7 @@ export {
|
|
|
16
16
|
} from './middleware-helpers.js';
|
|
17
17
|
import { getTokenClaims, refreshSession, saveSession, withAuth } from './session.js';
|
|
18
18
|
import { validateApiKey } from './validate-api-key.js';
|
|
19
|
+
import { getFeatureFlagsRuntimeClient } from './feature-flags.js';
|
|
19
20
|
import { getWorkOS } from './workos.js';
|
|
20
21
|
|
|
21
22
|
export * from './interfaces.js';
|
|
@@ -28,6 +29,7 @@ export {
|
|
|
28
29
|
authkitProxy,
|
|
29
30
|
getSignInUrl,
|
|
30
31
|
getSignUpUrl,
|
|
32
|
+
getFeatureFlagsRuntimeClient,
|
|
31
33
|
getTokenClaims,
|
|
32
34
|
getWorkOS,
|
|
33
35
|
handleAuth,
|
package/src/utils.ts
CHANGED
|
@@ -38,12 +38,12 @@ export function errorResponseWithFallback(errorBody: { error: { message: string;
|
|
|
38
38
|
* @param fn - The function to be called once.
|
|
39
39
|
* @returns A function that can only be called once.
|
|
40
40
|
*/
|
|
41
|
-
export function lazy<
|
|
41
|
+
export function lazy<TArgs extends unknown[], TResult>(fn: (...args: TArgs) => TResult): (...args: TArgs) => TResult {
|
|
42
42
|
let called = false;
|
|
43
|
-
let result:
|
|
44
|
-
return () => {
|
|
43
|
+
let result: TResult;
|
|
44
|
+
return (...args: TArgs) => {
|
|
45
45
|
if (!called) {
|
|
46
|
-
result = fn();
|
|
46
|
+
result = fn(...args);
|
|
47
47
|
called = true;
|
|
48
48
|
}
|
|
49
49
|
return result;
|