@obm/apaas-embed-sdk 0.0.0-stage → 0.1.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/LICENSE +21 -0
- package/README.md +135 -2
- package/dist/index.cjs +28 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +128 -0
- package/dist/index.d.ts +128 -0
- package/dist/index.js +28 -0
- package/dist/index.js.map +1 -0
- package/package.json +66 -4
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 A-pass Team
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,136 @@
|
|
|
1
|
-
#
|
|
1
|
+
# MediPaaS Embed SDK
|
|
2
|
+
|
|
3
|
+
在第三方网页中通过 iframe 打开 MediPaaS 应用页面。SDK 负责嵌入与实例管理;第三方后端负责获取 embed token。
|
|
2
4
|
|
|
3
|
-
|
|
5
|
+
## 安装
|
|
6
|
+
|
|
7
|
+
发布到 npm 后,可在第三方项目中安装:
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install @obm/apaas-embed-sdk
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
当前为 `0.1.0` 初期 MVP 版本,API 仍可能调整。支持 ESM、CommonJS 与 TypeScript 类型声明。iframe 嵌入需要浏览器 DOM 环境。
|
|
14
|
+
|
|
15
|
+
```ts
|
|
16
|
+
import { ApassEmbedSDK, type EmbedParamValue } from '@obm/apaas-embed-sdk'
|
|
17
|
+
|
|
18
|
+
// getToken 是第三方系统自己的接口。后端调用
|
|
19
|
+
// POST /api/auth/token/embed?clientId=xxx,返回 token 和 runtimeAppId。
|
|
20
|
+
const { token, runtimeAppId } = await getToken()
|
|
21
|
+
const sdk = new ApassEmbedSDK({ baseUrl: 'https://your-medipaas-domain.com' })
|
|
22
|
+
const params: Record<string, EmbedParamValue> = {
|
|
23
|
+
orderId: '000123', mode: 'detail', editable: false,
|
|
24
|
+
}
|
|
25
|
+
const instance = await sdk.embed({
|
|
26
|
+
appId: String(runtimeAppId), apiName: 'order_detail', token,
|
|
27
|
+
containerId: 'medipaas-container', params,
|
|
28
|
+
onError: error => console.error(error),
|
|
29
|
+
onEvent: ({ name, payload }) => {
|
|
30
|
+
if (name === 'order.saved') refreshOrder((payload as { orderId: string }).orderId)
|
|
31
|
+
},
|
|
32
|
+
})
|
|
33
|
+
// 需要关闭时:instance.close()
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
宿主需提供 `<div id="medipaas-container" style="height: 600px"></div>`。
|
|
37
|
+
|
|
38
|
+
## Token 对接
|
|
39
|
+
|
|
40
|
+
第三方后端向 MediPaaS 发送以下请求。`encryptedData` 是按双方约定的 RSA 公钥加密并 Base64 编码后的用户信息,由后端生成;请勿在浏览器中放置服务端凭证。
|
|
41
|
+
|
|
42
|
+
```http
|
|
43
|
+
POST /api/auth/token/embed?clientId=your-client-id
|
|
44
|
+
Content-Type: application/json
|
|
45
|
+
|
|
46
|
+
{"encryptedData":"base64-rsa-encrypted-user-data"}
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
加密前的 JSON 至少包含 `{"clientId":"your-client-id","userId":20293}`。clientId 必须与请求一致;userId 是第三方用户 ID,对应目标应用启用用户的 `_app_user.relatedId`。
|
|
50
|
+
|
|
51
|
+
成功响应采用 Result 包装,token 位于 `data` 中:
|
|
52
|
+
|
|
53
|
+
```json
|
|
54
|
+
{
|
|
55
|
+
"code": 200,
|
|
56
|
+
"message": "Success",
|
|
57
|
+
"data": {
|
|
58
|
+
"token": "jwt-token",
|
|
59
|
+
"expireSeconds": 3600,
|
|
60
|
+
"runtimeAppId": 123,
|
|
61
|
+
"homePageId": 456,
|
|
62
|
+
"homePageCode": "order_detail"
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
上方 `getToken()` 表示宿主自有接口,示例假定它已检查 HTTP 与业务 code,并解包返回 `data`;如果宿主保留 Result 包装,应先读取 `response.data`。传给 SDK 的 appId 使用 `String(data.runtimeAppId)`,apiName 使用目标页面 API 名称。
|
|
68
|
+
|
|
69
|
+
## 参数契约
|
|
70
|
+
|
|
71
|
+
构造配置:`baseUrl` 必填,为完整前端地址;`timeout` 默认为 30000,当前尚未实现超时处理。
|
|
72
|
+
|
|
73
|
+
`embed()` 支持:
|
|
74
|
+
|
|
75
|
+
| 字段 | 说明 |
|
|
76
|
+
| --- | --- |
|
|
77
|
+
| `appId` | 必填字符串,使用 token 接口返回的 runtimeAppId |
|
|
78
|
+
| `apiName` | 推荐的页面 API 名称 |
|
|
79
|
+
| `pageCode` / `pageId` | 废弃但兼容,优先级 apiName → pageCode → pageId;三者至少一个 |
|
|
80
|
+
| `token` | 必填 embed token,SDK 不负责获取或刷新 |
|
|
81
|
+
| `params` | 可选业务参数普通对象,见下述规则 |
|
|
82
|
+
| `containerId` | 可选宿主 DOM ID;省略时创建遮罩弹窗 |
|
|
83
|
+
| `fullscreen` | 默认 false,仅控制默认弹窗尺寸 |
|
|
84
|
+
| `iframeAttrs` | iframe 属性字典,如 title、allow、sandbox;禁止 src/srcdoc(大小写均禁止) |
|
|
85
|
+
| `onReady` | iframe load 回调,不表示首次数据查询完成 |
|
|
86
|
+
| `onBridgeReady` | 消息桥握手完成回调;此后可调用实例 `send()` |
|
|
87
|
+
| `onClose` | 实例 close() 回调;closeAll()、遮罩点击不会触发 |
|
|
88
|
+
| `onError` | 参数、容器或 iframe 错误回调;参数错误同时拒绝 embed Promise |
|
|
89
|
+
| `onEvent` | 嵌入页业务事件回调,参数为 `{ name, payload }` |
|
|
90
|
+
|
|
91
|
+
`params` 的值允许字符串、有限数字、布尔值、null、undefined。字符串通过 URLSearchParams 编码,调用方传原始值,不预先编码。数字与布尔转为字符串;0、false、空字符串保留;null/undefined 属性忽略。对象、数组、函数、NaN、Infinity 均拒绝;params 本身不能为 null。
|
|
92
|
+
|
|
93
|
+
参数名称必须满足 `[A-Za-z_][A-Za-z0-9_]*`;token、__proto__、constructor、prototype 是保留名称。参数与 iframe 地址覆盖校验在创建 DOM 前完成,失败调用 onError 一次并拒绝 Promise。iframeAttrs 禁止 src/srcdoc 是对旧行为的收紧。
|
|
94
|
+
|
|
95
|
+
页面通过 `{{ urlParams.value.orderId }}` 读取。所有 SDK 参数在页面中均为字符串,布尔判断使用 `{{ urlParams.value.editable === 'true' }}`,ID 建议传字符串以保留前导零和大整数精度。URL 显式值优先于页面参数默认值,显式空字符串不会被默认值覆盖。目标页面需将参数绑定到数据源查询条件或初始化动作。
|
|
96
|
+
|
|
97
|
+
参数属于当前 iframe 地址,刷新可恢复,各实例互不共享;页间导航需显式传参。切换记录可重新 embed,或在握手完成后调用 `instance.send('params.update', { orderId: '000124' })` 更新目标页面已声明的 URL 参数;返回 `false` 表示通道尚未就绪。`instance.send('page.refresh')` 可重新加载当前页面。URL 中的业务参数不能赋予权限,服务端仍验证 token 与记录权限;不要通过 URL 传敏感内容或大量数据。
|
|
98
|
+
|
|
99
|
+
## 双向消息
|
|
100
|
+
|
|
101
|
+
页面设计器的“通知第三方宿主”动作可以在按钮点击、数据请求成功等事件中发送业务事件。配置稳定的事件名(如 `order.saved`)和可选事件数据表达式(如 `{{ { orderId: variables.orderId } }}`);第三方通过 `embed({ onEvent })` 接收 `{ name, payload }`。页面未嵌入或通道未握手时不会发出事件。
|
|
102
|
+
|
|
103
|
+
SDK 与嵌入页使用 `medipaas.embed` v1 协议。SDK 核对消息来源窗口、A-PaaS origin 和实例通道 ID;嵌入页只接受父窗口的握手,回复与发送时使用该窗口的精确 origin。握手用于隔离 iframe 消息,不替代 embed token 和服务端数据权限。不要在事件载荷中传 token 或敏感业务数据。
|
|
104
|
+
|
|
105
|
+
`onReady` 仍只表示 iframe `load`,不表示业务数据完成或消息桥就绪。`onBridgeReady` 表示可以调用 `send()`;提前调用会返回 `false`。页面初始化期间发给宿主的少量事件会在握手后发送。页面内保存成功通知应配置在数据请求成功事件中。消息仅送达当前打开的 iframe,不提供离线投递。
|
|
106
|
+
|
|
107
|
+
实例返回 iframe、token、expiresAt(当前固定 0)、send() 和 close()。SDK 还提供 closeAll() 与 getInstanceCount()。
|
|
108
|
+
|
|
109
|
+
## 构建与示例
|
|
110
|
+
|
|
111
|
+
在前端工作区执行 `pnpm build:sdk`,生成 ESM、CJS 和类型声明,不提供 UMD 全局变量版本。安装包后使用上述模块导入;原生 HTML 通过 `<script type="module">` 导入 ESM。
|
|
112
|
+
|
|
113
|
+
- [快速开始](docs/QUICKSTART.md)
|
|
114
|
+
- [文档索引](docs/README.md)
|
|
115
|
+
- [原生 HTML 示例](examples/basic-html/index.html):构建后以 HTTP 静态服务打开,填入前端地址、runtimeAppId、apiName、token 和参数。
|
|
116
|
+
|
|
117
|
+
测试:`pnpm --filter @obm/apaas-embed-sdk exec vitest run`。
|
|
118
|
+
|
|
119
|
+
在前端工作区运行 `pnpm --filter medipaas-frontend test:embed-params`,会先构建最新 SDK,再验证设计器生成代码与页面参数查询链路,避免读取旧 dist。
|
|
120
|
+
|
|
121
|
+
## 发布检查
|
|
122
|
+
|
|
123
|
+
在 SDK 目录执行 `pnpm run verify`,依次检查 lint、单元测试、构建、打包、临时项目安装、ESM/CJS 导入及类型声明。临时测试文件会自动清理。
|
|
124
|
+
|
|
125
|
+
`npm publish` 会通过 `prepublishOnly` 自动执行完整验证,检查失败时停止发布。发布前需确认使用 `obm` 账号发布到 `@obm` scope,且目标版本尚未发布。
|
|
126
|
+
|
|
127
|
+
```bash
|
|
128
|
+
pnpm run publish:check
|
|
129
|
+
pnpm run publish:release
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
第一条仅预演;第二条实际发布公开包,使用默认的 `latest` 标签。npm 版本发布后不能覆盖,后续发布需更新 `package.json` 版本号。
|
|
133
|
+
|
|
134
|
+
## 许可与反馈
|
|
135
|
+
|
|
136
|
+
本 SDK 使用 [MIT License](LICENSE)。仓库及问题反馈地址见 `package.json` 的 `repository`、`homepage` 与 `bugs` 字段;访问项目 GitLab 可能需要账号权限。
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"use strict";var g=Object.defineProperty;var D=Object.getOwnPropertyDescriptor;var $=Object.getOwnPropertyNames;var k=Object.prototype.hasOwnProperty;var K=(r,e,t)=>e in r?g(r,e,{enumerable:!0,configurable:!0,writable:!0,value:t}):r[e]=t;var L=(r,e)=>{for(var t in e)g(r,t,{get:e[t],enumerable:!0})},N=(r,e,t,n)=>{if(e&&typeof e=="object"||typeof e=="function")for(let s of $(e))!k.call(r,s)&&s!==t&&g(r,s,{get:()=>e[s],enumerable:!(n=D(e,s))||n.enumerable});return r};var T=r=>N(g({},"__esModule",{value:!0}),r);var l=(r,e,t)=>K(r,typeof e!="symbol"?e+"":e,t);var z={};L(z,{ApassEmbedSDK:()=>h});module.exports=T(z);var _=new Set(["token","__proto__","constructor","prototype"]);function x(r){if(r===null||typeof r!="object")return!1;let e=Object.getPrototypeOf(r);return e===null?!0:Object.getPrototypeOf(e)===null&&Object.prototype.hasOwnProperty.call(e,"constructor")&&typeof e.constructor=="function"&&Function.prototype.toString.call(e.constructor)===Function.prototype.toString.call(Object)}function O(r={}){if(!x(r))throw new Error("iframeAttrs must be a plain object");let e=Object.entries(r);for(let[t]of e){let n=t.trim().toLowerCase();if(n==="src"||n==="srcdoc")throw new Error(`iframeAttrs.${t} cannot override the SDK page address`);if(!t||/[\s"'>/=\u0000-\u001f]/.test(t))throw new Error(`Invalid iframe attribute name: ${t}`)}return Object.fromEntries(e)}function I(r,e){if(!r||r==="."||r==="..")throw new Error(`${e} must be a non-empty path segment other than . or ..`);return encodeURIComponent(r)}function S(r,e){if(!e.token)throw new Error("Token is required");let t=String(e.apiName||e.pageCode||e.pageId||"").trim();if(!t)throw new Error("Either apiName, pageCode (deprecated), or pageId (deprecated) is required");let n=new URL(`/apps/${I(e.appId,"appId")}/user/${I(t,"apiName")}`,r);n.searchParams.set("token",e.token);let s=e.params;if(s!==void 0){if(!x(s))throw new Error("params must be a plain object");for(let[i,c]of Object.entries(s)){if(!/^[A-Za-z_][A-Za-z0-9_]*$/.test(i)||_.has(i))throw new Error(`Invalid or reserved parameter name: ${i}`);if(c!=null){if(!["string","number","boolean"].includes(typeof c)||typeof c=="number"&&!Number.isFinite(c))throw new Error(`Parameter ${i} must be a string, finite number, or boolean`);n.searchParams.set(i,String(c))}}}return n.toString()}var h=class{constructor(e){l(this,"config");l(this,"instances",new Map);l(this,"cleanups",new Map);l(this,"instanceCounter",0);this.validateConfig(e),this.config={timeout:3e4,...e}}async embed(e){let{apiName:t,pageCode:n,pageId:s,token:i,containerId:c,onReady:A,onBridgeReady:R,onClose:U,onError:E,onEvent:j,iframeAttrs:P={},fullscreen:M=!1}=e;try{let d=S(this.config.baseUrl,e),u=O(P);n&&!t&&console.warn("[MediPaaS SDK] pageCode is deprecated. Please use apiName instead."),s&&!t&&!n&&console.warn("[MediPaaS SDK] pageId is deprecated. Please use apiName instead.");let p=this.createIframe(d,u),v=this.prepareContainer(c,M),m=`apass-${++this.instanceCounter}`,b=crypto.randomUUID(),w=new URL(d).origin,f=!1,C=o=>{if(o.source!==p.contentWindow||o.origin!==w)return;let a=o.data;!a||typeof a!="object"||a.namespace!=="medipaas.embed"||a.version!==1||a.channelId!==b||(a.type==="ready"&&!f&&(f=!0,R?.()),a.type==="event"&&f&&typeof a.name=="string"&&a.name&&j?.({name:a.name,payload:a.payload}))};window.addEventListener("message",C);let y={iframe:p,token:i,expiresAt:0,close:()=>{this.closeInstance(m),U?.()},send:(o,a)=>{if(!f||!this.instances.has(m)||o!=="page.refresh"&&o!=="params.update")return!1;try{return p.contentWindow?.postMessage({namespace:"medipaas.embed",version:1,channelId:b,type:"event",name:o,payload:a},w),!0}catch{return!1}}};if(!c){let o=v.parentElement;o.dataset.apassOverlay=m,o.addEventListener("click",a=>{a.target===o&&y.close()})}return p.onload=()=>{f=!1,p.contentWindow?.postMessage({namespace:"medipaas.embed",version:1,channelId:b,type:"hello"},w),A?.()},p.onerror=()=>{let o=new Error("Failed to load A-pass iframe");E?.(o)},v.appendChild(p),this.instances.set(m,y),this.cleanups.set(m,()=>window.removeEventListener("message",C)),y}catch(d){let u=d instanceof Error?d:new Error(String(d));throw E?.(u),u}}closeInstance(e){let t=this.instances.get(e);if(t){this.cleanups.get(e)?.(),this.cleanups.delete(e);let n=t.iframe.parentElement?.parentElement;t.iframe.remove(),n?.dataset.apassOverlay===e&&n.remove(),this.instances.delete(e)}}prepareContainer(e,t){if(e){let i=document.getElementById(e);if(!i)throw new Error(`Container with id "${e}" not found`);return i}let n=document.createElement("div");n.style.cssText=`
|
|
2
|
+
position: fixed;
|
|
3
|
+
top: 0;
|
|
4
|
+
left: 0;
|
|
5
|
+
width: 100%;
|
|
6
|
+
height: 100%;
|
|
7
|
+
z-index: 10000;
|
|
8
|
+
background: rgba(0, 0, 0, 0.5);
|
|
9
|
+
display: flex;
|
|
10
|
+
align-items: center;
|
|
11
|
+
justify-content: center;
|
|
12
|
+
`;let s=document.createElement("div");return t?s.style.cssText=`
|
|
13
|
+
width: 100%;
|
|
14
|
+
height: 100%;
|
|
15
|
+
`:s.style.cssText=`
|
|
16
|
+
width: 90%;
|
|
17
|
+
height: 90%;
|
|
18
|
+
max-width: 1200px;
|
|
19
|
+
max-height: 800px;
|
|
20
|
+
border-radius: 8px;
|
|
21
|
+
overflow: hidden;
|
|
22
|
+
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
|
23
|
+
`,n.appendChild(s),document.body.appendChild(n),s}createIframe(e,t){let n=document.createElement("iframe");return n.src=e,n.style.cssText=`
|
|
24
|
+
width: 100%;
|
|
25
|
+
height: 100%;
|
|
26
|
+
border: none;
|
|
27
|
+
`,Object.entries(t).forEach(([s,i])=>{n.setAttribute(s,i)}),n}validateConfig(e){if(!e.baseUrl)throw new Error("SDKConfig.baseUrl is required");try{new URL(e.baseUrl)}catch{throw new Error(`SDKConfig.baseUrl must be a valid URL: ${e.baseUrl}`)}}closeAll(){this.instances.forEach((e,t)=>{this.closeInstance(t)})}getInstanceCount(){return this.instances.size}};0&&(module.exports={ApassEmbedSDK});
|
|
28
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/core/embedUrl.ts","../src/core/sdk.ts"],"sourcesContent":["export { ApassEmbedSDK } from './core/sdk';\r\nexport type {\r\n SDKConfig,\r\n EmbedOptions,\r\n EmbedParamValue,\n EmbedBusinessEvent,\n ApassInstanceRef,\r\n} from './types';\r\n","import type { EmbedOptions } from '../types';\r\n\r\nconst reservedNames = new Set(['token', '__proto__', 'constructor', 'prototype']);\r\n\r\nfunction isPlainObject(value: unknown): value is Record<string, unknown> {\r\n if (value === null || typeof value !== 'object') return false;\r\n const prototype = Object.getPrototypeOf(value);\r\n if (prototype === null) return true;\r\n // Object.prototype differs between same-origin windows. Compare its native\r\n // constructor instead of requiring this window's exact prototype identity.\r\n return Object.getPrototypeOf(prototype) === null &&\r\n Object.prototype.hasOwnProperty.call(prototype, 'constructor') &&\r\n typeof prototype.constructor === 'function' &&\r\n Function.prototype.toString.call(prototype.constructor) === Function.prototype.toString.call(Object);\r\n}\r\n\r\n/** Snapshot once so parameter/attribute getters cannot later replace the address. */\r\nexport function prepareIframeAttributes(attrs: Record<string, string> = {}): Record<string, string> {\r\n if (!isPlainObject(attrs)) throw new Error('iframeAttrs must be a plain object');\r\n const entries = Object.entries(attrs);\r\n for (const [name] of entries) {\r\n const normalized = name.trim().toLowerCase();\r\n if (normalized === 'src' || normalized === 'srcdoc') {\r\n throw new Error(`iframeAttrs.${name} cannot override the SDK page address`);\r\n }\r\n // eslint-disable-next-line no-control-regex -- Intentionally reject ASCII control characters in attribute names.\r\n if (!name || /[\\s\"'>/=\\u0000-\\u001f]/.test(name)) {\r\n throw new Error(`Invalid iframe attribute name: ${name}`);\r\n }\r\n }\r\n return Object.fromEntries(entries);\r\n}\r\n\r\nfunction encodeSegment(value: string, name: string): string {\r\n if (!value || value === '.' || value === '..') {\r\n throw new Error(`${name} must be a non-empty path segment other than . or ..`);\r\n }\r\n return encodeURIComponent(value);\r\n}\r\n\r\n/** Build and validate the complete iframe address before any DOM is created. */\r\nexport function buildEmbedUrl(baseUrl: string, options: EmbedOptions): string {\r\n if (!options.token) throw new Error('Token is required');\r\n const apiName = String(options.apiName || options.pageCode || options.pageId || '').trim();\r\n if (!apiName) {\r\n throw new Error('Either apiName, pageCode (deprecated), or pageId (deprecated) is required');\r\n }\r\n const url = new URL(`/apps/${encodeSegment(options.appId, 'appId')}/user/${encodeSegment(apiName, 'apiName')}`, baseUrl);\r\n url.searchParams.set('token', options.token);\r\n const params = options.params;\r\n if (params !== undefined) {\r\n if (!isPlainObject(params)) {\r\n throw new Error('params must be a plain object');\r\n }\r\n for (const [name, value] of Object.entries(params)) {\r\n if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(name) || reservedNames.has(name)) {\r\n throw new Error(`Invalid or reserved parameter name: ${name}`);\r\n }\r\n if (value === null || value === undefined) continue;\r\n if (!['string', 'number', 'boolean'].includes(typeof value) ||\r\n (typeof value === 'number' && !Number.isFinite(value))) {\r\n throw new Error(`Parameter ${name} must be a string, finite number, or boolean`);\r\n }\r\n url.searchParams.set(name, String(value));\r\n }\r\n }\r\n return url.toString();\r\n}\r\n","import type {\r\n SDKConfig,\r\n EmbedOptions,\r\n ApassInstanceRef,\r\n} from '../types';\r\nimport { buildEmbedUrl, prepareIframeAttributes } from './embedUrl';\r\n\r\n/**\r\n * Main SDK class for embedding A-pass applications\r\n *\r\n * @example\r\n * ```ts\r\n * const sdk = new ApassEmbedSDK({\r\n * baseUrl: 'https://apass.example.com',\r\n * });\r\n *\r\n * // CRM 项目先调自己的后端获取 token\r\n * const tokenResponse = await fetch('/api/embed-token', {\r\n * method: 'POST',\r\n * body: JSON.stringify({\r\n * recordId: 416,\r\n * enterpriseId: 456,\r\n * }),\r\n * });\r\n * const { token } = await tokenResponse.json();\r\n *\r\n * // 然后把 token 传给 SDK\r\n * await sdk.embed({\r\n * appId: '180',\r\n * apiName: 'patient_list',\r\n * token: token,\r\n * containerId: 'my-container',\r\n * });\r\n * ```\r\n */\r\nexport class ApassEmbedSDK {\r\n private config: SDKConfig;\r\n private instances: Map<string, ApassInstanceRef> = new Map();\n private cleanups: Map<string, () => void> = new Map();\n private instanceCounter = 0;\r\n\r\n constructor(config: SDKConfig) {\r\n this.validateConfig(config);\r\n this.config = {\r\n timeout: 30000,\r\n ...config,\r\n };\r\n }\r\n\r\n /**\r\n * Embed an A-pass application in an iframe\r\n */\r\n async embed(options: EmbedOptions): Promise<ApassInstanceRef> {\r\n const {\r\n apiName,\r\n pageCode,\r\n pageId,\r\n token,\r\n containerId,\r\n onReady,\n onBridgeReady,\n onClose,\r\n onError,\n onEvent,\n iframeAttrs = {},\r\n fullscreen = false,\r\n } = options;\r\n\r\n try {\r\n const url = buildEmbedUrl(this.config.baseUrl, options);\r\n const attrs = prepareIframeAttributes(iframeAttrs);\r\n // Warn if using deprecated parameters\r\n if (pageCode && !apiName) {\r\n console.warn('[MediPaaS SDK] pageCode is deprecated. Please use apiName instead.');\r\n }\r\n if (pageId && !apiName && !pageCode) {\r\n console.warn('[MediPaaS SDK] pageId is deprecated. Please use apiName instead.');\r\n }\r\n\r\n // Prepare the detached iframe first: DOM attribute validation must finish\r\n // before a default overlay is attached to the host page.\r\n const iframe = this.createIframe(url, attrs);\r\n const container = this.prepareContainer(containerId, fullscreen);\r\n\r\n // Step 5: Setup lifecycle handlers\r\n const instanceId = `apass-${++this.instanceCounter}`;\n const channelId = crypto.randomUUID();\n const targetOrigin = new URL(url).origin;\n let ready = false;\n const receiveMessage = (event: MessageEvent) => {\n if (event.source !== iframe.contentWindow || event.origin !== targetOrigin) return;\n const message = event.data;\n if (!message || typeof message !== 'object' || message.namespace !== 'medipaas.embed'\n || message.version !== 1 || message.channelId !== channelId) return;\n if (message.type === 'ready' && !ready) {\n ready = true;\n onBridgeReady?.();\n }\n if (message.type === 'event' && ready && typeof message.name === 'string' && message.name) {\n onEvent?.({ name: message.name, payload: message.payload });\n }\n };\n window.addEventListener('message', receiveMessage);\n\n const instance: ApassInstanceRef = {\n iframe,\r\n token,\r\n expiresAt: 0, // Token expiration not tracked in this mode\r\n close: () => {\n this.closeInstance(instanceId);\n onClose?.();\n },\n send: (name, payload) => {\n if (!ready || !this.instances.has(instanceId)) return false;\n if (name !== 'page.refresh' && name !== 'params.update') return false;\n try {\n iframe.contentWindow?.postMessage({ namespace: 'medipaas.embed', version: 1,\n channelId, type: 'event', name, payload }, targetOrigin);\n return true;\n } catch {\n return false;\n }\n },\n };\n\n if (!containerId) {\n const overlay = container.parentElement!;\n overlay.dataset.apassOverlay = instanceId;\n overlay.addEventListener('click', (event) => {\n if (event.target === overlay) instance.close();\n });\n }\n\r\n // Step 6: Setup ready handler before attaching the iframe\n iframe.onload = () => {\n ready = false;\n iframe.contentWindow?.postMessage({ namespace: 'medipaas.embed', version: 1,\n channelId, type: 'hello' }, targetOrigin);\n onReady?.();\n };\r\n\r\n // Step 8: Setup error handler\r\n iframe.onerror = () => {\n const error = new Error('Failed to load A-pass iframe');\r\n onError?.(error);\r\n };\n\n container.appendChild(iframe);\n\r\n // Store instance reference\r\n this.instances.set(instanceId, instance);\n this.cleanups.set(instanceId, () => window.removeEventListener('message', receiveMessage));\n\r\n return instance;\r\n } catch (error) {\r\n const err =\r\n error instanceof Error ? error : new Error(String(error));\r\n onError?.(err);\r\n throw err;\r\n }\r\n }\r\n\r\n /**\r\n * Close and cleanup an instance\r\n */\r\n private closeInstance(instanceId: string): void {\n const instance = this.instances.get(instanceId);\r\n if (instance) {\n this.cleanups.get(instanceId)?.();\n this.cleanups.delete(instanceId);\n // Remove iframe from DOM\n const overlay = instance.iframe.parentElement?.parentElement;\n instance.iframe.remove();\n if (overlay?.dataset.apassOverlay === instanceId) overlay.remove();\n // Remove from tracking\r\n this.instances.delete(instanceId);\r\n }\r\n }\n\r\n\r\n /**\r\n * Prepare container for iframe\r\n */\r\n private prepareContainer(\r\n containerId: string | undefined,\r\n fullscreen: boolean,\r\n ): HTMLElement {\r\n if (containerId) {\r\n const container = document.getElementById(containerId);\r\n if (!container) {\r\n throw new Error(`Container with id \"${containerId}\" not found`);\r\n }\r\n return container;\r\n }\r\n\r\n // Create default container\r\n const container = document.createElement('div');\r\n container.style.cssText = `\r\n position: fixed;\r\n top: 0;\r\n left: 0;\r\n width: 100%;\r\n height: 100%;\r\n z-index: 10000;\r\n background: rgba(0, 0, 0, 0.5);\r\n display: flex;\r\n align-items: center;\r\n justify-content: center;\r\n `;\r\n\r\n const wrapper = document.createElement('div');\r\n if (fullscreen) {\r\n wrapper.style.cssText = `\r\n width: 100%;\r\n height: 100%;\r\n `;\r\n } else {\r\n wrapper.style.cssText = `\r\n width: 90%;\r\n height: 90%;\r\n max-width: 1200px;\r\n max-height: 800px;\r\n border-radius: 8px;\r\n overflow: hidden;\r\n box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);\r\n `;\r\n }\r\n\r\n container.appendChild(wrapper);\r\n document.body.appendChild(container);\r\n\r\n return wrapper;\n }\r\n\r\n /**\r\n * Create iframe element\r\n */\r\n private createIframe(\r\n url: string,\r\n attrs: Record<string, string>,\r\n ): HTMLIFrameElement {\r\n const iframe = document.createElement('iframe');\r\n\r\n // Build A-pass URL with token parameter\r\n iframe.src = url;\r\n iframe.style.cssText = `\r\n width: 100%;\r\n height: 100%;\r\n border: none;\r\n `;\r\n\r\n // Apply custom attributes\r\n Object.entries(attrs).forEach(([key, value]) => {\r\n iframe.setAttribute(key, value);\r\n });\r\n\r\n return iframe;\r\n }\r\n\r\n /**\r\n * Validate SDK configuration\r\n */\r\n private validateConfig(config: SDKConfig): void {\r\n if (!config.baseUrl) {\r\n throw new Error('SDKConfig.baseUrl is required');\r\n }\r\n\r\n try {\r\n new URL(config.baseUrl);\r\n } catch {\r\n throw new Error(\r\n `SDKConfig.baseUrl must be a valid URL: ${config.baseUrl}`,\r\n );\r\n }\r\n }\r\n\r\n /**\r\n * Close all instances\r\n */\r\n closeAll(): void {\r\n this.instances.forEach((_, instanceId) => {\r\n this.closeInstance(instanceId);\r\n });\r\n }\r\n\r\n /**\r\n * Get active instances count\r\n */\r\n getInstanceCount(): number {\r\n return this.instances.size;\r\n }\r\n}\r\n"],"mappings":"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","names":["src_exports","__export","ApassEmbedSDK","__toCommonJS","reservedNames","isPlainObject","value","prototype","prepareIframeAttributes","attrs","entries","name","normalized","encodeSegment","buildEmbedUrl","baseUrl","options","apiName","url","params","ApassEmbedSDK","config","__publicField","options","apiName","pageCode","pageId","token","containerId","onReady","onBridgeReady","onClose","onError","onEvent","iframeAttrs","fullscreen","url","buildEmbedUrl","attrs","prepareIframeAttributes","iframe","container","instanceId","channelId","targetOrigin","ready","receiveMessage","event","message","instance","name","payload","overlay","error","err","wrapper","key","value","_"]}
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SDK configuration options
|
|
3
|
+
*/
|
|
4
|
+
interface SDKConfig {
|
|
5
|
+
/** Base URL of the A-pass server (e.g., https://apass.example.com) */
|
|
6
|
+
baseUrl: string;
|
|
7
|
+
/** Optional timeout for operations in milliseconds (default: 30000) */
|
|
8
|
+
timeout?: number;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Options for embedding an A-pass application
|
|
12
|
+
*/
|
|
13
|
+
type EmbedParamValue = string | number | boolean | null | undefined;
|
|
14
|
+
interface EmbedBusinessEvent {
|
|
15
|
+
name: string;
|
|
16
|
+
payload: unknown;
|
|
17
|
+
}
|
|
18
|
+
interface EmbedOptions {
|
|
19
|
+
/** Application ID in A-pass */
|
|
20
|
+
appId: string;
|
|
21
|
+
/** Page API name (recommended) */
|
|
22
|
+
apiName?: string;
|
|
23
|
+
/** @deprecated Use apiName instead. Kept for backward compatibility. */
|
|
24
|
+
pageCode?: string;
|
|
25
|
+
/** @deprecated Legacy runtime page ID. Use apiName instead. */
|
|
26
|
+
pageId?: string;
|
|
27
|
+
/** Embed token (from CRM backend) */
|
|
28
|
+
token: string;
|
|
29
|
+
/** Initial business parameters. Scalars become URL strings; null/undefined are omitted. */
|
|
30
|
+
params?: Record<string, EmbedParamValue>;
|
|
31
|
+
/** Optional target container ID. If not provided, creates a modal-like div */
|
|
32
|
+
containerId?: string;
|
|
33
|
+
/** Optional callback when iframe is loaded */
|
|
34
|
+
onReady?: () => void;
|
|
35
|
+
/** Called after the embedded page acknowledges the message channel. */
|
|
36
|
+
onBridgeReady?: () => void;
|
|
37
|
+
/** Optional callback when iframe is closed/destroyed */
|
|
38
|
+
onClose?: () => void;
|
|
39
|
+
/** Optional callback on error */
|
|
40
|
+
onError?: (error: Error) => void;
|
|
41
|
+
/** Called for business events emitted by the embedded page. */
|
|
42
|
+
onEvent?: (event: EmbedBusinessEvent) => void;
|
|
43
|
+
/** Optional custom iframe attributes. src and srcdoc are prohibited. */
|
|
44
|
+
iframeAttrs?: Record<string, string>;
|
|
45
|
+
/** Whether to fullscreen the iframe (default: false) */
|
|
46
|
+
fullscreen?: boolean;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Reference to an opened A-pass application instance
|
|
50
|
+
*/
|
|
51
|
+
interface ApassInstanceRef {
|
|
52
|
+
/** The iframe element */
|
|
53
|
+
iframe: HTMLIFrameElement;
|
|
54
|
+
/** Close/destroy the instance */
|
|
55
|
+
close: () => void;
|
|
56
|
+
/** Send a supported event to the running page. Returns false until the bridge is ready. */
|
|
57
|
+
send: (name: 'page.refresh' | 'params.update', payload?: unknown) => boolean;
|
|
58
|
+
/** Token expiration time */
|
|
59
|
+
expiresAt: number;
|
|
60
|
+
/** Token value */
|
|
61
|
+
token: string;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Main SDK class for embedding A-pass applications
|
|
66
|
+
*
|
|
67
|
+
* @example
|
|
68
|
+
* ```ts
|
|
69
|
+
* const sdk = new ApassEmbedSDK({
|
|
70
|
+
* baseUrl: 'https://apass.example.com',
|
|
71
|
+
* });
|
|
72
|
+
*
|
|
73
|
+
* // CRM 项目先调自己的后端获取 token
|
|
74
|
+
* const tokenResponse = await fetch('/api/embed-token', {
|
|
75
|
+
* method: 'POST',
|
|
76
|
+
* body: JSON.stringify({
|
|
77
|
+
* recordId: 416,
|
|
78
|
+
* enterpriseId: 456,
|
|
79
|
+
* }),
|
|
80
|
+
* });
|
|
81
|
+
* const { token } = await tokenResponse.json();
|
|
82
|
+
*
|
|
83
|
+
* // 然后把 token 传给 SDK
|
|
84
|
+
* await sdk.embed({
|
|
85
|
+
* appId: '180',
|
|
86
|
+
* apiName: 'patient_list',
|
|
87
|
+
* token: token,
|
|
88
|
+
* containerId: 'my-container',
|
|
89
|
+
* });
|
|
90
|
+
* ```
|
|
91
|
+
*/
|
|
92
|
+
declare class ApassEmbedSDK {
|
|
93
|
+
private config;
|
|
94
|
+
private instances;
|
|
95
|
+
private cleanups;
|
|
96
|
+
private instanceCounter;
|
|
97
|
+
constructor(config: SDKConfig);
|
|
98
|
+
/**
|
|
99
|
+
* Embed an A-pass application in an iframe
|
|
100
|
+
*/
|
|
101
|
+
embed(options: EmbedOptions): Promise<ApassInstanceRef>;
|
|
102
|
+
/**
|
|
103
|
+
* Close and cleanup an instance
|
|
104
|
+
*/
|
|
105
|
+
private closeInstance;
|
|
106
|
+
/**
|
|
107
|
+
* Prepare container for iframe
|
|
108
|
+
*/
|
|
109
|
+
private prepareContainer;
|
|
110
|
+
/**
|
|
111
|
+
* Create iframe element
|
|
112
|
+
*/
|
|
113
|
+
private createIframe;
|
|
114
|
+
/**
|
|
115
|
+
* Validate SDK configuration
|
|
116
|
+
*/
|
|
117
|
+
private validateConfig;
|
|
118
|
+
/**
|
|
119
|
+
* Close all instances
|
|
120
|
+
*/
|
|
121
|
+
closeAll(): void;
|
|
122
|
+
/**
|
|
123
|
+
* Get active instances count
|
|
124
|
+
*/
|
|
125
|
+
getInstanceCount(): number;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export { ApassEmbedSDK, type ApassInstanceRef, type EmbedBusinessEvent, type EmbedOptions, type EmbedParamValue, type SDKConfig };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SDK configuration options
|
|
3
|
+
*/
|
|
4
|
+
interface SDKConfig {
|
|
5
|
+
/** Base URL of the A-pass server (e.g., https://apass.example.com) */
|
|
6
|
+
baseUrl: string;
|
|
7
|
+
/** Optional timeout for operations in milliseconds (default: 30000) */
|
|
8
|
+
timeout?: number;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Options for embedding an A-pass application
|
|
12
|
+
*/
|
|
13
|
+
type EmbedParamValue = string | number | boolean | null | undefined;
|
|
14
|
+
interface EmbedBusinessEvent {
|
|
15
|
+
name: string;
|
|
16
|
+
payload: unknown;
|
|
17
|
+
}
|
|
18
|
+
interface EmbedOptions {
|
|
19
|
+
/** Application ID in A-pass */
|
|
20
|
+
appId: string;
|
|
21
|
+
/** Page API name (recommended) */
|
|
22
|
+
apiName?: string;
|
|
23
|
+
/** @deprecated Use apiName instead. Kept for backward compatibility. */
|
|
24
|
+
pageCode?: string;
|
|
25
|
+
/** @deprecated Legacy runtime page ID. Use apiName instead. */
|
|
26
|
+
pageId?: string;
|
|
27
|
+
/** Embed token (from CRM backend) */
|
|
28
|
+
token: string;
|
|
29
|
+
/** Initial business parameters. Scalars become URL strings; null/undefined are omitted. */
|
|
30
|
+
params?: Record<string, EmbedParamValue>;
|
|
31
|
+
/** Optional target container ID. If not provided, creates a modal-like div */
|
|
32
|
+
containerId?: string;
|
|
33
|
+
/** Optional callback when iframe is loaded */
|
|
34
|
+
onReady?: () => void;
|
|
35
|
+
/** Called after the embedded page acknowledges the message channel. */
|
|
36
|
+
onBridgeReady?: () => void;
|
|
37
|
+
/** Optional callback when iframe is closed/destroyed */
|
|
38
|
+
onClose?: () => void;
|
|
39
|
+
/** Optional callback on error */
|
|
40
|
+
onError?: (error: Error) => void;
|
|
41
|
+
/** Called for business events emitted by the embedded page. */
|
|
42
|
+
onEvent?: (event: EmbedBusinessEvent) => void;
|
|
43
|
+
/** Optional custom iframe attributes. src and srcdoc are prohibited. */
|
|
44
|
+
iframeAttrs?: Record<string, string>;
|
|
45
|
+
/** Whether to fullscreen the iframe (default: false) */
|
|
46
|
+
fullscreen?: boolean;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Reference to an opened A-pass application instance
|
|
50
|
+
*/
|
|
51
|
+
interface ApassInstanceRef {
|
|
52
|
+
/** The iframe element */
|
|
53
|
+
iframe: HTMLIFrameElement;
|
|
54
|
+
/** Close/destroy the instance */
|
|
55
|
+
close: () => void;
|
|
56
|
+
/** Send a supported event to the running page. Returns false until the bridge is ready. */
|
|
57
|
+
send: (name: 'page.refresh' | 'params.update', payload?: unknown) => boolean;
|
|
58
|
+
/** Token expiration time */
|
|
59
|
+
expiresAt: number;
|
|
60
|
+
/** Token value */
|
|
61
|
+
token: string;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Main SDK class for embedding A-pass applications
|
|
66
|
+
*
|
|
67
|
+
* @example
|
|
68
|
+
* ```ts
|
|
69
|
+
* const sdk = new ApassEmbedSDK({
|
|
70
|
+
* baseUrl: 'https://apass.example.com',
|
|
71
|
+
* });
|
|
72
|
+
*
|
|
73
|
+
* // CRM 项目先调自己的后端获取 token
|
|
74
|
+
* const tokenResponse = await fetch('/api/embed-token', {
|
|
75
|
+
* method: 'POST',
|
|
76
|
+
* body: JSON.stringify({
|
|
77
|
+
* recordId: 416,
|
|
78
|
+
* enterpriseId: 456,
|
|
79
|
+
* }),
|
|
80
|
+
* });
|
|
81
|
+
* const { token } = await tokenResponse.json();
|
|
82
|
+
*
|
|
83
|
+
* // 然后把 token 传给 SDK
|
|
84
|
+
* await sdk.embed({
|
|
85
|
+
* appId: '180',
|
|
86
|
+
* apiName: 'patient_list',
|
|
87
|
+
* token: token,
|
|
88
|
+
* containerId: 'my-container',
|
|
89
|
+
* });
|
|
90
|
+
* ```
|
|
91
|
+
*/
|
|
92
|
+
declare class ApassEmbedSDK {
|
|
93
|
+
private config;
|
|
94
|
+
private instances;
|
|
95
|
+
private cleanups;
|
|
96
|
+
private instanceCounter;
|
|
97
|
+
constructor(config: SDKConfig);
|
|
98
|
+
/**
|
|
99
|
+
* Embed an A-pass application in an iframe
|
|
100
|
+
*/
|
|
101
|
+
embed(options: EmbedOptions): Promise<ApassInstanceRef>;
|
|
102
|
+
/**
|
|
103
|
+
* Close and cleanup an instance
|
|
104
|
+
*/
|
|
105
|
+
private closeInstance;
|
|
106
|
+
/**
|
|
107
|
+
* Prepare container for iframe
|
|
108
|
+
*/
|
|
109
|
+
private prepareContainer;
|
|
110
|
+
/**
|
|
111
|
+
* Create iframe element
|
|
112
|
+
*/
|
|
113
|
+
private createIframe;
|
|
114
|
+
/**
|
|
115
|
+
* Validate SDK configuration
|
|
116
|
+
*/
|
|
117
|
+
private validateConfig;
|
|
118
|
+
/**
|
|
119
|
+
* Close all instances
|
|
120
|
+
*/
|
|
121
|
+
closeAll(): void;
|
|
122
|
+
/**
|
|
123
|
+
* Get active instances count
|
|
124
|
+
*/
|
|
125
|
+
getInstanceCount(): number;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export { ApassEmbedSDK, type ApassInstanceRef, type EmbedBusinessEvent, type EmbedOptions, type EmbedParamValue, type SDKConfig };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
var M=Object.defineProperty;var D=(n,e,t)=>e in n?M(n,e,{enumerable:!0,configurable:!0,writable:!0,value:t}):n[e]=t;var l=(n,e,t)=>D(n,typeof e!="symbol"?e+"":e,t);var $=new Set(["token","__proto__","constructor","prototype"]);function I(n){if(n===null||typeof n!="object")return!1;let e=Object.getPrototypeOf(n);return e===null?!0:Object.getPrototypeOf(e)===null&&Object.prototype.hasOwnProperty.call(e,"constructor")&&typeof e.constructor=="function"&&Function.prototype.toString.call(e.constructor)===Function.prototype.toString.call(Object)}function x(n={}){if(!I(n))throw new Error("iframeAttrs must be a plain object");let e=Object.entries(n);for(let[t]of e){let r=t.trim().toLowerCase();if(r==="src"||r==="srcdoc")throw new Error(`iframeAttrs.${t} cannot override the SDK page address`);if(!t||/[\s"'>/=\u0000-\u001f]/.test(t))throw new Error(`Invalid iframe attribute name: ${t}`)}return Object.fromEntries(e)}function C(n,e){if(!n||n==="."||n==="..")throw new Error(`${e} must be a non-empty path segment other than . or ..`);return encodeURIComponent(n)}function O(n,e){if(!e.token)throw new Error("Token is required");let t=String(e.apiName||e.pageCode||e.pageId||"").trim();if(!t)throw new Error("Either apiName, pageCode (deprecated), or pageId (deprecated) is required");let r=new URL(`/apps/${C(e.appId,"appId")}/user/${C(t,"apiName")}`,n);r.searchParams.set("token",e.token);let o=e.params;if(o!==void 0){if(!I(o))throw new Error("params must be a plain object");for(let[i,c]of Object.entries(o)){if(!/^[A-Za-z_][A-Za-z0-9_]*$/.test(i)||$.has(i))throw new Error(`Invalid or reserved parameter name: ${i}`);if(c!=null){if(!["string","number","boolean"].includes(typeof c)||typeof c=="number"&&!Number.isFinite(c))throw new Error(`Parameter ${i} must be a string, finite number, or boolean`);r.searchParams.set(i,String(c))}}}return r.toString()}var w=class{constructor(e){l(this,"config");l(this,"instances",new Map);l(this,"cleanups",new Map);l(this,"instanceCounter",0);this.validateConfig(e),this.config={timeout:3e4,...e}}async embed(e){let{apiName:t,pageCode:r,pageId:o,token:i,containerId:c,onReady:S,onBridgeReady:A,onClose:R,onError:y,onEvent:U,iframeAttrs:j={},fullscreen:P=!1}=e;try{let d=O(this.config.baseUrl,e),u=x(j);r&&!t&&console.warn("[MediPaaS SDK] pageCode is deprecated. Please use apiName instead."),o&&!t&&!r&&console.warn("[MediPaaS SDK] pageId is deprecated. Please use apiName instead.");let p=this.createIframe(d,u),E=this.prepareContainer(c,P),m=`apass-${++this.instanceCounter}`,g=crypto.randomUUID(),h=new URL(d).origin,f=!1,v=a=>{if(a.source!==p.contentWindow||a.origin!==h)return;let s=a.data;!s||typeof s!="object"||s.namespace!=="medipaas.embed"||s.version!==1||s.channelId!==g||(s.type==="ready"&&!f&&(f=!0,A?.()),s.type==="event"&&f&&typeof s.name=="string"&&s.name&&U?.({name:s.name,payload:s.payload}))};window.addEventListener("message",v);let b={iframe:p,token:i,expiresAt:0,close:()=>{this.closeInstance(m),R?.()},send:(a,s)=>{if(!f||!this.instances.has(m)||a!=="page.refresh"&&a!=="params.update")return!1;try{return p.contentWindow?.postMessage({namespace:"medipaas.embed",version:1,channelId:g,type:"event",name:a,payload:s},h),!0}catch{return!1}}};if(!c){let a=E.parentElement;a.dataset.apassOverlay=m,a.addEventListener("click",s=>{s.target===a&&b.close()})}return p.onload=()=>{f=!1,p.contentWindow?.postMessage({namespace:"medipaas.embed",version:1,channelId:g,type:"hello"},h),S?.()},p.onerror=()=>{let a=new Error("Failed to load A-pass iframe");y?.(a)},E.appendChild(p),this.instances.set(m,b),this.cleanups.set(m,()=>window.removeEventListener("message",v)),b}catch(d){let u=d instanceof Error?d:new Error(String(d));throw y?.(u),u}}closeInstance(e){let t=this.instances.get(e);if(t){this.cleanups.get(e)?.(),this.cleanups.delete(e);let r=t.iframe.parentElement?.parentElement;t.iframe.remove(),r?.dataset.apassOverlay===e&&r.remove(),this.instances.delete(e)}}prepareContainer(e,t){if(e){let i=document.getElementById(e);if(!i)throw new Error(`Container with id "${e}" not found`);return i}let r=document.createElement("div");r.style.cssText=`
|
|
2
|
+
position: fixed;
|
|
3
|
+
top: 0;
|
|
4
|
+
left: 0;
|
|
5
|
+
width: 100%;
|
|
6
|
+
height: 100%;
|
|
7
|
+
z-index: 10000;
|
|
8
|
+
background: rgba(0, 0, 0, 0.5);
|
|
9
|
+
display: flex;
|
|
10
|
+
align-items: center;
|
|
11
|
+
justify-content: center;
|
|
12
|
+
`;let o=document.createElement("div");return t?o.style.cssText=`
|
|
13
|
+
width: 100%;
|
|
14
|
+
height: 100%;
|
|
15
|
+
`:o.style.cssText=`
|
|
16
|
+
width: 90%;
|
|
17
|
+
height: 90%;
|
|
18
|
+
max-width: 1200px;
|
|
19
|
+
max-height: 800px;
|
|
20
|
+
border-radius: 8px;
|
|
21
|
+
overflow: hidden;
|
|
22
|
+
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
|
23
|
+
`,r.appendChild(o),document.body.appendChild(r),o}createIframe(e,t){let r=document.createElement("iframe");return r.src=e,r.style.cssText=`
|
|
24
|
+
width: 100%;
|
|
25
|
+
height: 100%;
|
|
26
|
+
border: none;
|
|
27
|
+
`,Object.entries(t).forEach(([o,i])=>{r.setAttribute(o,i)}),r}validateConfig(e){if(!e.baseUrl)throw new Error("SDKConfig.baseUrl is required");try{new URL(e.baseUrl)}catch{throw new Error(`SDKConfig.baseUrl must be a valid URL: ${e.baseUrl}`)}}closeAll(){this.instances.forEach((e,t)=>{this.closeInstance(t)})}getInstanceCount(){return this.instances.size}};export{w as ApassEmbedSDK};
|
|
28
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/core/embedUrl.ts","../src/core/sdk.ts"],"sourcesContent":["import type { EmbedOptions } from '../types';\r\n\r\nconst reservedNames = new Set(['token', '__proto__', 'constructor', 'prototype']);\r\n\r\nfunction isPlainObject(value: unknown): value is Record<string, unknown> {\r\n if (value === null || typeof value !== 'object') return false;\r\n const prototype = Object.getPrototypeOf(value);\r\n if (prototype === null) return true;\r\n // Object.prototype differs between same-origin windows. Compare its native\r\n // constructor instead of requiring this window's exact prototype identity.\r\n return Object.getPrototypeOf(prototype) === null &&\r\n Object.prototype.hasOwnProperty.call(prototype, 'constructor') &&\r\n typeof prototype.constructor === 'function' &&\r\n Function.prototype.toString.call(prototype.constructor) === Function.prototype.toString.call(Object);\r\n}\r\n\r\n/** Snapshot once so parameter/attribute getters cannot later replace the address. */\r\nexport function prepareIframeAttributes(attrs: Record<string, string> = {}): Record<string, string> {\r\n if (!isPlainObject(attrs)) throw new Error('iframeAttrs must be a plain object');\r\n const entries = Object.entries(attrs);\r\n for (const [name] of entries) {\r\n const normalized = name.trim().toLowerCase();\r\n if (normalized === 'src' || normalized === 'srcdoc') {\r\n throw new Error(`iframeAttrs.${name} cannot override the SDK page address`);\r\n }\r\n // eslint-disable-next-line no-control-regex -- Intentionally reject ASCII control characters in attribute names.\r\n if (!name || /[\\s\"'>/=\\u0000-\\u001f]/.test(name)) {\r\n throw new Error(`Invalid iframe attribute name: ${name}`);\r\n }\r\n }\r\n return Object.fromEntries(entries);\r\n}\r\n\r\nfunction encodeSegment(value: string, name: string): string {\r\n if (!value || value === '.' || value === '..') {\r\n throw new Error(`${name} must be a non-empty path segment other than . or ..`);\r\n }\r\n return encodeURIComponent(value);\r\n}\r\n\r\n/** Build and validate the complete iframe address before any DOM is created. */\r\nexport function buildEmbedUrl(baseUrl: string, options: EmbedOptions): string {\r\n if (!options.token) throw new Error('Token is required');\r\n const apiName = String(options.apiName || options.pageCode || options.pageId || '').trim();\r\n if (!apiName) {\r\n throw new Error('Either apiName, pageCode (deprecated), or pageId (deprecated) is required');\r\n }\r\n const url = new URL(`/apps/${encodeSegment(options.appId, 'appId')}/user/${encodeSegment(apiName, 'apiName')}`, baseUrl);\r\n url.searchParams.set('token', options.token);\r\n const params = options.params;\r\n if (params !== undefined) {\r\n if (!isPlainObject(params)) {\r\n throw new Error('params must be a plain object');\r\n }\r\n for (const [name, value] of Object.entries(params)) {\r\n if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(name) || reservedNames.has(name)) {\r\n throw new Error(`Invalid or reserved parameter name: ${name}`);\r\n }\r\n if (value === null || value === undefined) continue;\r\n if (!['string', 'number', 'boolean'].includes(typeof value) ||\r\n (typeof value === 'number' && !Number.isFinite(value))) {\r\n throw new Error(`Parameter ${name} must be a string, finite number, or boolean`);\r\n }\r\n url.searchParams.set(name, String(value));\r\n }\r\n }\r\n return url.toString();\r\n}\r\n","import type {\r\n SDKConfig,\r\n EmbedOptions,\r\n ApassInstanceRef,\r\n} from '../types';\r\nimport { buildEmbedUrl, prepareIframeAttributes } from './embedUrl';\r\n\r\n/**\r\n * Main SDK class for embedding A-pass applications\r\n *\r\n * @example\r\n * ```ts\r\n * const sdk = new ApassEmbedSDK({\r\n * baseUrl: 'https://apass.example.com',\r\n * });\r\n *\r\n * // CRM 项目先调自己的后端获取 token\r\n * const tokenResponse = await fetch('/api/embed-token', {\r\n * method: 'POST',\r\n * body: JSON.stringify({\r\n * recordId: 416,\r\n * enterpriseId: 456,\r\n * }),\r\n * });\r\n * const { token } = await tokenResponse.json();\r\n *\r\n * // 然后把 token 传给 SDK\r\n * await sdk.embed({\r\n * appId: '180',\r\n * apiName: 'patient_list',\r\n * token: token,\r\n * containerId: 'my-container',\r\n * });\r\n * ```\r\n */\r\nexport class ApassEmbedSDK {\r\n private config: SDKConfig;\r\n private instances: Map<string, ApassInstanceRef> = new Map();\n private cleanups: Map<string, () => void> = new Map();\n private instanceCounter = 0;\r\n\r\n constructor(config: SDKConfig) {\r\n this.validateConfig(config);\r\n this.config = {\r\n timeout: 30000,\r\n ...config,\r\n };\r\n }\r\n\r\n /**\r\n * Embed an A-pass application in an iframe\r\n */\r\n async embed(options: EmbedOptions): Promise<ApassInstanceRef> {\r\n const {\r\n apiName,\r\n pageCode,\r\n pageId,\r\n token,\r\n containerId,\r\n onReady,\n onBridgeReady,\n onClose,\r\n onError,\n onEvent,\n iframeAttrs = {},\r\n fullscreen = false,\r\n } = options;\r\n\r\n try {\r\n const url = buildEmbedUrl(this.config.baseUrl, options);\r\n const attrs = prepareIframeAttributes(iframeAttrs);\r\n // Warn if using deprecated parameters\r\n if (pageCode && !apiName) {\r\n console.warn('[MediPaaS SDK] pageCode is deprecated. Please use apiName instead.');\r\n }\r\n if (pageId && !apiName && !pageCode) {\r\n console.warn('[MediPaaS SDK] pageId is deprecated. Please use apiName instead.');\r\n }\r\n\r\n // Prepare the detached iframe first: DOM attribute validation must finish\r\n // before a default overlay is attached to the host page.\r\n const iframe = this.createIframe(url, attrs);\r\n const container = this.prepareContainer(containerId, fullscreen);\r\n\r\n // Step 5: Setup lifecycle handlers\r\n const instanceId = `apass-${++this.instanceCounter}`;\n const channelId = crypto.randomUUID();\n const targetOrigin = new URL(url).origin;\n let ready = false;\n const receiveMessage = (event: MessageEvent) => {\n if (event.source !== iframe.contentWindow || event.origin !== targetOrigin) return;\n const message = event.data;\n if (!message || typeof message !== 'object' || message.namespace !== 'medipaas.embed'\n || message.version !== 1 || message.channelId !== channelId) return;\n if (message.type === 'ready' && !ready) {\n ready = true;\n onBridgeReady?.();\n }\n if (message.type === 'event' && ready && typeof message.name === 'string' && message.name) {\n onEvent?.({ name: message.name, payload: message.payload });\n }\n };\n window.addEventListener('message', receiveMessage);\n\n const instance: ApassInstanceRef = {\n iframe,\r\n token,\r\n expiresAt: 0, // Token expiration not tracked in this mode\r\n close: () => {\n this.closeInstance(instanceId);\n onClose?.();\n },\n send: (name, payload) => {\n if (!ready || !this.instances.has(instanceId)) return false;\n if (name !== 'page.refresh' && name !== 'params.update') return false;\n try {\n iframe.contentWindow?.postMessage({ namespace: 'medipaas.embed', version: 1,\n channelId, type: 'event', name, payload }, targetOrigin);\n return true;\n } catch {\n return false;\n }\n },\n };\n\n if (!containerId) {\n const overlay = container.parentElement!;\n overlay.dataset.apassOverlay = instanceId;\n overlay.addEventListener('click', (event) => {\n if (event.target === overlay) instance.close();\n });\n }\n\r\n // Step 6: Setup ready handler before attaching the iframe\n iframe.onload = () => {\n ready = false;\n iframe.contentWindow?.postMessage({ namespace: 'medipaas.embed', version: 1,\n channelId, type: 'hello' }, targetOrigin);\n onReady?.();\n };\r\n\r\n // Step 8: Setup error handler\r\n iframe.onerror = () => {\n const error = new Error('Failed to load A-pass iframe');\r\n onError?.(error);\r\n };\n\n container.appendChild(iframe);\n\r\n // Store instance reference\r\n this.instances.set(instanceId, instance);\n this.cleanups.set(instanceId, () => window.removeEventListener('message', receiveMessage));\n\r\n return instance;\r\n } catch (error) {\r\n const err =\r\n error instanceof Error ? error : new Error(String(error));\r\n onError?.(err);\r\n throw err;\r\n }\r\n }\r\n\r\n /**\r\n * Close and cleanup an instance\r\n */\r\n private closeInstance(instanceId: string): void {\n const instance = this.instances.get(instanceId);\r\n if (instance) {\n this.cleanups.get(instanceId)?.();\n this.cleanups.delete(instanceId);\n // Remove iframe from DOM\n const overlay = instance.iframe.parentElement?.parentElement;\n instance.iframe.remove();\n if (overlay?.dataset.apassOverlay === instanceId) overlay.remove();\n // Remove from tracking\r\n this.instances.delete(instanceId);\r\n }\r\n }\n\r\n\r\n /**\r\n * Prepare container for iframe\r\n */\r\n private prepareContainer(\r\n containerId: string | undefined,\r\n fullscreen: boolean,\r\n ): HTMLElement {\r\n if (containerId) {\r\n const container = document.getElementById(containerId);\r\n if (!container) {\r\n throw new Error(`Container with id \"${containerId}\" not found`);\r\n }\r\n return container;\r\n }\r\n\r\n // Create default container\r\n const container = document.createElement('div');\r\n container.style.cssText = `\r\n position: fixed;\r\n top: 0;\r\n left: 0;\r\n width: 100%;\r\n height: 100%;\r\n z-index: 10000;\r\n background: rgba(0, 0, 0, 0.5);\r\n display: flex;\r\n align-items: center;\r\n justify-content: center;\r\n `;\r\n\r\n const wrapper = document.createElement('div');\r\n if (fullscreen) {\r\n wrapper.style.cssText = `\r\n width: 100%;\r\n height: 100%;\r\n `;\r\n } else {\r\n wrapper.style.cssText = `\r\n width: 90%;\r\n height: 90%;\r\n max-width: 1200px;\r\n max-height: 800px;\r\n border-radius: 8px;\r\n overflow: hidden;\r\n box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);\r\n `;\r\n }\r\n\r\n container.appendChild(wrapper);\r\n document.body.appendChild(container);\r\n\r\n return wrapper;\n }\r\n\r\n /**\r\n * Create iframe element\r\n */\r\n private createIframe(\r\n url: string,\r\n attrs: Record<string, string>,\r\n ): HTMLIFrameElement {\r\n const iframe = document.createElement('iframe');\r\n\r\n // Build A-pass URL with token parameter\r\n iframe.src = url;\r\n iframe.style.cssText = `\r\n width: 100%;\r\n height: 100%;\r\n border: none;\r\n `;\r\n\r\n // Apply custom attributes\r\n Object.entries(attrs).forEach(([key, value]) => {\r\n iframe.setAttribute(key, value);\r\n });\r\n\r\n return iframe;\r\n }\r\n\r\n /**\r\n * Validate SDK configuration\r\n */\r\n private validateConfig(config: SDKConfig): void {\r\n if (!config.baseUrl) {\r\n throw new Error('SDKConfig.baseUrl is required');\r\n }\r\n\r\n try {\r\n new URL(config.baseUrl);\r\n } catch {\r\n throw new Error(\r\n `SDKConfig.baseUrl must be a valid URL: ${config.baseUrl}`,\r\n );\r\n }\r\n }\r\n\r\n /**\r\n * Close all instances\r\n */\r\n closeAll(): void {\r\n this.instances.forEach((_, instanceId) => {\r\n this.closeInstance(instanceId);\r\n });\r\n }\r\n\r\n /**\r\n * Get active instances count\r\n */\r\n getInstanceCount(): number {\r\n return this.instances.size;\r\n }\r\n}\r\n"],"mappings":"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","names":["reservedNames","isPlainObject","value","prototype","prepareIframeAttributes","attrs","entries","name","normalized","encodeSegment","buildEmbedUrl","baseUrl","options","apiName","url","params","ApassEmbedSDK","config","__publicField","options","apiName","pageCode","pageId","token","containerId","onReady","onBridgeReady","onClose","onError","onEvent","iframeAttrs","fullscreen","url","buildEmbedUrl","attrs","prepareIframeAttributes","iframe","container","instanceId","channelId","targetOrigin","ready","receiveMessage","event","message","instance","name","payload","overlay","error","err","wrapper","key","value","_"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,68 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@obm/apaas-embed-sdk",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "SDK for embedding A-pass pages into third-party applications using embed tokens",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.cjs",
|
|
7
|
+
"module": "./dist/index.js",
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"require": "./dist/index.cjs",
|
|
13
|
+
"import": "./dist/index.js"
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
"files": [
|
|
17
|
+
"dist"
|
|
18
|
+
],
|
|
19
|
+
"scripts": {
|
|
20
|
+
"build": "tsup",
|
|
21
|
+
"dev": "tsup --watch",
|
|
22
|
+
"test": "vitest",
|
|
23
|
+
"test:ui": "vitest --ui",
|
|
24
|
+
"lint": "eslint src --ext .ts",
|
|
25
|
+
"verify": "npm run lint && vitest run && npm run build && node scripts/verify-package.mjs",
|
|
26
|
+
"publish:check": "npm publish --dry-run --registry=https://registry.npmjs.org/",
|
|
27
|
+
"publish:release": "npm publish --registry=https://registry.npmjs.org/",
|
|
28
|
+
"format": "prettier --write src tests examples",
|
|
29
|
+
"prepublishOnly": "npm run verify"
|
|
30
|
+
},
|
|
31
|
+
"keywords": [
|
|
32
|
+
"a-pass",
|
|
33
|
+
"embed",
|
|
34
|
+
"iframe",
|
|
35
|
+
"sdk",
|
|
36
|
+
"token"
|
|
37
|
+
],
|
|
38
|
+
"author": "A-pass Team",
|
|
39
|
+
"license": "MIT",
|
|
40
|
+
"repository": {
|
|
41
|
+
"type": "git",
|
|
42
|
+
"url": "git+https://gitlab-ce.100url.cn/apaas/medipaas.git",
|
|
43
|
+
"directory": "medipaas-frontend/packages/embed-sdk"
|
|
44
|
+
},
|
|
45
|
+
"homepage": "https://gitlab-ce.100url.cn/apaas/medipaas/-/tree/develop/medipaas-frontend/packages/embed-sdk",
|
|
46
|
+
"bugs": {
|
|
47
|
+
"url": "https://gitlab-ce.100url.cn/apaas/medipaas/-/issues"
|
|
48
|
+
},
|
|
49
|
+
"devDependencies": {
|
|
50
|
+
"@types/node": "^20.10.0",
|
|
51
|
+
"@typescript-eslint/eslint-plugin": "^8.71.1",
|
|
52
|
+
"@typescript-eslint/parser": "^8.71.1",
|
|
53
|
+
"eslint": "^8.55.0",
|
|
54
|
+
"jsdom": "^29.1.1",
|
|
55
|
+
"prettier": "^3.1.1",
|
|
56
|
+
"tsup": "^8.0.1",
|
|
57
|
+
"typescript": "^5.3.3",
|
|
58
|
+
"vitest": "^1.0.4"
|
|
59
|
+
},
|
|
60
|
+
"publishConfig": {
|
|
61
|
+
"access": "public"
|
|
62
|
+
},
|
|
63
|
+
"directories": {
|
|
64
|
+
"doc": "docs",
|
|
65
|
+
"example": "examples",
|
|
66
|
+
"test": "tests"
|
|
67
|
+
}
|
|
68
|
+
}
|