@ph-cms/client-sdk 0.1.5 → 0.1.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 CHANGED
@@ -10,6 +10,7 @@ This package provides:
10
10
  - **Integrated React Query support** (from v0.1.1)
11
11
  - **Automatic Auth State Management** (from v0.1.3)
12
12
  - **Conditional profile fetching & guaranteed post-login profile load** (from v0.1.4)
13
+ - **Firebase Auth State Sync** — automatic Firebase↔PH-CMS session synchronization (from v0.1.6)
13
14
 
14
15
  ## Installation
15
16
 
@@ -462,6 +463,130 @@ await client.auth.logout();
462
463
  | `useLogin()` | `{ mutateAsync: login }` |
463
464
  | `useLogout()` | `{ mutateAsync: logout }` |
464
465
 
466
+ ### Firebase Auth Sync
467
+
468
+ Firebase Authentication을 사용하는 경우, Firebase의 인증 상태가 변경될 때마다 PH-CMS 백엔드와 자동으로 동기화할 수 있습니다.
469
+
470
+ SDK는 두 가지 방식을 제공합니다:
471
+
472
+ | 방식 | 용도 |
473
+ |---|---|
474
+ | `useFirebaseAuthSync` 훅 | 기존 컴포넌트에 동기화 로직을 삽입할 때 |
475
+ | `<FirebaseAuthSync>` 컴포넌트 | 컴포넌트 트리를 감싸서 선언적으로 사용할 때 |
476
+
477
+ #### 동기화 동작
478
+
479
+ ```
480
+ Firebase onAuthStateChanged
481
+
482
+ ├─ fbUser 존재 + PH-CMS 비인증 상태
483
+ │ → fbUser.getIdToken()
484
+ │ → client.auth.loginWithFirebase({ idToken })
485
+ │ → provider.setTokens(...)
486
+ │ → refreshUser() ← me() 호출하여 프로필 로드
487
+
488
+ └─ fbUser null (로그아웃) + PH-CMS 인증 상태
489
+ → client.auth.logout()
490
+ → refreshUser() ← 상태 초기화
491
+ ```
492
+
493
+ #### `<FirebaseAuthSync>` 컴포넌트 사용
494
+
495
+ `<PHCMSProvider>` 안에서 사용합니다:
496
+
497
+ ```tsx
498
+ import { PHCMSProvider, FirebaseAuthSync } from '@ph-cms/client-sdk';
499
+ import { getAuth } from 'firebase/auth';
500
+
501
+ const firebaseAuth = getAuth(firebaseApp);
502
+
503
+ function App() {
504
+ return (
505
+ <PHCMSProvider client={client}>
506
+ <FirebaseAuthSync firebaseAuth={firebaseAuth}>
507
+ <MainContent />
508
+ </FirebaseAuthSync>
509
+ </PHCMSProvider>
510
+ );
511
+ }
512
+ ```
513
+
514
+ Props:
515
+
516
+ | Prop | 타입 | 기본값 | 설명 |
517
+ |---|---|---|---|
518
+ | `firebaseAuth` | `firebase/auth.Auth` | (필수) | Firebase Auth 인스턴스 |
519
+ | `logoutOnFirebaseSignOut` | `boolean` | `true` | Firebase 로그아웃 시 PH-CMS도 자동 로그아웃할지 여부 |
520
+ | `onSyncSuccess` | `() => void` | — | 토큰 교환 성공 시 콜백 |
521
+ | `onSyncError` | `(error: unknown) => void` | — | 토큰 교환 실패 시 콜백 |
522
+
523
+ #### `useFirebaseAuthSync` 훅 사용
524
+
525
+ ```tsx
526
+ import { useFirebaseAuthSync } from '@ph-cms/client-sdk';
527
+ import { getAuth } from 'firebase/auth';
528
+
529
+ const firebaseAuth = getAuth(firebaseApp);
530
+
531
+ function AppContent() {
532
+ const { isSyncing } = useFirebaseAuthSync({
533
+ firebaseAuth,
534
+ onSyncSuccess: () => console.log('Firebase↔PH-CMS 동기화 완료'),
535
+ onSyncError: (err) => console.error('동기화 실패:', err),
536
+ });
537
+
538
+ if (isSyncing) return <div>인증 동기화 중...</div>;
539
+
540
+ return <MainContent />;
541
+ }
542
+ ```
543
+
544
+ 반환값:
545
+
546
+ | 필드 | 타입 | 설명 |
547
+ |---|---|---|
548
+ | `isSyncing` | `boolean` | 토큰 교환 요청이 진행 중인지 여부 |
549
+
550
+ #### 전체 구성 예시 (Firebase + LocalAuthProvider)
551
+
552
+ ```tsx
553
+ import { PHCMSClient, LocalAuthProvider, FirebaseAuthProvider, PHCMSProvider, FirebaseAuthSync } from '@ph-cms/client-sdk';
554
+ import { initializeApp } from 'firebase/app';
555
+ import { getAuth } from 'firebase/auth';
556
+
557
+ // 1. Firebase 초기화
558
+ const firebaseApp = initializeApp({ /* config */ });
559
+ const firebaseAuth = getAuth(firebaseApp);
560
+
561
+ // 2. PH-CMS 프로바이더 (Firebase 타입)
562
+ const authProvider = new FirebaseAuthProvider(firebaseAuth, 'my_app_');
563
+
564
+ // 3. PH-CMS 클라이언트
565
+ const client = new PHCMSClient({
566
+ baseURL: 'https://api.example.com',
567
+ auth: authProvider,
568
+ });
569
+
570
+ // 4. 앱 구성
571
+ function App() {
572
+ return (
573
+ <PHCMSProvider client={client}>
574
+ <FirebaseAuthSync
575
+ firebaseAuth={firebaseAuth}
576
+ onSyncError={(err) => alert('인증 동기화에 실패했습니다.')}
577
+ >
578
+ <Router />
579
+ </FirebaseAuthSync>
580
+ </PHCMSProvider>
581
+ );
582
+ }
583
+ ```
584
+
585
+ 이 구성에서는:
586
+ - 사용자가 Firebase (예: Google 로그인)로 인증하면, `FirebaseAuthSync`가 자동으로 PH-CMS 토큰 교환 + 프로필 조회를 수행합니다.
587
+ - 사용자가 Firebase에서 로그아웃하면, PH-CMS 세션도 자동으로 정리됩니다.
588
+ - 앱 재방문 시 localStorage에 토큰이 남아있으면 `PHCMSProvider`가 자동으로 `me()`를 호출하여 세션을 복원합니다.
589
+
465
590
  ---
466
591
 
467
592
  ## Using Data Hooks
@@ -0,0 +1,108 @@
1
+ import type { Auth } from 'firebase/auth';
2
+ import React from 'react';
3
+ export interface UseFirebaseAuthSyncOptions {
4
+ /**
5
+ * Firebase Auth instance.
6
+ * Must be provided so the hook can subscribe to `onAuthStateChanged`.
7
+ */
8
+ firebaseAuth: Auth;
9
+ /**
10
+ * If true, automatically call `logout()` on the PH-CMS side
11
+ * when the Firebase user signs out.
12
+ * @default true
13
+ */
14
+ logoutOnFirebaseSignOut?: boolean;
15
+ /**
16
+ * Called when the sync exchange succeeds.
17
+ */
18
+ onSyncSuccess?: () => void;
19
+ /**
20
+ * Called when the sync exchange fails.
21
+ */
22
+ onSyncError?: (error: unknown) => void;
23
+ }
24
+ export interface UseFirebaseAuthSyncReturn {
25
+ /** Whether a token-exchange request is currently in flight. */
26
+ isSyncing: boolean;
27
+ }
28
+ /**
29
+ * `useFirebaseAuthSync`
30
+ *
31
+ * Subscribes to Firebase `onAuthStateChanged` and automatically synchronizes
32
+ * the Firebase authentication state with the PH-CMS backend:
33
+ *
34
+ * - **Firebase user signs in** and PH-CMS is not yet authenticated →
35
+ * obtains a Firebase ID token and calls `loginWithFirebase({ idToken })`
36
+ * which exchanges the token for PH-CMS access/refresh tokens and then
37
+ * fetches the user profile via `/auth/me`.
38
+ *
39
+ * - **Firebase user signs out** and PH-CMS is still authenticated →
40
+ * calls `logout()` on PH-CMS (configurable via `logoutOnFirebaseSignOut`).
41
+ *
42
+ * The hook must be rendered **inside** `<PHCMSProvider>` because it relies
43
+ * on context values (`isAuthenticated`, `isLoading`) and the `loginWithFirebase`
44
+ * / `logout` actions exposed by the auth module.
45
+ *
46
+ * ### Example
47
+ *
48
+ * ```tsx
49
+ * import { useFirebaseAuthSync } from '@ph-cms/client-sdk';
50
+ * import { getAuth } from 'firebase/auth';
51
+ *
52
+ * const firebaseAuth = getAuth(firebaseApp);
53
+ *
54
+ * function App() {
55
+ * useFirebaseAuthSync({ firebaseAuth });
56
+ * return <MainContent />;
57
+ * }
58
+ * ```
59
+ */
60
+ export declare const useFirebaseAuthSync: (options: UseFirebaseAuthSyncOptions) => UseFirebaseAuthSyncReturn;
61
+ export interface FirebaseAuthSyncProps {
62
+ /**
63
+ * Firebase Auth instance.
64
+ */
65
+ firebaseAuth: Auth;
66
+ /**
67
+ * If true, automatically call `logout()` on the PH-CMS side
68
+ * when the Firebase user signs out.
69
+ * @default true
70
+ */
71
+ logoutOnFirebaseSignOut?: boolean;
72
+ /**
73
+ * Called when the sync exchange succeeds.
74
+ */
75
+ onSyncSuccess?: () => void;
76
+ /**
77
+ * Called when the sync exchange fails.
78
+ */
79
+ onSyncError?: (error: unknown) => void;
80
+ children: React.ReactNode;
81
+ }
82
+ /**
83
+ * `<FirebaseAuthSync>`
84
+ *
85
+ * A convenience wrapper component that calls `useFirebaseAuthSync` internally.
86
+ * Place it **inside** `<PHCMSProvider>` and wrap the part of the tree that
87
+ * should wait for or react to Firebase↔PH-CMS auth synchronization.
88
+ *
89
+ * ### Example
90
+ *
91
+ * ```tsx
92
+ * import { PHCMSProvider, FirebaseAuthSync } from '@ph-cms/client-sdk';
93
+ * import { getAuth } from 'firebase/auth';
94
+ *
95
+ * const firebaseAuth = getAuth(firebaseApp);
96
+ *
97
+ * function App() {
98
+ * return (
99
+ * <PHCMSProvider client={client}>
100
+ * <FirebaseAuthSync firebaseAuth={firebaseAuth}>
101
+ * <MainContent />
102
+ * </FirebaseAuthSync>
103
+ * </PHCMSProvider>
104
+ * );
105
+ * }
106
+ * ```
107
+ */
108
+ export declare const FirebaseAuthSync: React.FC<FirebaseAuthSyncProps>;
@@ -0,0 +1,221 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.FirebaseAuthSync = exports.useFirebaseAuthSync = void 0;
37
+ const react_1 = __importStar(require("react"));
38
+ const context_1 = require("../context");
39
+ /**
40
+ * `useFirebaseAuthSync`
41
+ *
42
+ * Subscribes to Firebase `onAuthStateChanged` and automatically synchronizes
43
+ * the Firebase authentication state with the PH-CMS backend:
44
+ *
45
+ * - **Firebase user signs in** and PH-CMS is not yet authenticated →
46
+ * obtains a Firebase ID token and calls `loginWithFirebase({ idToken })`
47
+ * which exchanges the token for PH-CMS access/refresh tokens and then
48
+ * fetches the user profile via `/auth/me`.
49
+ *
50
+ * - **Firebase user signs out** and PH-CMS is still authenticated →
51
+ * calls `logout()` on PH-CMS (configurable via `logoutOnFirebaseSignOut`).
52
+ *
53
+ * The hook must be rendered **inside** `<PHCMSProvider>` because it relies
54
+ * on context values (`isAuthenticated`, `isLoading`) and the `loginWithFirebase`
55
+ * / `logout` actions exposed by the auth module.
56
+ *
57
+ * ### Example
58
+ *
59
+ * ```tsx
60
+ * import { useFirebaseAuthSync } from '@ph-cms/client-sdk';
61
+ * import { getAuth } from 'firebase/auth';
62
+ *
63
+ * const firebaseAuth = getAuth(firebaseApp);
64
+ *
65
+ * function App() {
66
+ * useFirebaseAuthSync({ firebaseAuth });
67
+ * return <MainContent />;
68
+ * }
69
+ * ```
70
+ */
71
+ const useFirebaseAuthSync = (options) => {
72
+ const { firebaseAuth, logoutOnFirebaseSignOut = true, onSyncSuccess, onSyncError, } = options;
73
+ const { client, isAuthenticated, isLoading, refreshUser } = (0, context_1.usePHCMSContext)();
74
+ // Use refs for values that change frequently but should not re-trigger
75
+ // the effect (which would tear down and re-create the Firebase listener).
76
+ const isAuthenticatedRef = (0, react_1.useRef)(isAuthenticated);
77
+ const isLoadingRef = (0, react_1.useRef)(isLoading);
78
+ const syncInProgressRef = (0, react_1.useRef)(false);
79
+ const isSyncingStateRef = (0, react_1.useRef)(false);
80
+ const [isSyncing, setIsSyncing] = react_1.default.useState(false);
81
+ // Keep refs up to date.
82
+ (0, react_1.useEffect)(() => {
83
+ isAuthenticatedRef.current = isAuthenticated;
84
+ }, [isAuthenticated]);
85
+ (0, react_1.useEffect)(() => {
86
+ isLoadingRef.current = isLoading;
87
+ }, [isLoading]);
88
+ // Stable callback refs so the effect closure doesn't go stale.
89
+ const onSyncSuccessRef = (0, react_1.useRef)(onSyncSuccess);
90
+ const onSyncErrorRef = (0, react_1.useRef)(onSyncError);
91
+ (0, react_1.useEffect)(() => {
92
+ onSyncSuccessRef.current = onSyncSuccess;
93
+ }, [onSyncSuccess]);
94
+ (0, react_1.useEffect)(() => {
95
+ onSyncErrorRef.current = onSyncError;
96
+ }, [onSyncError]);
97
+ const handleFirebaseUser = (0, react_1.useCallback)(async (fbUser) => {
98
+ if (fbUser) {
99
+ // Firebase user is signed in.
100
+ // Only exchange if PH-CMS is not already authenticated and not loading.
101
+ if (!isAuthenticatedRef.current &&
102
+ !isLoadingRef.current &&
103
+ !syncInProgressRef.current) {
104
+ syncInProgressRef.current = true;
105
+ isSyncingStateRef.current = true;
106
+ setIsSyncing(true);
107
+ try {
108
+ const idToken = await fbUser.getIdToken();
109
+ await client.auth.loginWithFirebase({ idToken });
110
+ // loginWithFirebase stores the tokens in the provider.
111
+ // Now refresh the context so `isAuthenticated` and `user` update.
112
+ await refreshUser();
113
+ onSyncSuccessRef.current?.();
114
+ }
115
+ catch (error) {
116
+ console.error('[useFirebaseAuthSync] Token exchange failed:', error);
117
+ onSyncErrorRef.current?.(error);
118
+ }
119
+ finally {
120
+ syncInProgressRef.current = false;
121
+ isSyncingStateRef.current = false;
122
+ setIsSyncing(false);
123
+ }
124
+ }
125
+ }
126
+ else {
127
+ // Firebase user signed out.
128
+ if (logoutOnFirebaseSignOut && isAuthenticatedRef.current) {
129
+ try {
130
+ await client.auth.logout();
131
+ await refreshUser();
132
+ }
133
+ catch (error) {
134
+ console.error('[useFirebaseAuthSync] Logout failed:', error);
135
+ }
136
+ }
137
+ }
138
+ },
139
+ // `client`, `refreshUser`, and `logoutOnFirebaseSignOut` are stable
140
+ // across renders (client is a singleton, refreshUser is memoized,
141
+ // logoutOnFirebaseSignOut is typically a constant).
142
+ [client, refreshUser, logoutOnFirebaseSignOut]);
143
+ (0, react_1.useEffect)(() => {
144
+ // Dynamically import the listener so that this file has zero runtime
145
+ // dependency on firebase/auth at the module level. The `Auth` object
146
+ // passed in already carries the implementation; we just need the
147
+ // `onAuthStateChanged` function reference from it.
148
+ //
149
+ // Firebase Auth's `onAuthStateChanged` is available on the Auth object
150
+ // via the modular API through the standalone function, but we can also
151
+ // call it as `firebaseAuth.onAuthStateChanged(...)` which works on the
152
+ // Auth instance directly (compat style). However, the recommended
153
+ // modular approach is to use the standalone function.
154
+ //
155
+ // To avoid directly importing from 'firebase/auth' at the top level
156
+ // (since it's an optional peer dep), we use a dynamic import wrapped
157
+ // in a try-catch. If the import fails, we fall back to a noop.
158
+ let unsubscribe = null;
159
+ let cancelled = false;
160
+ const setup = async () => {
161
+ try {
162
+ // Dynamic import so builds without firebase don't break.
163
+ const firebaseAuthModule = await Promise.resolve().then(() => __importStar(require('firebase/auth')));
164
+ if (cancelled)
165
+ return;
166
+ unsubscribe = firebaseAuthModule.onAuthStateChanged(firebaseAuth, (fbUser) => {
167
+ // Fire and forget; errors are caught inside handleFirebaseUser.
168
+ void handleFirebaseUser(fbUser);
169
+ });
170
+ }
171
+ catch {
172
+ // firebase/auth is not installed — this is fine, it's optional.
173
+ console.warn('[useFirebaseAuthSync] firebase/auth could not be imported. ' +
174
+ 'Make sure it is installed if you intend to use Firebase auth sync.');
175
+ }
176
+ };
177
+ void setup();
178
+ return () => {
179
+ cancelled = true;
180
+ unsubscribe?.();
181
+ };
182
+ }, [firebaseAuth, handleFirebaseUser]);
183
+ return { isSyncing };
184
+ };
185
+ exports.useFirebaseAuthSync = useFirebaseAuthSync;
186
+ /**
187
+ * `<FirebaseAuthSync>`
188
+ *
189
+ * A convenience wrapper component that calls `useFirebaseAuthSync` internally.
190
+ * Place it **inside** `<PHCMSProvider>` and wrap the part of the tree that
191
+ * should wait for or react to Firebase↔PH-CMS auth synchronization.
192
+ *
193
+ * ### Example
194
+ *
195
+ * ```tsx
196
+ * import { PHCMSProvider, FirebaseAuthSync } from '@ph-cms/client-sdk';
197
+ * import { getAuth } from 'firebase/auth';
198
+ *
199
+ * const firebaseAuth = getAuth(firebaseApp);
200
+ *
201
+ * function App() {
202
+ * return (
203
+ * <PHCMSProvider client={client}>
204
+ * <FirebaseAuthSync firebaseAuth={firebaseAuth}>
205
+ * <MainContent />
206
+ * </FirebaseAuthSync>
207
+ * </PHCMSProvider>
208
+ * );
209
+ * }
210
+ * ```
211
+ */
212
+ const FirebaseAuthSync = ({ firebaseAuth, logoutOnFirebaseSignOut, onSyncSuccess, onSyncError, children, }) => {
213
+ (0, exports.useFirebaseAuthSync)({
214
+ firebaseAuth,
215
+ logoutOnFirebaseSignOut,
216
+ onSyncSuccess,
217
+ onSyncError,
218
+ });
219
+ return react_1.default.createElement(react_1.default.Fragment, null, children);
220
+ };
221
+ exports.FirebaseAuthSync = FirebaseAuthSync;
package/dist/index.d.ts CHANGED
@@ -1,17 +1,18 @@
1
+ export * from './auth/firebase-provider';
1
2
  export * from './auth/interfaces';
2
3
  export * from './auth/local-provider';
3
- export * from './auth/firebase-provider';
4
- export * from './errors';
5
4
  export * from './client';
5
+ export * from './errors';
6
6
  export * from './modules/auth';
7
- export * from './modules/content';
8
7
  export * from './modules/channel';
9
- export * from './modules/terms';
8
+ export * from './modules/content';
10
9
  export * from './modules/media';
10
+ export * from './modules/terms';
11
11
  export * from './context';
12
12
  export * from './hooks/useAuth';
13
- export * from './hooks/useContent';
14
13
  export * from './hooks/useChannel';
15
- export * from './hooks/useTerms';
14
+ export * from './hooks/useContent';
15
+ export * from './hooks/useFirebaseAuthSync';
16
16
  export * from './hooks/useMedia';
17
+ export * from './hooks/useTerms';
17
18
  export * from './types';
package/dist/index.js CHANGED
@@ -14,20 +14,21 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
14
  for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
15
  };
16
16
  Object.defineProperty(exports, "__esModule", { value: true });
17
+ __exportStar(require("./auth/firebase-provider"), exports);
17
18
  __exportStar(require("./auth/interfaces"), exports);
18
19
  __exportStar(require("./auth/local-provider"), exports);
19
- __exportStar(require("./auth/firebase-provider"), exports);
20
- __exportStar(require("./errors"), exports);
21
20
  __exportStar(require("./client"), exports);
21
+ __exportStar(require("./errors"), exports);
22
22
  __exportStar(require("./modules/auth"), exports);
23
- __exportStar(require("./modules/content"), exports);
24
23
  __exportStar(require("./modules/channel"), exports);
25
- __exportStar(require("./modules/terms"), exports);
24
+ __exportStar(require("./modules/content"), exports);
26
25
  __exportStar(require("./modules/media"), exports);
26
+ __exportStar(require("./modules/terms"), exports);
27
27
  __exportStar(require("./context"), exports);
28
28
  __exportStar(require("./hooks/useAuth"), exports);
29
- __exportStar(require("./hooks/useContent"), exports);
30
29
  __exportStar(require("./hooks/useChannel"), exports);
31
- __exportStar(require("./hooks/useTerms"), exports);
30
+ __exportStar(require("./hooks/useContent"), exports);
31
+ __exportStar(require("./hooks/useFirebaseAuthSync"), exports);
32
32
  __exportStar(require("./hooks/useMedia"), exports);
33
+ __exportStar(require("./hooks/useTerms"), exports);
33
34
  __exportStar(require("./types"), exports);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ph-cms/client-sdk",
3
- "version": "0.1.5",
3
+ "version": "0.1.6",
4
4
  "description": "Unified PH-CMS Client SDK (React + Core)",
5
5
  "keywords": [],
6
6
  "license": "MIT",