@yanolja-next/next-hub-bo-sdk 0.0.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.
package/README.md ADDED
@@ -0,0 +1,127 @@
1
+ ---
2
+ gdrive_sync: true
3
+ gdrive_pdf_name: "NCP Backoffice SDK"
4
+ ---
5
+
6
+ # NCP Backoffice SDK (Beta)
7
+
8
+ NCP Backoffice SDK는 Next Hub Console에 iframe으로 포함되는 Service Backoffice를 위한
9
+ 초기화, 등록 operation 호출, 백오피스 전용 토큰 갱신 기능을 제공한다.
10
+
11
+ ## 토큰 구분
12
+
13
+ | Context 필드 | 용도 | 전송 대상 |
14
+ | --- | --- | --- |
15
+ | `token` | Next Hub에 등록된 operation 호출 | Next Hub Core |
16
+ | `backofficeToken` | 서비스 backend 직접 호출 | 해당 서비스 backend |
17
+
18
+ 기존 `token`과 `projectId` 키는 변경되지 않는다. `backofficeToken`과
19
+ `backofficeTokenExpiresAt`은 추가 필드이므로 기존 SDK 사용 코드는 그대로 동작한다.
20
+ 백오피스 전용 토큰은 SDK 메모리에만 보관하며 브라우저 영구 저장소에는 기록하지 않는다.
21
+
22
+ ## 초기화
23
+
24
+ ### `waitForContext(targetOrigin, timeoutMs?)`
25
+
26
+ Console에서 context를 받을 때까지 기다린다. 다른 SDK API보다 먼저 호출해야 한다.
27
+
28
+ - `targetOrigin: string | string[]`: 신뢰할 Console origin
29
+ - `timeoutMs?: number`: 초기화 제한 시간, 기본값 10초
30
+ - 반환값: `Promise<void>`
31
+
32
+ ```tsx
33
+ import { waitForContext } from '@yanolja-next/next-hub-bo-sdk';
34
+
35
+ await waitForContext('https://console.ynext.cloud');
36
+ ```
37
+
38
+ 초기화 시 Console이 보내는 payload는 다음과 같다.
39
+
40
+ ```ts
41
+ interface InitMessage {
42
+ type: 'INIT';
43
+ token: string;
44
+ projectId: string;
45
+ backofficeToken?: string;
46
+ backofficeTokenExpiresAt?: string;
47
+ }
48
+ ```
49
+
50
+ ## 등록 operation 호출
51
+
52
+ ### `getApiClient(serviceName, baseUrl, options?)`
53
+
54
+ 기존 세션 토큰과 프로젝트 context를 사용하는 Axios client를 생성한다.
55
+
56
+ ```tsx
57
+ import { getApiClient, waitForContext } from '@yanolja-next/next-hub-bo-sdk';
58
+
59
+ await waitForContext('https://console.ynext.cloud');
60
+ const client = getApiClient('my-service', 'https://api.example.com');
61
+ const response = await client.get('/resources');
62
+ ```
63
+
64
+ 이 client는 `Authorization: Bearer {token}`과
65
+ `X-NEXTHUB-CLIENT-PROJECT-ID: {projectId}`를 Next Hub Core 요청에 추가한다. 서비스
66
+ backend 직접 호출에는 이 client 대신 백오피스 전용 토큰을 사용한다.
67
+
68
+ ## 서비스 backend 직접 호출
69
+
70
+ ### `getBackofficeToken()`
71
+
72
+ 현재 백오피스 전용 토큰과 만료 시각을 읽는다. `waitForContext()`가 전용 토큰 없이
73
+ 초기화되었거나 아직 호출되지 않았다면 오류를 던진다.
74
+
75
+ ```tsx
76
+ import { getBackofficeToken, waitForContext } from '@yanolja-next/next-hub-bo-sdk';
77
+
78
+ await waitForContext('https://console.ynext.cloud');
79
+ const { backofficeToken, backofficeTokenExpiresAt } = getBackofficeToken();
80
+
81
+ await fetch('https://service.example.com/admin/resources', {
82
+ headers: { Authorization: `Bearer ${backofficeToken}` },
83
+ });
84
+ ```
85
+
86
+ ### `refreshBackofficeToken()`
87
+
88
+ 부모 Console에 새 토큰을 요청하고 갱신된 토큰 context를 반환한다. 동시에 여러 번
89
+ 호출하면 같은 Promise를 반환해 발급 요청을 한 번만 전송한다. SDK는 만료 1분 전에도
90
+ 같은 갱신 흐름을 자동 실행한다.
91
+
92
+ ```tsx
93
+ import { refreshBackofficeToken } from '@yanolja-next/next-hub-bo-sdk';
94
+
95
+ const refreshed = await refreshBackofficeToken();
96
+ ```
97
+
98
+ 갱신 실패 시 기존 토큰을 덮어쓰지 않는다. 오류 메시지에는 토큰 값을 포함하지 않는다.
99
+
100
+ ## 초기화 오류 알림
101
+
102
+ ### `notifyInitError(message, targetOrigin)`
103
+
104
+ 백오피스 자체 초기화가 실패했음을 Console에 알린다.
105
+
106
+ ```tsx
107
+ import { notifyInitError } from '@yanolja-next/next-hub-bo-sdk';
108
+
109
+ notifyInitError('설정을 불러오지 못했습니다.', 'https://console.ynext.cloud');
110
+ ```
111
+
112
+ ## 서비스 backend 검증
113
+
114
+ 서비스 backend는 요청마다 Next Hub에 확인 요청을 보내지 않는다. Auth의
115
+ `/.well-known/jwks.json`을 캐시해 JWT 서명을 로컬 검증하고 claim을 함께 확인해야 한다.
116
+ 구체적인 검증 절차와 예제는
117
+ [백오피스 요청 인증 가이드](https://github.com/yanolja-org/next-hub/blob/main/docs/backoffice-authentication.md)를
118
+ 참고한다.
119
+
120
+ ## 템플릿
121
+
122
+ 백오피스 프로젝트는 템플릿 저장소에서 시작할 수 있다.
123
+
124
+ ```bash
125
+ git clone https://github.com/yanolja-org/iab-ncp-service-backoffice-template.git my-backoffice
126
+ cd my-backoffice
127
+ ```
@@ -0,0 +1,25 @@
1
+ export interface BackofficeTokenContext {
2
+ backofficeToken: string;
3
+ backofficeTokenExpiresAt: string;
4
+ }
5
+ export declare class BackofficeTokenManager {
6
+ private context;
7
+ private origins;
8
+ private refreshPromise;
9
+ private resolveRefresh;
10
+ private rejectRefresh;
11
+ private refreshTimer;
12
+ private refreshTimeout;
13
+ initialize(context: BackofficeTokenContext | null, origins: string[]): void;
14
+ reset(): void;
15
+ get(): BackofficeTokenContext;
16
+ refresh(): Promise<BackofficeTokenContext>;
17
+ private readonly handleMessage;
18
+ private completeRefresh;
19
+ private failRefresh;
20
+ private scheduleRefresh;
21
+ private clearTimer;
22
+ private clearRefreshTimeout;
23
+ private clearPendingRefresh;
24
+ }
25
+ export declare const isBackofficeTokenContext: (value: unknown) => value is BackofficeTokenContext;
package/dist/index.cjs ADDED
@@ -0,0 +1 @@
1
+ "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const p=require("axios"),d="READY",R="INIT",k="INIT_ERROR",u="BACKOFFICE_TOKEN_REFRESH_REQUEST",_="BACKOFFICE_TOKEN_REFRESH_RESPONSE",f="BACKOFFICE_TOKEN_ERROR",A=6e4,g=1e4;class w{constructor(){this.context=null,this.origins=[],this.refreshPromise=null,this.resolveRefresh=null,this.rejectRefresh=null,this.refreshTimer=null,this.refreshTimeout=null,this.handleMessage=e=>{var t,s,o;if(this.origins.includes(e.origin)){if(((t=e.data)==null?void 0:t.type)===_&&h(e.data)){this.completeRefresh({backofficeToken:e.data.backofficeToken,backofficeTokenExpiresAt:e.data.backofficeTokenExpiresAt});return}if(((s=e.data)==null?void 0:s.type)===f&&((o=e.data)==null?void 0:o.phase)==="refresh"){const n=typeof e.data.code=="string"?e.data.code:"UNKNOWN";this.failRefresh(new Error(`Backoffice token refresh failed: ${n}`))}}}}initialize(e,t){this.reset(),this.context=e,this.origins=[...t],window.addEventListener("message",this.handleMessage),e&&this.scheduleRefresh(e.backofficeTokenExpiresAt)}reset(){window.removeEventListener("message",this.handleMessage),this.clearTimer(),this.clearRefreshTimeout(),this.rejectRefresh&&this.rejectRefresh(new Error("Backoffice token context was reset")),this.context=null,this.origins=[],this.clearPendingRefresh()}get(){if(!this.context)throw new Error("Backoffice token is not initialized. Call waitForContext() first.");return{...this.context}}refresh(){if(!this.context||this.origins.length===0)return Promise.reject(new Error("Backoffice token is not initialized. Call waitForContext() first."));if(this.refreshPromise)return this.refreshPromise;this.refreshPromise=new Promise((e,t)=>{this.resolveRefresh=e,this.rejectRefresh=t,this.refreshTimeout=setTimeout(()=>{this.failRefresh(new Error("Backoffice token refresh timed out"))},g)});for(const e of this.origins)window.parent.postMessage({type:u},e);return this.refreshPromise}completeRefresh(e){const t=this.resolveRefresh;this.context=e,this.clearRefreshTimeout(),this.clearPendingRefresh(),this.scheduleRefresh(e.backofficeTokenExpiresAt),t==null||t({...e})}failRefresh(e){const t=this.rejectRefresh;this.clearRefreshTimeout(),this.clearPendingRefresh(),t==null||t(e)}scheduleRefresh(e){this.clearTimer();const t=Math.max(0,Date.parse(e)-Date.now()-A);this.refreshTimer=setTimeout(()=>{this.refresh().catch(()=>{})},t)}clearTimer(){this.refreshTimer!==null&&(clearTimeout(this.refreshTimer),this.refreshTimer=null)}clearRefreshTimeout(){this.refreshTimeout!==null&&(clearTimeout(this.refreshTimeout),this.refreshTimeout=null)}clearPendingRefresh(){this.refreshPromise=null,this.resolveRefresh=null,this.rejectRefresh=null}}const h=r=>{if(typeof r!="object"||r===null)return!1;const e=r;return typeof e.backofficeToken=="string"&&typeof e.backofficeTokenExpiresAt=="string"&&!Number.isNaN(Date.parse(e.backofficeTokenExpiresAt))};class C{constructor(){this.context=null}set(e){this.context=e}get(){return this.context}}const m=new C,c=new w,I=(r,e=1e4)=>{const t=Array.isArray(r)?r:[r];return c.reset(),new Promise((s,o)=>{const n=setTimeout(()=>{window.removeEventListener("message",a),o(new Error("Timeout waiting for context"))},e),a=i=>{var E,l,T;if(t.includes(i.origin)&&((E=i.data)==null?void 0:E.type)===f&&((l=i.data)==null?void 0:l.phase)==="init"){clearTimeout(n),window.removeEventListener("message",a);const S=typeof i.data.code=="string"?i.data.code:"UNKNOWN";o(new Error(`Backoffice token initialization failed: ${S}`));return}!t.includes(i.origin)||((T=i.data)==null?void 0:T.type)!==R||typeof i.data.token!="string"||typeof i.data.projectId!="string"||!M(i.data)||(clearTimeout(n),window.removeEventListener("message",a),m.set({token:i.data.token,projectId:i.data.projectId}),c.initialize(h(i.data)?{backofficeToken:i.data.backofficeToken,backofficeTokenExpiresAt:i.data.backofficeTokenExpiresAt}:null,t),s())};window.addEventListener("message",a);for(const i of t)window.parent.postMessage({type:d},i)})},O=()=>c.get(),F=()=>c.refresh(),N=(r,e)=>{const t=Array.isArray(e)?e:[e];for(const s of t)window.parent.postMessage({type:k,message:r},s)},P="X-NEXTHUB-CLIENT-PROJECT-ID",x=(r,e,t)=>{const s=p.create({baseURL:`${e}/services/${r}`,headers:{"Content-Type":"application/json",...t==null?void 0:t.headers}});return s.interceptors.request.use(o=>{const n=m.get();if(!n)throw new Error("Context not initialized. Call waitForContext() first.");return o.headers[P]=n.projectId,o.headers.Authorization=`Bearer ${n.token}`,o}),s},M=r=>{const e=r.backofficeToken!==void 0,t=r.backofficeTokenExpiresAt!==void 0;return!e&&!t||h(r)};exports.BACKOFFICE_TOKEN_ERROR_MESSAGE_TYPE=f;exports.BACKOFFICE_TOKEN_REFRESH_REQUEST_MESSAGE_TYPE=u;exports.BACKOFFICE_TOKEN_REFRESH_RESPONSE_MESSAGE_TYPE=_;exports.INIT_ERROR_MESSAGE_TYPE=k;exports.INIT_MESSAGE_TYPE=R;exports.READY_MESSAGE_TYPE=d;exports.getApiClient=x;exports.getBackofficeToken=O;exports.notifyInitError=N;exports.refreshBackofficeToken=F;exports.waitForContext=I;
@@ -0,0 +1,12 @@
1
+ import { AxiosInstance } from 'axios';
2
+ import { BackofficeTokenContext } from './backoffice-token-manager';
3
+ export type { BackofficeTokenContext } from './backoffice-token-manager';
4
+ export * from './message-types';
5
+ export declare const waitForContext: (targetOrigin: string | string[], timeoutMs?: number) => Promise<void>;
6
+ export declare const getBackofficeToken: () => BackofficeTokenContext;
7
+ export declare const refreshBackofficeToken: () => Promise<BackofficeTokenContext>;
8
+ export declare const notifyInitError: (message: string, targetOrigin: string | string[]) => void;
9
+ export declare const getApiClient: (serviceName: string, baseUrl: string, options?: ApiClientOptions) => AxiosInstance;
10
+ interface ApiClientOptions {
11
+ headers?: Record<string, string>;
12
+ }
package/dist/index.js ADDED
@@ -0,0 +1,154 @@
1
+ import k from "axios";
2
+ const u = "READY", m = "INIT", p = "INIT_ERROR", w = "BACKOFFICE_TOKEN_REFRESH_REQUEST", g = "BACKOFFICE_TOKEN_REFRESH_RESPONSE", d = "BACKOFFICE_TOKEN_ERROR", _ = 6e4, A = 1e4;
3
+ class x {
4
+ constructor() {
5
+ this.context = null, this.origins = [], this.refreshPromise = null, this.resolveRefresh = null, this.rejectRefresh = null, this.refreshTimer = null, this.refreshTimeout = null, this.handleMessage = (e) => {
6
+ var t, i, o;
7
+ if (this.origins.includes(e.origin)) {
8
+ if (((t = e.data) == null ? void 0 : t.type) === g && f(e.data)) {
9
+ this.completeRefresh({
10
+ backofficeToken: e.data.backofficeToken,
11
+ backofficeTokenExpiresAt: e.data.backofficeTokenExpiresAt
12
+ });
13
+ return;
14
+ }
15
+ if (((i = e.data) == null ? void 0 : i.type) === d && ((o = e.data) == null ? void 0 : o.phase) === "refresh") {
16
+ const n = typeof e.data.code == "string" ? e.data.code : "UNKNOWN";
17
+ this.failRefresh(new Error(`Backoffice token refresh failed: ${n}`));
18
+ }
19
+ }
20
+ };
21
+ }
22
+ initialize(e, t) {
23
+ this.reset(), this.context = e, this.origins = [...t], window.addEventListener("message", this.handleMessage), e && this.scheduleRefresh(e.backofficeTokenExpiresAt);
24
+ }
25
+ reset() {
26
+ window.removeEventListener("message", this.handleMessage), this.clearTimer(), this.clearRefreshTimeout(), this.rejectRefresh && this.rejectRefresh(new Error("Backoffice token context was reset")), this.context = null, this.origins = [], this.clearPendingRefresh();
27
+ }
28
+ get() {
29
+ if (!this.context)
30
+ throw new Error("Backoffice token is not initialized. Call waitForContext() first.");
31
+ return { ...this.context };
32
+ }
33
+ refresh() {
34
+ if (!this.context || this.origins.length === 0)
35
+ return Promise.reject(
36
+ new Error("Backoffice token is not initialized. Call waitForContext() first.")
37
+ );
38
+ if (this.refreshPromise)
39
+ return this.refreshPromise;
40
+ this.refreshPromise = new Promise((e, t) => {
41
+ this.resolveRefresh = e, this.rejectRefresh = t, this.refreshTimeout = setTimeout(() => {
42
+ this.failRefresh(new Error("Backoffice token refresh timed out"));
43
+ }, A);
44
+ });
45
+ for (const e of this.origins)
46
+ window.parent.postMessage({ type: w }, e);
47
+ return this.refreshPromise;
48
+ }
49
+ completeRefresh(e) {
50
+ const t = this.resolveRefresh;
51
+ this.context = e, this.clearRefreshTimeout(), this.clearPendingRefresh(), this.scheduleRefresh(e.backofficeTokenExpiresAt), t == null || t({ ...e });
52
+ }
53
+ failRefresh(e) {
54
+ const t = this.rejectRefresh;
55
+ this.clearRefreshTimeout(), this.clearPendingRefresh(), t == null || t(e);
56
+ }
57
+ scheduleRefresh(e) {
58
+ this.clearTimer();
59
+ const t = Math.max(0, Date.parse(e) - Date.now() - _);
60
+ this.refreshTimer = setTimeout(() => {
61
+ this.refresh().catch(() => {
62
+ });
63
+ }, t);
64
+ }
65
+ clearTimer() {
66
+ this.refreshTimer !== null && (clearTimeout(this.refreshTimer), this.refreshTimer = null);
67
+ }
68
+ clearRefreshTimeout() {
69
+ this.refreshTimeout !== null && (clearTimeout(this.refreshTimeout), this.refreshTimeout = null);
70
+ }
71
+ clearPendingRefresh() {
72
+ this.refreshPromise = null, this.resolveRefresh = null, this.rejectRefresh = null;
73
+ }
74
+ }
75
+ const f = (r) => {
76
+ if (typeof r != "object" || r === null)
77
+ return !1;
78
+ const e = r;
79
+ return typeof e.backofficeToken == "string" && typeof e.backofficeTokenExpiresAt == "string" && !Number.isNaN(Date.parse(e.backofficeTokenExpiresAt));
80
+ };
81
+ class C {
82
+ constructor() {
83
+ this.context = null;
84
+ }
85
+ set(e) {
86
+ this.context = e;
87
+ }
88
+ get() {
89
+ return this.context;
90
+ }
91
+ }
92
+ const T = new C(), c = new x(), P = (r, e = 1e4) => {
93
+ const t = Array.isArray(r) ? r : [r];
94
+ return c.reset(), new Promise((i, o) => {
95
+ const n = setTimeout(() => {
96
+ window.removeEventListener("message", a), o(new Error("Timeout waiting for context"));
97
+ }, e), a = (s) => {
98
+ var h, E, l;
99
+ if (t.includes(s.origin) && ((h = s.data) == null ? void 0 : h.type) === d && ((E = s.data) == null ? void 0 : E.phase) === "init") {
100
+ clearTimeout(n), window.removeEventListener("message", a);
101
+ const R = typeof s.data.code == "string" ? s.data.code : "UNKNOWN";
102
+ o(new Error(`Backoffice token initialization failed: ${R}`));
103
+ return;
104
+ }
105
+ !t.includes(s.origin) || ((l = s.data) == null ? void 0 : l.type) !== m || typeof s.data.token != "string" || typeof s.data.projectId != "string" || !S(s.data) || (clearTimeout(n), window.removeEventListener("message", a), T.set({
106
+ token: s.data.token,
107
+ projectId: s.data.projectId
108
+ }), c.initialize(
109
+ f(s.data) ? {
110
+ backofficeToken: s.data.backofficeToken,
111
+ backofficeTokenExpiresAt: s.data.backofficeTokenExpiresAt
112
+ } : null,
113
+ t
114
+ ), i());
115
+ };
116
+ window.addEventListener("message", a);
117
+ for (const s of t)
118
+ window.parent.postMessage({ type: u }, s);
119
+ });
120
+ }, F = () => c.get(), O = () => c.refresh(), y = (r, e) => {
121
+ const t = Array.isArray(e) ? e : [e];
122
+ for (const i of t)
123
+ window.parent.postMessage({ type: p, message: r }, i);
124
+ }, I = "X-NEXTHUB-CLIENT-PROJECT-ID", M = (r, e, t) => {
125
+ const i = k.create({
126
+ baseURL: `${e}/services/${r}`,
127
+ headers: {
128
+ "Content-Type": "application/json",
129
+ ...t == null ? void 0 : t.headers
130
+ }
131
+ });
132
+ return i.interceptors.request.use((o) => {
133
+ const n = T.get();
134
+ if (!n)
135
+ throw new Error("Context not initialized. Call waitForContext() first.");
136
+ return o.headers[I] = n.projectId, o.headers.Authorization = `Bearer ${n.token}`, o;
137
+ }), i;
138
+ }, S = (r) => {
139
+ const e = r.backofficeToken !== void 0, t = r.backofficeTokenExpiresAt !== void 0;
140
+ return !e && !t || f(r);
141
+ };
142
+ export {
143
+ d as BACKOFFICE_TOKEN_ERROR_MESSAGE_TYPE,
144
+ w as BACKOFFICE_TOKEN_REFRESH_REQUEST_MESSAGE_TYPE,
145
+ g as BACKOFFICE_TOKEN_REFRESH_RESPONSE_MESSAGE_TYPE,
146
+ p as INIT_ERROR_MESSAGE_TYPE,
147
+ m as INIT_MESSAGE_TYPE,
148
+ u as READY_MESSAGE_TYPE,
149
+ M as getApiClient,
150
+ F as getBackofficeToken,
151
+ y as notifyInitError,
152
+ O as refreshBackofficeToken,
153
+ P as waitForContext
154
+ };
@@ -0,0 +1,6 @@
1
+ export declare const READY_MESSAGE_TYPE = "READY";
2
+ export declare const INIT_MESSAGE_TYPE = "INIT";
3
+ export declare const INIT_ERROR_MESSAGE_TYPE = "INIT_ERROR";
4
+ export declare const BACKOFFICE_TOKEN_REFRESH_REQUEST_MESSAGE_TYPE = "BACKOFFICE_TOKEN_REFRESH_REQUEST";
5
+ export declare const BACKOFFICE_TOKEN_REFRESH_RESPONSE_MESSAGE_TYPE = "BACKOFFICE_TOKEN_REFRESH_RESPONSE";
6
+ export declare const BACKOFFICE_TOKEN_ERROR_MESSAGE_TYPE = "BACKOFFICE_TOKEN_ERROR";
package/package.json ADDED
@@ -0,0 +1,44 @@
1
+ {
2
+ "name": "@yanolja-next/next-hub-bo-sdk",
3
+ "private": false,
4
+ "version": "0.0.1",
5
+ "description": "NCP Backoffice SDK - Build and deploy service backoffice UIs",
6
+ "type": "module",
7
+ "scripts": {
8
+ "build": "vite build",
9
+ "test": "vitest run",
10
+ "test:watch": "vitest"
11
+ },
12
+ "main": "./dist/index.cjs",
13
+ "types": "./dist/index.d.ts",
14
+ "exports": {
15
+ ".": {
16
+ "types": "./dist/index.d.ts",
17
+ "import": "./dist/index.js",
18
+ "require": "./dist/index.cjs"
19
+ }
20
+ },
21
+ "files": [
22
+ "dist"
23
+ ],
24
+ "repository": {
25
+ "type": "git",
26
+ "url": "git+https://github.com/yanolja-org/next-hub.git",
27
+ "directory": "bo-sdk"
28
+ },
29
+ "publishConfig": {
30
+ "access": "public",
31
+ "registry": "https://registry.npmjs.org"
32
+ },
33
+ "dependencies": {
34
+ "axios": "^1.7.2"
35
+ },
36
+ "devDependencies": {
37
+ "jsdom": "^26.0.0",
38
+ "typescript": "^5.4.4",
39
+ "unplugin-dts": "^1.0.3",
40
+ "vite": "^4.4.5",
41
+ "vite-plugin-externalize-deps": "^0.10.0",
42
+ "vitest": "^3.0.0"
43
+ }
44
+ }