@ichinese/open-js-sdk 0.0.0-stage → 1.0.0-rc.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 +39 -2
- package/dist/bridge.d.ts +31 -0
- package/dist/gen/meta.d.ts +8 -0
- package/dist/ichinese-open.js +1 -0
- package/dist/index.d.ts +81 -0
- package/dist/index.js +333 -0
- package/dist/oidc.d.ts +33 -0
- package/package.json +41 -4
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Center for Language Education and Cooperation (CLEC)
|
|
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,40 @@
|
|
|
1
|
-
#
|
|
1
|
+
# iChinese Open Platform JS SDK
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
爱中文开放平台的网页 JS SDK:应用的网页在桌面端、手机 App 和普通浏览器里用同一套代码登录,并调用容器的方法。
|
|
4
|
+
|
|
5
|
+
The web JS SDK of the iChinese open platform: one set of code for an app's web pages, whether they run in the
|
|
6
|
+
desktop app, the mobile apps' web containers or a plain browser. Inside a container `requestAuthCode()` gets the
|
|
7
|
+
login code straight from the bridge, without a redirect; in a plain browser it falls back to OIDC.
|
|
8
|
+
|
|
9
|
+
## Install
|
|
10
|
+
|
|
11
|
+
```sh
|
|
12
|
+
npm install @ichinese/open-js-sdk
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Quick start
|
|
16
|
+
|
|
17
|
+
```ts
|
|
18
|
+
import * as ichinese from '@ichinese/open-js-sdk';
|
|
19
|
+
|
|
20
|
+
ichinese.config({ appId: 'cn.cli_…' });
|
|
21
|
+
const env = await ichinese.ready(); // { container: 'desktop' | 'ios' | 'android' | 'browser', … }
|
|
22
|
+
|
|
23
|
+
// Your backend exchanges the code with the server SDK's exchangeAuthCode.
|
|
24
|
+
const result = await ichinese.requestAuthCode();
|
|
25
|
+
await fetch('/api/login', { method: 'POST', body: JSON.stringify(result) });
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
In a plain browser `requestAuthCode()` signs in at the issuer (silently first). The page goes there and comes
|
|
29
|
+
back; calling `requestAuthCode()` again on the way back returns the code.
|
|
30
|
+
|
|
31
|
+
## Documentation
|
|
32
|
+
|
|
33
|
+
Guides and the API reference: <https://open.ichinese.cn>
|
|
34
|
+
|
|
35
|
+
This repository is generated from the platform's monorepo at each release, so a change made here would be
|
|
36
|
+
overwritten; please open an issue instead.
|
|
37
|
+
|
|
38
|
+
## License
|
|
39
|
+
|
|
40
|
+
MIT
|
package/dist/bridge.d.ts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export interface BridgeHost {
|
|
2
|
+
postMessage(message: string): void;
|
|
3
|
+
}
|
|
4
|
+
export interface BridgeErrorInfo {
|
|
5
|
+
code: string;
|
|
6
|
+
message?: string;
|
|
7
|
+
}
|
|
8
|
+
/** An error from the container (or the fallback): `code` is one of the protocol's error codes. */
|
|
9
|
+
export declare class BridgeError extends Error {
|
|
10
|
+
readonly code: string;
|
|
11
|
+
readonly name = "BridgeError";
|
|
12
|
+
constructor(code: string, message?: string);
|
|
13
|
+
}
|
|
14
|
+
declare global {
|
|
15
|
+
interface Window {
|
|
16
|
+
ichOpenBridge?: BridgeHost;
|
|
17
|
+
__ichOpenBridgeReceive?: (message: string) => void;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
/** The container's bridge object, when the page is inside one. */
|
|
21
|
+
export declare function host(): BridgeHost | undefined;
|
|
22
|
+
export declare class Transport {
|
|
23
|
+
private readonly bridge;
|
|
24
|
+
private seq;
|
|
25
|
+
private readonly pending;
|
|
26
|
+
private readonly listeners;
|
|
27
|
+
constructor(bridge: BridgeHost);
|
|
28
|
+
call<T>(method: string, params?: Record<string, unknown>, timeoutMs?: number | null): Promise<T>;
|
|
29
|
+
on(event: string, listener: (data: unknown) => void): () => void;
|
|
30
|
+
private receive;
|
|
31
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/** The JS SDK's version (it follows the open API's). */
|
|
2
|
+
export declare const VERSION = "1.0.0-rc.1";
|
|
3
|
+
/** The single issuer (the plain-browser fallback signs in there); `config({ issuer })` overrides it. */
|
|
4
|
+
export declare const DEFAULT_ISSUER = "https://id.ichinese.cn";
|
|
5
|
+
/** Where each region's web serves the single-file build of this SDK. */
|
|
6
|
+
export declare const SCRIPT_URL = "https://ichinese.cn/open-js-sdk/1/ichinese-open.js";
|
|
7
|
+
/** The version of the container bridge's message protocol (docs/api/open-bridge.md). */
|
|
8
|
+
export declare const BRIDGE_PROTOCOL = 1;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"use strict";var ichinese=(()=>{var v=Object.defineProperty;var A=Object.getOwnPropertyDescriptor;var L=Object.getOwnPropertyNames;var V=Object.prototype.hasOwnProperty;var D=(e,r,n)=>r in e?v(e,r,{enumerable:!0,configurable:!0,writable:!0,value:n}):e[r]=n;var $=(e,r)=>{for(var n in r)v(e,n,{get:r[n],enumerable:!0})},q=(e,r,n,t)=>{if(r&&typeof r=="object"||typeof r=="function")for(let i of L(r))!V.call(e,i)&&i!==n&&v(e,i,{get:()=>r[i],enumerable:!(t=A(r,i))||t.enumerable});return e};var M=e=>q(v({},"__esModule",{value:!0}),e);var g=(e,r,n)=>D(e,typeof r!="symbol"?r+"":r,n);var re={};$(re,{BRIDGE_PROTOCOL:()=>_,BridgeError:()=>d,DEFAULT_ISSUER:()=>y,SCRIPT_URL:()=>P,VERSION:()=>h,_reset:()=>ee,close:()=>Y,config:()=>J,env:()=>B,inContainer:()=>K,on:()=>Z,openExternal:()=>z,ready:()=>f,requestAuthCode:()=>W,setTitle:()=>X,shareToChat:()=>Q});var h="1.0.0-rc.1",y="https://id.ichinese.cn",P="https://ichinese.cn/open-js-sdk/1/ichinese-open.js",_=1;var d=class extends Error{constructor(n,t){super(t||n);g(this,"code",n);g(this,"name","BridgeError")}},R=()=>typeof window=="undefined"?void 0:window;function b(){var r;let e=(r=R())==null?void 0:r.ichOpenBridge;return e&&typeof e.postMessage=="function"?e:void 0}var x=class{constructor(r){g(this,"bridge",r);g(this,"seq",0);g(this,"pending",new Map);g(this,"listeners",new Map);let n=R();if(!n)return;let t=n.__ichOpenBridgeReceive;n.__ichOpenBridgeReceive=i=>{this.receive(i),t&&t!==n.__ichOpenBridgeReceive&&t(i)}}call(r,n={},t=15e3){let i=`${Date.now().toString(36)}-${(++this.seq).toString(36)}`;return new Promise((c,s)=>{let o=t===null?null:setTimeout(()=>{this.pending.delete(i),s(new d("timeout",`${r}: no answer from the container`))},t);this.pending.set(i,{resolve:c,reject:s,timer:o});try{this.bridge.postMessage(JSON.stringify({v:1,id:i,method:r,params:n}))}catch(p){o&&clearTimeout(o),this.pending.delete(i),s(new d("internal",p.message))}})}on(r,n){let t=this.listeners.get(r);return t||(t=new Set,this.listeners.set(r,t)),t.add(n),()=>t.delete(n)}receive(r){var i,c,s,o,p;let n;try{n=typeof r=="string"?JSON.parse(r):r}catch{return}if(!n||typeof n!="object")return;if(typeof n.event=="string"){for(let w of(i=this.listeners.get(n.event))!=null?i:[])try{w(n.data)}catch{}return}let t=n.id?this.pending.get(n.id):void 0;t&&(this.pending.delete(n.id),t.timer&&clearTimeout(t.timer),n.ok?t.resolve((c=n.result)!=null?c:{}):t.reject(new d((o=(s=n.error)==null?void 0:s.code)!=null?o:"internal",(p=n.error)==null?void 0:p.message)))}};var I="ichopen:oidc:",N=10*6e4,j=new Set(["login_required","consent_required","interaction_required","account_selection_required"]),U=e=>btoa(String.fromCharCode(...e)).replace(/\+/g,"-").replace(/\//g,"_").replace(/=+$/,""),E=e=>U(crypto.getRandomValues(new Uint8Array(e)));async function H(e){let r=await crypto.subtle.digest("SHA-256",new TextEncoder().encode(e));return U(new Uint8Array(r))}function O(){try{return window.sessionStorage}catch{return null}}function G(e){for(let r of["code","state","error","error_description","iss","session_state"])e.searchParams.delete(r);try{history.replaceState(history.state,"",e.toString())}catch{}}function T(){let e=new URL(window.location.href),r=e.searchParams.get("state"),n=e.searchParams.get("code"),t=e.searchParams.get("error");if(!r||!n&&!t)return null;let i=O(),c=i==null?void 0:i.getItem(I+r);if(!c)return null;i==null||i.removeItem(I+r),G(e);let s=JSON.parse(c);return Date.now()-s.at>N?{error:"expired"}:n?{ok:{code:n,state:r,redirectUri:s.redirectUri,codeVerifier:s.verifier}}:s.silent&&t&&j.has(t)?{retry:{...s,state:r}}:{error:t!=null?t:"unknown"}}async function S(e,r){let n=O();if(!n)throw new d("unsupported","sessionStorage is not available");let t=E(16),i=E(32);n.setItem(I+t,JSON.stringify({verifier:i,redirectUri:e.redirectUri,appId:e.appId,silent:r,at:Date.now()}));let c=new URLSearchParams({client_id:e.appId,redirect_uri:e.redirectUri,response_type:"code",scope:["openid",...e.scopes.filter(s=>s!=="openid")].join(" "),state:t,nonce:E(16),code_challenge:await H(i),code_challenge_method:"S256",...r?{prompt:"none"}:{},...e.locale?{ui_locales:e.locale}:{}});return window.location.assign(`${e.issuer.replace(/\/$/,"")}/auth?${c}`),new Promise(()=>{})}var u={},a,l,m;function J(e){u={...u,...e}}function k(){var r;let e=typeof matchMedia=="function"&&matchMedia("(prefers-color-scheme: dark)").matches;return{container:"browser",containerVersion:null,locale:typeof navigator!="undefined"&&navigator.language||"en",theme:e?"dark":"light",appId:(r=u.appId)!=null?r:null,sdkVersion:h}}function f(){return m||(m=(async()=>{var n,t,i,c,s;typeof document!="undefined"&&document.readyState==="loading"&&await new Promise(o=>document.addEventListener("DOMContentLoaded",()=>o(),{once:!0}));let e=b();for(let o=0;!e&&o<5&&F();o++)await new Promise(p=>setTimeout(p,60)),e=b();if(!e)return l=k(),l;a=new x(e);let r=await a.call("env",{},5e3);return l={container:r.container,containerVersion:(n=r.container_version)!=null?n:null,locale:(t=r.locale)!=null?t:k().locale,theme:(i=r.theme)!=null?i:"light",appId:(s=(c=r.app_id)!=null?c:u.appId)!=null?s:null,sdkVersion:h},a.on("themeChanged",o=>{l&&o&&typeof o=="object"&&(l.theme=o.theme)}),a.on("localeChanged",o=>{l&&o&&typeof o=="object"&&(l.locale=o.locale)}),l})(),m)}function F(){return typeof navigator!="undefined"&&/\bIchineseApp\//.test(navigator.userAgent)}function B(){return l!=null?l:k()}var K=()=>B().container!=="browser";async function W(e={}){var c,s,o,p,w;let r=await f();if(r.container!=="browser"&&a){let C=await a.call("requestAuthCode",(c=e.scopes)!=null&&c.length?{scopes:e.scopes}:{},null);return{code:C.code,via:"bridge",expiresIn:C.expires_in}}let n=T();if(n&&"ok"in n)return{...n.ok,via:"oidc"};let t=u.appId;if(!t)throw new d("invalid_params","config({ appId }) first: needed outside the containers");let i={issuer:(s=u.issuer)!=null?s:y,appId:t,redirectUri:(o=u.redirectUri)!=null?o:`${location.origin}${location.pathname}`,scopes:(w=(p=e.scopes)!=null?p:u.scopes)!=null?w:[],locale:r.locale};if(n&&"retry"in n)return S(i,!1);if(n&&"error"in n)throw new d(n.error==="access_denied"?"consent_denied":n.error,`the issuer answered ${n.error}`);return S(i,!0)}async function X(e){let r=await f(),n=String(e).slice(0,100);if(r.container==="browser"||!a){document.title=n;return}await a.call("setTitle",{title:n})}async function Y(){if((await f()).container==="browser"||!a){window.close();return}await a.call("close")}async function z(e){let r=new URL(e,location.href);if(r.protocol!=="https:"&&r.protocol!=="http:")throw new d("invalid_params","http(s) links only");if((await f()).container==="browser"||!a){window.open(r.toString(),"_blank","noopener,noreferrer");return}await a.call("openExternal",{url:r.toString()})}async function Q(e){if((await f()).container==="browser"||!a){if(typeof navigator!="undefined"&&typeof navigator.share=="function")try{return await navigator.share({title:e.title,text:e.description,url:e.url}),{shared:!0}}catch{return{shared:!1}}throw new d("unsupported_method","sharing to a conversation needs the desktop or mobile app")}return a.call("shareToChat",{title:e.title,description:e.description,url:e.url,image_url:e.imageUrl},null)}function Z(e,r){return a?a.on(e,r):()=>{}}function ee(){u={},a=void 0,l=void 0,m=void 0}return M(re);})();
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
export { BridgeError } from './bridge.ts';
|
|
2
|
+
export { BRIDGE_PROTOCOL, DEFAULT_ISSUER, SCRIPT_URL, VERSION } from './gen/meta.ts';
|
|
3
|
+
export type Container = 'desktop' | 'ios' | 'android' | 'browser';
|
|
4
|
+
export interface Env {
|
|
5
|
+
container: Container;
|
|
6
|
+
/** The desktop or mobile app's version (null in a browser). */
|
|
7
|
+
containerVersion: string | null;
|
|
8
|
+
/** The interface language (BCP 47). */
|
|
9
|
+
locale: string;
|
|
10
|
+
theme: 'light' | 'dark';
|
|
11
|
+
/** The app the container opened (null in a browser: from `config`). */
|
|
12
|
+
appId: string | null;
|
|
13
|
+
/** This SDK's version. */
|
|
14
|
+
sdkVersion: string;
|
|
15
|
+
}
|
|
16
|
+
export interface Config {
|
|
17
|
+
/** Your App ID (`<region>.cli_…`): needed in a plain browser (the container knows which app it opened). */
|
|
18
|
+
appId?: string;
|
|
19
|
+
/** The issuer of the browser fallback (default: the release's, `https://id.ichinese.cn`). */
|
|
20
|
+
issuer?: string;
|
|
21
|
+
/** Where the issuer sends the browser back (registered in the developer console; default: this page). */
|
|
22
|
+
redirectUri?: string;
|
|
23
|
+
/** Scopes to ask for in the browser fallback besides `openid` (in a container: all the app declares). */
|
|
24
|
+
scopes?: string[];
|
|
25
|
+
}
|
|
26
|
+
export interface AuthCode {
|
|
27
|
+
/** One-time, 5 minutes, bound to your app (and in a container, to this page's origin). */
|
|
28
|
+
code: string;
|
|
29
|
+
/** `bridge`: from the container; `oidc`: from the issuer (plain browser). */
|
|
30
|
+
via: 'bridge' | 'oidc';
|
|
31
|
+
/** For `exchangeAuthCode` (oidc only). */
|
|
32
|
+
redirectUri?: string;
|
|
33
|
+
/** For `exchangeAuthCode` (oidc only). */
|
|
34
|
+
codeVerifier?: string;
|
|
35
|
+
/** The OAuth state that came back (oidc only; already checked against the one sent). */
|
|
36
|
+
state?: string;
|
|
37
|
+
/** Seconds the code stays valid (bridge only). */
|
|
38
|
+
expiresIn?: number;
|
|
39
|
+
}
|
|
40
|
+
export interface ShareCard {
|
|
41
|
+
title: string;
|
|
42
|
+
description?: string;
|
|
43
|
+
/** A page of your app (on one of its trusted origins). */
|
|
44
|
+
url: string;
|
|
45
|
+
imageUrl?: string;
|
|
46
|
+
}
|
|
47
|
+
/** Sets the app and the browser fallback's options (call before `requestAuthCode` in a browser). */
|
|
48
|
+
export declare function config(c: Config): void;
|
|
49
|
+
/** Waits for the page and the container (if any); what `env()` returns afterwards. */
|
|
50
|
+
export declare function ready(): Promise<Env>;
|
|
51
|
+
/** Where the page runs (call after `ready()`; before it, a browser's guess). */
|
|
52
|
+
export declare function env(): Env;
|
|
53
|
+
/** Whether the page is inside the desktop app or a mobile app. */
|
|
54
|
+
export declare const inContainer: () => boolean;
|
|
55
|
+
/**
|
|
56
|
+
* 免登: an authorization code for the signed-in user, for your backend to exchange (`exchangeAuthCode`
|
|
57
|
+
* of the server SDKs). In a container, the user is asked scope by scope first when the app is not added
|
|
58
|
+
* yet or wants something new; `scopes` asks again for ones refused before (unless 「不再询问」).
|
|
59
|
+
* Rejects with a `BridgeError` (`consent_denied`, `origin_not_trusted`, `app_disabled`, …).
|
|
60
|
+
*/
|
|
61
|
+
export declare function requestAuthCode(opts?: {
|
|
62
|
+
scopes?: string[];
|
|
63
|
+
}): Promise<AuthCode>;
|
|
64
|
+
/** The window's title (in a browser: the document's). */
|
|
65
|
+
export declare function setTitle(title: string): Promise<void>;
|
|
66
|
+
/** Closes the app's window (or its page in the mobile container). */
|
|
67
|
+
export declare function close(): Promise<void>;
|
|
68
|
+
/** Opens a link in the system browser (http and https only). */
|
|
69
|
+
export declare function openExternal(url: string): Promise<void>;
|
|
70
|
+
/**
|
|
71
|
+
* 分享到会话: the container's own conversation picker; the chosen conversations get a card of your app.
|
|
72
|
+
* `shared: false` when the user closed the picker. In a browser: the system share sheet when there is
|
|
73
|
+
* one, else `BridgeError('unsupported_method')`.
|
|
74
|
+
*/
|
|
75
|
+
export declare function shareToChat(card: ShareCard): Promise<{
|
|
76
|
+
shared: boolean;
|
|
77
|
+
}>;
|
|
78
|
+
/** Container events: `themeChanged` ({ theme }), `localeChanged` ({ locale }). Returns an unsubscribe. */
|
|
79
|
+
export declare function on(event: 'themeChanged' | 'localeChanged', listener: (data: unknown) => void): () => void;
|
|
80
|
+
/** Tests: forget what `ready()` found. */
|
|
81
|
+
export declare function _reset(): void;
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
var __defProp = Object.defineProperty;
|
|
2
|
+
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
|
|
3
|
+
var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
|
|
4
|
+
|
|
5
|
+
// src/gen/meta.ts
|
|
6
|
+
var VERSION = "1.0.0-rc.1";
|
|
7
|
+
var DEFAULT_ISSUER = "https://id.ichinese.cn";
|
|
8
|
+
var SCRIPT_URL = "https://ichinese.cn/open-js-sdk/1/ichinese-open.js";
|
|
9
|
+
var BRIDGE_PROTOCOL = 1;
|
|
10
|
+
|
|
11
|
+
// src/bridge.ts
|
|
12
|
+
var BridgeError = class extends Error {
|
|
13
|
+
constructor(code, message) {
|
|
14
|
+
super(message || code);
|
|
15
|
+
__publicField(this, "code", code);
|
|
16
|
+
__publicField(this, "name", "BridgeError");
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
var w = () => typeof window === "undefined" ? void 0 : window;
|
|
20
|
+
function host() {
|
|
21
|
+
var _a;
|
|
22
|
+
const b = (_a = w()) == null ? void 0 : _a.ichOpenBridge;
|
|
23
|
+
return b && typeof b.postMessage === "function" ? b : void 0;
|
|
24
|
+
}
|
|
25
|
+
var Transport = class {
|
|
26
|
+
constructor(bridge) {
|
|
27
|
+
__publicField(this, "bridge", bridge);
|
|
28
|
+
__publicField(this, "seq", 0);
|
|
29
|
+
__publicField(this, "pending", /* @__PURE__ */ new Map());
|
|
30
|
+
__publicField(this, "listeners", /* @__PURE__ */ new Map());
|
|
31
|
+
const win = w();
|
|
32
|
+
if (!win) return;
|
|
33
|
+
const previous = win.__ichOpenBridgeReceive;
|
|
34
|
+
win.__ichOpenBridgeReceive = (message) => {
|
|
35
|
+
this.receive(message);
|
|
36
|
+
if (previous && previous !== win.__ichOpenBridgeReceive) previous(message);
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
call(method, params = {}, timeoutMs = 15e3) {
|
|
40
|
+
const id = `${Date.now().toString(36)}-${(++this.seq).toString(36)}`;
|
|
41
|
+
return new Promise((resolve, reject) => {
|
|
42
|
+
const timer = timeoutMs === null ? null : setTimeout(() => {
|
|
43
|
+
this.pending.delete(id);
|
|
44
|
+
reject(new BridgeError("timeout", `${method}: no answer from the container`));
|
|
45
|
+
}, timeoutMs);
|
|
46
|
+
this.pending.set(id, { resolve, reject, timer });
|
|
47
|
+
try {
|
|
48
|
+
this.bridge.postMessage(JSON.stringify({ v: BRIDGE_PROTOCOL, id, method, params }));
|
|
49
|
+
} catch (e) {
|
|
50
|
+
if (timer) clearTimeout(timer);
|
|
51
|
+
this.pending.delete(id);
|
|
52
|
+
reject(new BridgeError("internal", e.message));
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
on(event, listener) {
|
|
57
|
+
let set = this.listeners.get(event);
|
|
58
|
+
if (!set) {
|
|
59
|
+
set = /* @__PURE__ */ new Set();
|
|
60
|
+
this.listeners.set(event, set);
|
|
61
|
+
}
|
|
62
|
+
set.add(listener);
|
|
63
|
+
return () => set.delete(listener);
|
|
64
|
+
}
|
|
65
|
+
receive(message) {
|
|
66
|
+
var _a, _b, _c, _d, _e;
|
|
67
|
+
let m;
|
|
68
|
+
try {
|
|
69
|
+
m = typeof message === "string" ? JSON.parse(message) : message;
|
|
70
|
+
} catch {
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
if (!m || typeof m !== "object") return;
|
|
74
|
+
if (typeof m.event === "string") {
|
|
75
|
+
for (const l of (_a = this.listeners.get(m.event)) != null ? _a : []) {
|
|
76
|
+
try {
|
|
77
|
+
l(m.data);
|
|
78
|
+
} catch {
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
const p = m.id ? this.pending.get(m.id) : void 0;
|
|
84
|
+
if (!p) return;
|
|
85
|
+
this.pending.delete(m.id);
|
|
86
|
+
if (p.timer) clearTimeout(p.timer);
|
|
87
|
+
if (m.ok) p.resolve((_b = m.result) != null ? _b : {});
|
|
88
|
+
else p.reject(new BridgeError((_d = (_c = m.error) == null ? void 0 : _c.code) != null ? _d : "internal", (_e = m.error) == null ? void 0 : _e.message));
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
// src/oidc.ts
|
|
93
|
+
var KEY = "ichopen:oidc:";
|
|
94
|
+
var MAX_AGE = 10 * 6e4;
|
|
95
|
+
var INTERACTIVE = /* @__PURE__ */ new Set([
|
|
96
|
+
"login_required",
|
|
97
|
+
"consent_required",
|
|
98
|
+
"interaction_required",
|
|
99
|
+
"account_selection_required"
|
|
100
|
+
]);
|
|
101
|
+
var b64url = (bytes) => btoa(String.fromCharCode(...bytes)).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
102
|
+
var random = (n) => b64url(crypto.getRandomValues(new Uint8Array(n)));
|
|
103
|
+
async function challenge(verifier) {
|
|
104
|
+
const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(verifier));
|
|
105
|
+
return b64url(new Uint8Array(digest));
|
|
106
|
+
}
|
|
107
|
+
function store() {
|
|
108
|
+
try {
|
|
109
|
+
return window.sessionStorage;
|
|
110
|
+
} catch {
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
function cleanUrl(url) {
|
|
115
|
+
for (const k of ["code", "state", "error", "error_description", "iss", "session_state"]) url.searchParams.delete(k);
|
|
116
|
+
try {
|
|
117
|
+
history.replaceState(history.state, "", url.toString());
|
|
118
|
+
} catch {
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
function takeResult() {
|
|
122
|
+
const url = new URL(window.location.href);
|
|
123
|
+
const state = url.searchParams.get("state");
|
|
124
|
+
const code = url.searchParams.get("code");
|
|
125
|
+
const error = url.searchParams.get("error");
|
|
126
|
+
if (!state || !code && !error) return null;
|
|
127
|
+
const s = store();
|
|
128
|
+
const raw = s == null ? void 0 : s.getItem(KEY + state);
|
|
129
|
+
if (!raw) return null;
|
|
130
|
+
s == null ? void 0 : s.removeItem(KEY + state);
|
|
131
|
+
cleanUrl(url);
|
|
132
|
+
const p = JSON.parse(raw);
|
|
133
|
+
if (Date.now() - p.at > MAX_AGE) return { error: "expired" };
|
|
134
|
+
if (code) return { ok: { code, state, redirectUri: p.redirectUri, codeVerifier: p.verifier } };
|
|
135
|
+
if (p.silent && error && INTERACTIVE.has(error)) return { retry: { ...p, state } };
|
|
136
|
+
return { error: error != null ? error : "unknown" };
|
|
137
|
+
}
|
|
138
|
+
async function redirect(o, silent) {
|
|
139
|
+
const s = store();
|
|
140
|
+
if (!s) throw new BridgeError("unsupported", "sessionStorage is not available");
|
|
141
|
+
const state = random(16);
|
|
142
|
+
const verifier = random(32);
|
|
143
|
+
s.setItem(
|
|
144
|
+
KEY + state,
|
|
145
|
+
JSON.stringify({ verifier, redirectUri: o.redirectUri, appId: o.appId, silent, at: Date.now() })
|
|
146
|
+
);
|
|
147
|
+
const q = new URLSearchParams({
|
|
148
|
+
client_id: o.appId,
|
|
149
|
+
redirect_uri: o.redirectUri,
|
|
150
|
+
response_type: "code",
|
|
151
|
+
scope: ["openid", ...o.scopes.filter((x) => x !== "openid")].join(" "),
|
|
152
|
+
state,
|
|
153
|
+
nonce: random(16),
|
|
154
|
+
code_challenge: await challenge(verifier),
|
|
155
|
+
code_challenge_method: "S256",
|
|
156
|
+
...silent ? { prompt: "none" } : {},
|
|
157
|
+
...o.locale ? { ui_locales: o.locale } : {}
|
|
158
|
+
});
|
|
159
|
+
window.location.assign(`${o.issuer.replace(/\/$/, "")}/auth?${q}`);
|
|
160
|
+
return new Promise(() => {
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// src/index.ts
|
|
165
|
+
var cfg = {};
|
|
166
|
+
var transport;
|
|
167
|
+
var current;
|
|
168
|
+
var readyPromise;
|
|
169
|
+
function config(c) {
|
|
170
|
+
cfg = { ...cfg, ...c };
|
|
171
|
+
}
|
|
172
|
+
function browserEnv() {
|
|
173
|
+
var _a;
|
|
174
|
+
const dark = typeof matchMedia === "function" && matchMedia("(prefers-color-scheme: dark)").matches;
|
|
175
|
+
return {
|
|
176
|
+
container: "browser",
|
|
177
|
+
containerVersion: null,
|
|
178
|
+
locale: typeof navigator !== "undefined" && navigator.language || "en",
|
|
179
|
+
theme: dark ? "dark" : "light",
|
|
180
|
+
appId: (_a = cfg.appId) != null ? _a : null,
|
|
181
|
+
sdkVersion: VERSION
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
function ready() {
|
|
185
|
+
if (readyPromise) return readyPromise;
|
|
186
|
+
readyPromise = (async () => {
|
|
187
|
+
var _a, _b, _c, _d, _e;
|
|
188
|
+
if (typeof document !== "undefined" && document.readyState === "loading") {
|
|
189
|
+
await new Promise((r) => document.addEventListener("DOMContentLoaded", () => r(), { once: true }));
|
|
190
|
+
}
|
|
191
|
+
let b = host();
|
|
192
|
+
for (let i = 0; !b && i < 5 && looksLikeContainer(); i++) {
|
|
193
|
+
await new Promise((r) => setTimeout(r, 60));
|
|
194
|
+
b = host();
|
|
195
|
+
}
|
|
196
|
+
if (!b) {
|
|
197
|
+
current = browserEnv();
|
|
198
|
+
return current;
|
|
199
|
+
}
|
|
200
|
+
transport = new Transport(b);
|
|
201
|
+
const e = await transport.call("env", {}, 5e3);
|
|
202
|
+
current = {
|
|
203
|
+
container: e.container,
|
|
204
|
+
containerVersion: (_a = e.container_version) != null ? _a : null,
|
|
205
|
+
locale: (_b = e.locale) != null ? _b : browserEnv().locale,
|
|
206
|
+
theme: (_c = e.theme) != null ? _c : "light",
|
|
207
|
+
appId: (_e = (_d = e.app_id) != null ? _d : cfg.appId) != null ? _e : null,
|
|
208
|
+
sdkVersion: VERSION
|
|
209
|
+
};
|
|
210
|
+
transport.on("themeChanged", (d) => {
|
|
211
|
+
if (current && d && typeof d === "object") current.theme = d.theme;
|
|
212
|
+
});
|
|
213
|
+
transport.on("localeChanged", (d) => {
|
|
214
|
+
if (current && d && typeof d === "object") current.locale = d.locale;
|
|
215
|
+
});
|
|
216
|
+
return current;
|
|
217
|
+
})();
|
|
218
|
+
return readyPromise;
|
|
219
|
+
}
|
|
220
|
+
function looksLikeContainer() {
|
|
221
|
+
return typeof navigator !== "undefined" && /\bIchineseApp\//.test(navigator.userAgent);
|
|
222
|
+
}
|
|
223
|
+
function env() {
|
|
224
|
+
return current != null ? current : browserEnv();
|
|
225
|
+
}
|
|
226
|
+
var inContainer = () => env().container !== "browser";
|
|
227
|
+
async function requestAuthCode(opts = {}) {
|
|
228
|
+
var _a, _b, _c, _d, _e;
|
|
229
|
+
const e = await ready();
|
|
230
|
+
if (e.container !== "browser" && transport) {
|
|
231
|
+
const r = await transport.call(
|
|
232
|
+
"requestAuthCode",
|
|
233
|
+
((_a = opts.scopes) == null ? void 0 : _a.length) ? { scopes: opts.scopes } : {},
|
|
234
|
+
// The user may take a while in the consent window.
|
|
235
|
+
null
|
|
236
|
+
);
|
|
237
|
+
return { code: r.code, via: "bridge", expiresIn: r.expires_in };
|
|
238
|
+
}
|
|
239
|
+
const back = takeResult();
|
|
240
|
+
if (back && "ok" in back) return { ...back.ok, via: "oidc" };
|
|
241
|
+
const appId = cfg.appId;
|
|
242
|
+
if (!appId) throw new BridgeError("invalid_params", "config({ appId }) first: needed outside the containers");
|
|
243
|
+
const o = {
|
|
244
|
+
issuer: (_b = cfg.issuer) != null ? _b : DEFAULT_ISSUER,
|
|
245
|
+
appId,
|
|
246
|
+
redirectUri: (_c = cfg.redirectUri) != null ? _c : `${location.origin}${location.pathname}`,
|
|
247
|
+
scopes: (_e = (_d = opts.scopes) != null ? _d : cfg.scopes) != null ? _e : [],
|
|
248
|
+
locale: e.locale
|
|
249
|
+
};
|
|
250
|
+
if (back && "retry" in back) return redirect(o, false);
|
|
251
|
+
if (back && "error" in back) {
|
|
252
|
+
throw new BridgeError(
|
|
253
|
+
back.error === "access_denied" ? "consent_denied" : back.error,
|
|
254
|
+
`the issuer answered ${back.error}`
|
|
255
|
+
);
|
|
256
|
+
}
|
|
257
|
+
return redirect(o, true);
|
|
258
|
+
}
|
|
259
|
+
async function setTitle(title) {
|
|
260
|
+
const e = await ready();
|
|
261
|
+
const t = String(title).slice(0, 100);
|
|
262
|
+
if (e.container === "browser" || !transport) {
|
|
263
|
+
document.title = t;
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
await transport.call("setTitle", { title: t });
|
|
267
|
+
}
|
|
268
|
+
async function close() {
|
|
269
|
+
const e = await ready();
|
|
270
|
+
if (e.container === "browser" || !transport) {
|
|
271
|
+
window.close();
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
await transport.call("close");
|
|
275
|
+
}
|
|
276
|
+
async function openExternal(url) {
|
|
277
|
+
const u = new URL(url, location.href);
|
|
278
|
+
if (u.protocol !== "https:" && u.protocol !== "http:") throw new BridgeError("invalid_params", "http(s) links only");
|
|
279
|
+
const e = await ready();
|
|
280
|
+
if (e.container === "browser" || !transport) {
|
|
281
|
+
window.open(u.toString(), "_blank", "noopener,noreferrer");
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
await transport.call("openExternal", { url: u.toString() });
|
|
285
|
+
}
|
|
286
|
+
async function shareToChat(card) {
|
|
287
|
+
const e = await ready();
|
|
288
|
+
if (e.container === "browser" || !transport) {
|
|
289
|
+
if (typeof navigator !== "undefined" && typeof navigator.share === "function") {
|
|
290
|
+
try {
|
|
291
|
+
await navigator.share({ title: card.title, text: card.description, url: card.url });
|
|
292
|
+
return { shared: true };
|
|
293
|
+
} catch {
|
|
294
|
+
return { shared: false };
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
throw new BridgeError("unsupported_method", "sharing to a conversation needs the desktop or mobile app");
|
|
298
|
+
}
|
|
299
|
+
return transport.call(
|
|
300
|
+
"shareToChat",
|
|
301
|
+
{ title: card.title, description: card.description, url: card.url, image_url: card.imageUrl },
|
|
302
|
+
null
|
|
303
|
+
);
|
|
304
|
+
}
|
|
305
|
+
function on(event, listener) {
|
|
306
|
+
if (!transport) return () => {
|
|
307
|
+
};
|
|
308
|
+
return transport.on(event, listener);
|
|
309
|
+
}
|
|
310
|
+
function _reset() {
|
|
311
|
+
cfg = {};
|
|
312
|
+
transport = void 0;
|
|
313
|
+
current = void 0;
|
|
314
|
+
readyPromise = void 0;
|
|
315
|
+
}
|
|
316
|
+
export {
|
|
317
|
+
BRIDGE_PROTOCOL,
|
|
318
|
+
BridgeError,
|
|
319
|
+
DEFAULT_ISSUER,
|
|
320
|
+
SCRIPT_URL,
|
|
321
|
+
VERSION,
|
|
322
|
+
_reset,
|
|
323
|
+
close,
|
|
324
|
+
config,
|
|
325
|
+
env,
|
|
326
|
+
inContainer,
|
|
327
|
+
on,
|
|
328
|
+
openExternal,
|
|
329
|
+
ready,
|
|
330
|
+
requestAuthCode,
|
|
331
|
+
setTitle,
|
|
332
|
+
shareToChat
|
|
333
|
+
};
|
package/dist/oidc.d.ts
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
export interface OidcOptions {
|
|
2
|
+
issuer: string;
|
|
3
|
+
appId: string;
|
|
4
|
+
redirectUri: string;
|
|
5
|
+
scopes: string[];
|
|
6
|
+
locale?: string;
|
|
7
|
+
}
|
|
8
|
+
export interface OidcCode {
|
|
9
|
+
code: string;
|
|
10
|
+
state: string;
|
|
11
|
+
redirectUri: string;
|
|
12
|
+
codeVerifier: string;
|
|
13
|
+
}
|
|
14
|
+
interface Pending {
|
|
15
|
+
verifier: string;
|
|
16
|
+
redirectUri: string;
|
|
17
|
+
appId: string;
|
|
18
|
+
silent: boolean;
|
|
19
|
+
at: number;
|
|
20
|
+
}
|
|
21
|
+
/** The answer in the current address, if it is the end of one of our own requests. */
|
|
22
|
+
export declare function takeResult(): {
|
|
23
|
+
ok: OidcCode;
|
|
24
|
+
} | {
|
|
25
|
+
retry: Pending & {
|
|
26
|
+
state: string;
|
|
27
|
+
};
|
|
28
|
+
} | {
|
|
29
|
+
error: string;
|
|
30
|
+
} | null;
|
|
31
|
+
/** Sends the page to the issuer (the returned promise never settles: the page goes away). */
|
|
32
|
+
export declare function redirect(o: OidcOptions, silent: boolean): Promise<never>;
|
|
33
|
+
export {};
|
package/package.json
CHANGED
|
@@ -1,6 +1,43 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ichinese/open-js-sdk",
|
|
3
|
-
"version": "
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "1.0.0-rc.1",
|
|
4
|
+
"description": "Web JS SDK for iChinese open platform web apps: the login bridge (requestAuthCode) in the desktop and mobile apps, falling back to OIDC in a plain browser, and the container's methods",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"types": "./dist/index.d.ts",
|
|
10
|
+
"import": "./dist/index.js"
|
|
11
|
+
}
|
|
12
|
+
},
|
|
13
|
+
"files": [
|
|
14
|
+
"dist",
|
|
15
|
+
"README.md",
|
|
16
|
+
"LICENSE"
|
|
17
|
+
],
|
|
18
|
+
"scripts": {
|
|
19
|
+
"build": "node build.mjs",
|
|
20
|
+
"typecheck": "tsc --noEmit",
|
|
21
|
+
"test": "vitest run"
|
|
22
|
+
},
|
|
23
|
+
"module": "./dist/index.js",
|
|
24
|
+
"types": "./dist/index.d.ts",
|
|
25
|
+
"repository": {
|
|
26
|
+
"type": "git",
|
|
27
|
+
"url": "git+https://github.com/ichinese-cn/open-sdk-js.git"
|
|
28
|
+
},
|
|
29
|
+
"homepage": "https://github.com/ichinese-cn/open-sdk-js#readme",
|
|
30
|
+
"bugs": {
|
|
31
|
+
"url": "https://github.com/ichinese-cn/open-sdk-js/issues"
|
|
32
|
+
},
|
|
33
|
+
"publishConfig": {
|
|
34
|
+
"access": "public"
|
|
35
|
+
},
|
|
36
|
+
"devDependencies": {
|
|
37
|
+
"@types/node": "24.19.2",
|
|
38
|
+
"esbuild": "0.28.2",
|
|
39
|
+
"happy-dom": "21.0.1",
|
|
40
|
+
"typescript": "5.9.3",
|
|
41
|
+
"vitest": "5.0.4"
|
|
42
|
+
}
|
|
43
|
+
}
|