@obm/apaas-embed-sdk 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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,123 @@
1
- # Temporary Holding Version
1
+ # MediPaaS Embed SDK
2
+
3
+ 在第三方网页中通过 iframe 打开 MediPaaS 应用页面。SDK 负责嵌入与实例管理;第三方后端负责获取 embed token。
2
4
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
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
+ })
30
+ // 需要关闭时:instance.close()
31
+ ```
32
+
33
+ 宿主需提供 `<div id="medipaas-container" style="height: 600px"></div>`。
34
+
35
+ ## Token 对接
36
+
37
+ 第三方后端向 MediPaaS 发送以下请求。`encryptedData` 是按双方约定的 RSA 公钥加密并 Base64 编码后的用户信息,由后端生成;请勿在浏览器中放置服务端凭证。
38
+
39
+ ```http
40
+ POST /api/auth/token/embed?clientId=your-client-id
41
+ Content-Type: application/json
42
+
43
+ {"encryptedData":"base64-rsa-encrypted-user-data"}
44
+ ```
45
+
46
+ 加密前的 JSON 至少包含 `{"clientId":"your-client-id","userId":20293}`。clientId 必须与请求一致;userId 是第三方用户 ID,对应目标应用启用用户的 `_app_user.relatedId`。
47
+
48
+ 成功响应采用 Result 包装,token 位于 `data` 中:
49
+
50
+ ```json
51
+ {
52
+ "code": 200,
53
+ "message": "Success",
54
+ "data": {
55
+ "token": "jwt-token",
56
+ "expireSeconds": 3600,
57
+ "runtimeAppId": 123,
58
+ "homePageId": 456,
59
+ "homePageCode": "order_detail"
60
+ }
61
+ }
62
+ ```
63
+
64
+ 上方 `getToken()` 表示宿主自有接口,示例假定它已检查 HTTP 与业务 code,并解包返回 `data`;如果宿主保留 Result 包装,应先读取 `response.data`。传给 SDK 的 appId 使用 `String(data.runtimeAppId)`,apiName 使用目标页面 API 名称。
65
+
66
+ ## 参数契约
67
+
68
+ 构造配置:`baseUrl` 必填,为完整前端地址;`timeout` 默认为 30000,当前尚未实现超时处理。
69
+
70
+ `embed()` 支持:
71
+
72
+ | 字段 | 说明 |
73
+ | --- | --- |
74
+ | `appId` | 必填字符串,使用 token 接口返回的 runtimeAppId |
75
+ | `apiName` | 推荐的页面 API 名称 |
76
+ | `pageCode` / `pageId` | 废弃但兼容,优先级 apiName → pageCode → pageId;三者至少一个 |
77
+ | `token` | 必填 embed token,SDK 不负责获取或刷新 |
78
+ | `params` | 可选业务参数普通对象,见下述规则 |
79
+ | `containerId` | 可选宿主 DOM ID;省略时创建遮罩弹窗 |
80
+ | `fullscreen` | 默认 false,仅控制默认弹窗尺寸 |
81
+ | `iframeAttrs` | iframe 属性字典,如 title、allow、sandbox;禁止 src/srcdoc(大小写均禁止) |
82
+ | `onReady` | iframe load 回调,不表示首次数据查询完成 |
83
+ | `onClose` | 实例 close() 回调;closeAll()、遮罩点击不会触发 |
84
+ | `onError` | 参数、容器或 iframe 错误回调;参数错误同时拒绝 embed Promise |
85
+
86
+ `params` 的值允许字符串、有限数字、布尔值、null、undefined。字符串通过 URLSearchParams 编码,调用方传原始值,不预先编码。数字与布尔转为字符串;0、false、空字符串保留;null/undefined 属性忽略。对象、数组、函数、NaN、Infinity 均拒绝;params 本身不能为 null。
87
+
88
+ 参数名称必须满足 `[A-Za-z_][A-Za-z0-9_]*`;token、__proto__、constructor、prototype 是保留名称。参数与 iframe 地址覆盖校验在创建 DOM 前完成,失败调用 onError 一次并拒绝 Promise。iframeAttrs 禁止 src/srcdoc 是对旧行为的收紧。
89
+
90
+ 页面通过 `{{ urlParams.value.orderId }}` 读取。所有 SDK 参数在页面中均为字符串,布尔判断使用 `{{ urlParams.value.editable === 'true' }}`,ID 建议传字符串以保留前导零和大整数精度。URL 显式值优先于页面参数默认值,显式空字符串不会被默认值覆盖。目标页面需将参数绑定到数据源查询条件或初始化动作。
91
+
92
+ 参数属于当前 iframe 地址,刷新可恢复,各实例互不共享;页间导航需显式传参。切换记录请重新 embed,暂不支持 updateParams 或业务 postMessage 通信。URL 中的业务参数不能赋予权限,服务端仍验证 token 与记录权限;不要通过 URL 传敏感内容或大量数据。
93
+
94
+ 实例返回 iframe、token、expiresAt(当前固定 0)和 close()。SDK 还提供 closeAll() 与 getInstanceCount()。现有默认弹窗关闭行为尚未完整清理遮罩,建议第三方使用指定容器。
95
+
96
+ ## 构建与示例
97
+
98
+ 在前端工作区执行 `pnpm build:sdk`,生成 ESM、CJS 和类型声明,不提供 UMD 全局变量版本。安装包后使用上述模块导入;原生 HTML 通过 `<script type="module">` 导入 ESM。
99
+
100
+ - [快速开始](docs/QUICKSTART.md)
101
+ - [文档索引](docs/README.md)
102
+ - [原生 HTML 示例](examples/basic-html/index.html):构建后以 HTTP 静态服务打开,填入前端地址、runtimeAppId、apiName、token 和参数。
103
+
104
+ 测试:`pnpm --filter @obm/apaas-embed-sdk exec vitest run`。
105
+
106
+ 在前端工作区运行 `pnpm --filter medipaas-frontend test:embed-params`,会先构建最新 SDK,再验证设计器生成代码与页面参数查询链路,避免读取旧 dist。
107
+
108
+ ## 发布检查
109
+
110
+ 在 SDK 目录执行 `pnpm run verify`,依次检查 lint、单元测试、构建、打包、临时项目安装、ESM/CJS 导入及类型声明。临时测试文件会自动清理。
111
+
112
+ `npm publish` 会通过 `prepublishOnly` 自动执行完整验证,检查失败时停止发布。发布前需确认使用 `obm` 账号发布到 `@obm` scope,且目标版本尚未发布。
113
+
114
+ ```bash
115
+ pnpm run publish:check
116
+ pnpm run publish:release
117
+ ```
118
+
119
+ 第一条仅预演;第二条实际发布公开包,使用默认的 `latest` 标签。npm 版本发布后不能覆盖,后续发布需更新 `package.json` 版本号。
120
+
121
+ ## 许可与反馈
122
+
123
+ 本 SDK 使用 [MIT License](LICENSE)。仓库及问题反馈地址见 `package.json` 的 `repository`、`homepage` 与 `bugs` 字段;访问项目 GitLab 可能需要账号权限。
package/dist/index.cjs ADDED
@@ -0,0 +1,28 @@
1
+ "use strict";var d=Object.defineProperty;var O=Object.getOwnPropertyDescriptor;var A=Object.getOwnPropertyNames;var R=Object.prototype.hasOwnProperty;var j=(r,e,t)=>e in r?d(r,e,{enumerable:!0,configurable:!0,writable:!0,value:t}):r[e]=t;var P=(r,e)=>{for(var t in e)d(r,t,{get:e[t],enumerable:!0})},U=(r,e,t,n)=>{if(e&&typeof e=="object"||typeof e=="function")for(let o of A(e))!R.call(r,o)&&o!==t&&d(r,o,{get:()=>e[o],enumerable:!(n=O(e,o))||n.enumerable});return r};var $=r=>U(d({},"__esModule",{value:!0}),r);var m=(r,e,t)=>j(r,typeof e!="symbol"?e+"":e,t);var D={};P(D,{ApassEmbedSDK:()=>f});module.exports=$(D);var k=new Set(["token","__proto__","constructor","prototype"]);function b(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 w(r={}){if(!b(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 g(r,e){if(!r||r==="."||r==="..")throw new Error(`${e} must be a non-empty path segment other than . or ..`);return encodeURIComponent(r)}function E(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/${g(e.appId,"appId")}/user/${g(t,"apiName")}`,r);n.searchParams.set("token",e.token);let o=e.params;if(o!==void 0){if(!b(o))throw new Error("params must be a plain object");for(let[s,i]of Object.entries(o)){if(!/^[A-Za-z_][A-Za-z0-9_]*$/.test(s)||k.has(s))throw new Error(`Invalid or reserved parameter name: ${s}`);if(i!=null){if(!["string","number","boolean"].includes(typeof i)||typeof i=="number"&&!Number.isFinite(i))throw new Error(`Parameter ${s} must be a string, finite number, or boolean`);n.searchParams.set(s,String(i))}}}return n.toString()}var f=class{constructor(e){m(this,"config");m(this,"instances",new Map);m(this,"instanceCounter",0);this.validateConfig(e),this.config={timeout:3e4,...e}}async embed(e){let{apiName:t,pageCode:n,pageId:o,token:s,containerId:i,onReady:y,onClose:C,onError:l,iframeAttrs:I={},fullscreen:v=!1}=e;try{let a=E(this.config.baseUrl,e),c=w(I);n&&!t&&console.warn("[MediPaaS SDK] pageCode is deprecated. Please use apiName instead."),o&&!t&&!n&&console.warn("[MediPaaS SDK] pageId is deprecated. Please use apiName instead.");let p=this.createIframe(a,c),x=this.prepareContainer(i,v),u=`apass-${++this.instanceCounter}`,h={iframe:p,token:s,expiresAt:0,close:()=>{this.closeInstance(u),C?.()}};return x.appendChild(p),p.onload=()=>{y?.()},p.onerror=()=>{let S=new Error("Failed to load A-pass iframe");l?.(S)},this.instances.set(u,h),h}catch(a){let c=a instanceof Error?a:new Error(String(a));throw l?.(c),c}}closeInstance(e){let t=this.instances.get(e);t&&(t.iframe.remove(),this.instances.delete(e))}prepareContainer(e,t){if(e){let s=document.getElementById(e);if(!s)throw new Error(`Container with id "${e}" not found`);return s}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 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
+ `,n.appendChild(o),document.body.appendChild(n),n.addEventListener("click",s=>{s.target===n&&n.remove()}),o}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(([o,s])=>{n.setAttribute(o,s)}),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,\r\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();\r\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,\r\n onClose,\r\n onError,\r\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}`;\r\n\r\n const instance: ApassInstanceRef = {\r\n iframe,\r\n token,\r\n expiresAt: 0, // Token expiration not tracked in this mode\r\n close: () => {\r\n this.closeInstance(instanceId);\r\n onClose?.();\r\n },\r\n };\r\n\r\n // Step 6: Append iframe to container\r\n container.appendChild(iframe);\r\n\r\n // Step 7: Setup ready handler\r\n iframe.onload = () => {\r\n onReady?.();\r\n };\r\n\r\n // Step 8: Setup error handler\r\n iframe.onerror = () => {\r\n const error = new Error('Failed to load A-pass iframe');\r\n onError?.(error);\r\n };\r\n\r\n // Store instance reference\r\n this.instances.set(instanceId, instance);\r\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 {\r\n const instance = this.instances.get(instanceId);\r\n if (instance) {\r\n // Remove iframe from DOM\r\n instance.iframe.remove();\r\n // Remove from tracking\r\n this.instances.delete(instanceId);\r\n }\r\n }\r\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 // Auto-close on background click\r\n container.addEventListener('click', (e) => {\r\n if (e.target === container) {\r\n container.remove();\r\n }\r\n });\r\n\r\n return wrapper;\r\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","onClose","onError","iframeAttrs","fullscreen","url","buildEmbedUrl","attrs","prepareIframeAttributes","iframe","container","instanceId","instance","error","err","wrapper","e","key","value","_"]}
@@ -0,0 +1,117 @@
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 EmbedOptions {
15
+ /** Application ID in A-pass */
16
+ appId: string;
17
+ /** Page API name (recommended) */
18
+ apiName?: string;
19
+ /** @deprecated Use apiName instead. Kept for backward compatibility. */
20
+ pageCode?: string;
21
+ /** @deprecated Legacy runtime page ID. Use apiName instead. */
22
+ pageId?: string;
23
+ /** Embed token (from CRM backend) */
24
+ token: string;
25
+ /** Initial business parameters. Scalars become URL strings; null/undefined are omitted. */
26
+ params?: Record<string, EmbedParamValue>;
27
+ /** Optional target container ID. If not provided, creates a modal-like div */
28
+ containerId?: string;
29
+ /** Optional callback when iframe is loaded */
30
+ onReady?: () => void;
31
+ /** Optional callback when iframe is closed/destroyed */
32
+ onClose?: () => void;
33
+ /** Optional callback on error */
34
+ onError?: (error: Error) => void;
35
+ /** Optional custom iframe attributes. src and srcdoc are prohibited. */
36
+ iframeAttrs?: Record<string, string>;
37
+ /** Whether to fullscreen the iframe (default: false) */
38
+ fullscreen?: boolean;
39
+ }
40
+ /**
41
+ * Reference to an opened A-pass application instance
42
+ */
43
+ interface ApassInstanceRef {
44
+ /** The iframe element */
45
+ iframe: HTMLIFrameElement;
46
+ /** Close/destroy the instance */
47
+ close: () => void;
48
+ /** Token expiration time */
49
+ expiresAt: number;
50
+ /** Token value */
51
+ token: string;
52
+ }
53
+
54
+ /**
55
+ * Main SDK class for embedding A-pass applications
56
+ *
57
+ * @example
58
+ * ```ts
59
+ * const sdk = new ApassEmbedSDK({
60
+ * baseUrl: 'https://apass.example.com',
61
+ * });
62
+ *
63
+ * // CRM 项目先调自己的后端获取 token
64
+ * const tokenResponse = await fetch('/api/embed-token', {
65
+ * method: 'POST',
66
+ * body: JSON.stringify({
67
+ * recordId: 416,
68
+ * enterpriseId: 456,
69
+ * }),
70
+ * });
71
+ * const { token } = await tokenResponse.json();
72
+ *
73
+ * // 然后把 token 传给 SDK
74
+ * await sdk.embed({
75
+ * appId: '180',
76
+ * apiName: 'patient_list',
77
+ * token: token,
78
+ * containerId: 'my-container',
79
+ * });
80
+ * ```
81
+ */
82
+ declare class ApassEmbedSDK {
83
+ private config;
84
+ private instances;
85
+ private instanceCounter;
86
+ constructor(config: SDKConfig);
87
+ /**
88
+ * Embed an A-pass application in an iframe
89
+ */
90
+ embed(options: EmbedOptions): Promise<ApassInstanceRef>;
91
+ /**
92
+ * Close and cleanup an instance
93
+ */
94
+ private closeInstance;
95
+ /**
96
+ * Prepare container for iframe
97
+ */
98
+ private prepareContainer;
99
+ /**
100
+ * Create iframe element
101
+ */
102
+ private createIframe;
103
+ /**
104
+ * Validate SDK configuration
105
+ */
106
+ private validateConfig;
107
+ /**
108
+ * Close all instances
109
+ */
110
+ closeAll(): void;
111
+ /**
112
+ * Get active instances count
113
+ */
114
+ getInstanceCount(): number;
115
+ }
116
+
117
+ export { ApassEmbedSDK, type ApassInstanceRef, type EmbedOptions, type EmbedParamValue, type SDKConfig };
@@ -0,0 +1,117 @@
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 EmbedOptions {
15
+ /** Application ID in A-pass */
16
+ appId: string;
17
+ /** Page API name (recommended) */
18
+ apiName?: string;
19
+ /** @deprecated Use apiName instead. Kept for backward compatibility. */
20
+ pageCode?: string;
21
+ /** @deprecated Legacy runtime page ID. Use apiName instead. */
22
+ pageId?: string;
23
+ /** Embed token (from CRM backend) */
24
+ token: string;
25
+ /** Initial business parameters. Scalars become URL strings; null/undefined are omitted. */
26
+ params?: Record<string, EmbedParamValue>;
27
+ /** Optional target container ID. If not provided, creates a modal-like div */
28
+ containerId?: string;
29
+ /** Optional callback when iframe is loaded */
30
+ onReady?: () => void;
31
+ /** Optional callback when iframe is closed/destroyed */
32
+ onClose?: () => void;
33
+ /** Optional callback on error */
34
+ onError?: (error: Error) => void;
35
+ /** Optional custom iframe attributes. src and srcdoc are prohibited. */
36
+ iframeAttrs?: Record<string, string>;
37
+ /** Whether to fullscreen the iframe (default: false) */
38
+ fullscreen?: boolean;
39
+ }
40
+ /**
41
+ * Reference to an opened A-pass application instance
42
+ */
43
+ interface ApassInstanceRef {
44
+ /** The iframe element */
45
+ iframe: HTMLIFrameElement;
46
+ /** Close/destroy the instance */
47
+ close: () => void;
48
+ /** Token expiration time */
49
+ expiresAt: number;
50
+ /** Token value */
51
+ token: string;
52
+ }
53
+
54
+ /**
55
+ * Main SDK class for embedding A-pass applications
56
+ *
57
+ * @example
58
+ * ```ts
59
+ * const sdk = new ApassEmbedSDK({
60
+ * baseUrl: 'https://apass.example.com',
61
+ * });
62
+ *
63
+ * // CRM 项目先调自己的后端获取 token
64
+ * const tokenResponse = await fetch('/api/embed-token', {
65
+ * method: 'POST',
66
+ * body: JSON.stringify({
67
+ * recordId: 416,
68
+ * enterpriseId: 456,
69
+ * }),
70
+ * });
71
+ * const { token } = await tokenResponse.json();
72
+ *
73
+ * // 然后把 token 传给 SDK
74
+ * await sdk.embed({
75
+ * appId: '180',
76
+ * apiName: 'patient_list',
77
+ * token: token,
78
+ * containerId: 'my-container',
79
+ * });
80
+ * ```
81
+ */
82
+ declare class ApassEmbedSDK {
83
+ private config;
84
+ private instances;
85
+ private instanceCounter;
86
+ constructor(config: SDKConfig);
87
+ /**
88
+ * Embed an A-pass application in an iframe
89
+ */
90
+ embed(options: EmbedOptions): Promise<ApassInstanceRef>;
91
+ /**
92
+ * Close and cleanup an instance
93
+ */
94
+ private closeInstance;
95
+ /**
96
+ * Prepare container for iframe
97
+ */
98
+ private prepareContainer;
99
+ /**
100
+ * Create iframe element
101
+ */
102
+ private createIframe;
103
+ /**
104
+ * Validate SDK configuration
105
+ */
106
+ private validateConfig;
107
+ /**
108
+ * Close all instances
109
+ */
110
+ closeAll(): void;
111
+ /**
112
+ * Get active instances count
113
+ */
114
+ getInstanceCount(): number;
115
+ }
116
+
117
+ export { ApassEmbedSDK, type ApassInstanceRef, type EmbedOptions, type EmbedParamValue, type SDKConfig };
package/dist/index.js ADDED
@@ -0,0 +1,28 @@
1
+ var S=Object.defineProperty;var O=(n,e,t)=>e in n?S(n,e,{enumerable:!0,configurable:!0,writable:!0,value:t}):n[e]=t;var d=(n,e,t)=>O(n,typeof e!="symbol"?e+"":e,t);var A=new Set(["token","__proto__","constructor","prototype"]);function g(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 b(n={}){if(!g(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 h(n,e){if(!n||n==="."||n==="..")throw new Error(`${e} must be a non-empty path segment other than . or ..`);return encodeURIComponent(n)}function w(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/${h(e.appId,"appId")}/user/${h(t,"apiName")}`,n);r.searchParams.set("token",e.token);let s=e.params;if(s!==void 0){if(!g(s))throw new Error("params must be a plain object");for(let[o,i]of Object.entries(s)){if(!/^[A-Za-z_][A-Za-z0-9_]*$/.test(o)||A.has(o))throw new Error(`Invalid or reserved parameter name: ${o}`);if(i!=null){if(!["string","number","boolean"].includes(typeof i)||typeof i=="number"&&!Number.isFinite(i))throw new Error(`Parameter ${o} must be a string, finite number, or boolean`);r.searchParams.set(o,String(i))}}}return r.toString()}var m=class{constructor(e){d(this,"config");d(this,"instances",new Map);d(this,"instanceCounter",0);this.validateConfig(e),this.config={timeout:3e4,...e}}async embed(e){let{apiName:t,pageCode:r,pageId:s,token:o,containerId:i,onReady:E,onClose:y,onError:f,iframeAttrs:C={},fullscreen:I=!1}=e;try{let a=w(this.config.baseUrl,e),c=b(C);r&&!t&&console.warn("[MediPaaS SDK] pageCode is deprecated. Please use apiName instead."),s&&!t&&!r&&console.warn("[MediPaaS SDK] pageId is deprecated. Please use apiName instead.");let p=this.createIframe(a,c),v=this.prepareContainer(i,I),l=`apass-${++this.instanceCounter}`,u={iframe:p,token:o,expiresAt:0,close:()=>{this.closeInstance(l),y?.()}};return v.appendChild(p),p.onload=()=>{E?.()},p.onerror=()=>{let x=new Error("Failed to load A-pass iframe");f?.(x)},this.instances.set(l,u),u}catch(a){let c=a instanceof Error?a:new Error(String(a));throw f?.(c),c}}closeInstance(e){let t=this.instances.get(e);t&&(t.iframe.remove(),this.instances.delete(e))}prepareContainer(e,t){if(e){let o=document.getElementById(e);if(!o)throw new Error(`Container with id "${e}" not found`);return o}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 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
+ `,r.appendChild(s),document.body.appendChild(r),r.addEventListener("click",o=>{o.target===r&&r.remove()}),s}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(([s,o])=>{r.setAttribute(s,o)}),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{m 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();\r\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,\r\n onClose,\r\n onError,\r\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}`;\r\n\r\n const instance: ApassInstanceRef = {\r\n iframe,\r\n token,\r\n expiresAt: 0, // Token expiration not tracked in this mode\r\n close: () => {\r\n this.closeInstance(instanceId);\r\n onClose?.();\r\n },\r\n };\r\n\r\n // Step 6: Append iframe to container\r\n container.appendChild(iframe);\r\n\r\n // Step 7: Setup ready handler\r\n iframe.onload = () => {\r\n onReady?.();\r\n };\r\n\r\n // Step 8: Setup error handler\r\n iframe.onerror = () => {\r\n const error = new Error('Failed to load A-pass iframe');\r\n onError?.(error);\r\n };\r\n\r\n // Store instance reference\r\n this.instances.set(instanceId, instance);\r\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 {\r\n const instance = this.instances.get(instanceId);\r\n if (instance) {\r\n // Remove iframe from DOM\r\n instance.iframe.remove();\r\n // Remove from tracking\r\n this.instances.delete(instanceId);\r\n }\r\n }\r\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 // Auto-close on background click\r\n container.addEventListener('click', (e) => {\r\n if (e.target === container) {\r\n container.remove();\r\n }\r\n });\r\n\r\n return wrapper;\r\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","onClose","onError","iframeAttrs","fullscreen","url","buildEmbedUrl","attrs","prepareIframeAttributes","iframe","container","instanceId","instance","error","err","wrapper","e","key","value","_"]}
package/package.json CHANGED
@@ -1,6 +1,68 @@
1
1
  {
2
2
  "name": "@obm/apaas-embed-sdk",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.0",
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
+ }