chore(repo): initialize WebDrop monorepo
Signed-off-by: JustMe <mr.toing@outlook.com>
This commit is contained in:
commit
53768a195d
39 files changed
+9063
No files matched your search
@@ -0,0 +1,92 @@
|
||||
import { ID_SIZE, KEY_SIZE, type ChannelMode } from './constants.js';
|
||||
import { fromBase64Url, toBase64Url, type Bytes } from './bytes.js';
|
||||
import { LinkError } from './errors.js';
|
||||
|
||||
/**
|
||||
* 频道链接(protocol §2):
|
||||
* enc 所有者 /c/{id}#k=<K>&a=<A> 成员 /c/{id}#k=<K>
|
||||
* raw 所有者 /c/{id}#a=<A>[&w=<W>] 成员 /c/{id}[#w=<W>]
|
||||
*
|
||||
* 密钥只放在 fragment 里,浏览器不会把它发给服务器。降级防御:带 k 就是 enc,否则是 raw。
|
||||
*/
|
||||
|
||||
export interface ParsedLink {
|
||||
/** 频道 id 的 base64url 文本。 */
|
||||
id: string;
|
||||
idBytes: Bytes;
|
||||
/** 带 k 即 enc,否则 raw。客户端必须据此核对服务端报告的模式。 */
|
||||
mode: ChannelMode;
|
||||
role: 'owner' | 'member';
|
||||
/** 频道密钥,仅 enc。 */
|
||||
k?: Bytes;
|
||||
/** 管理密钥,仅所有者链接。 */
|
||||
a?: Bytes;
|
||||
/** raw 模式成员的写密钥。 */
|
||||
w?: Bytes;
|
||||
}
|
||||
|
||||
const PATH = /(?:^|\/)c\/([A-Za-z0-9_-]{22})\/?$/;
|
||||
|
||||
export function parseLink(input: string | URL): ParsedLink {
|
||||
let url: URL;
|
||||
try {
|
||||
url = typeof input === 'string' ? new URL(input) : input;
|
||||
} catch {
|
||||
throw new LinkError('不是合法的链接');
|
||||
}
|
||||
|
||||
const match = PATH.exec(url.pathname);
|
||||
if (!match) throw new LinkError('链接里没有频道 id');
|
||||
const id = match[1] as string;
|
||||
const idBytes = fromBase64Url(id, ID_SIZE);
|
||||
if (!idBytes) throw new LinkError('频道 id 不合法');
|
||||
|
||||
// 密钥放在查询串里会随请求发给服务器,等于已经泄露,必须拒绝而不是悄悄当成 raw 链接
|
||||
for (const name of ['k', 'a', 'w']) {
|
||||
if (url.searchParams.has(name)) throw new LinkError('密钥必须放在 # 后面,放在 ? 后面会被发送给服务器');
|
||||
}
|
||||
|
||||
const params = new URLSearchParams(url.hash.startsWith('#') ? url.hash.slice(1) : url.hash);
|
||||
const key = (name: string): Bytes | undefined => {
|
||||
const v = params.get(name);
|
||||
if (v === null) return undefined;
|
||||
const bytes = fromBase64Url(v, KEY_SIZE);
|
||||
if (!bytes) throw new LinkError(`链接里的 ${name} 不合法`);
|
||||
return bytes;
|
||||
};
|
||||
|
||||
const k = key('k');
|
||||
const a = key('a');
|
||||
const w = key('w');
|
||||
|
||||
// enc 频道的写令牌由 K 派生,出现 w 说明链接被拼错或被篡改
|
||||
if (k && w) throw new LinkError('enc 链接不应包含 w');
|
||||
|
||||
const link: ParsedLink = {
|
||||
id,
|
||||
idBytes,
|
||||
mode: k ? 'enc' : 'raw',
|
||||
role: a ? 'owner' : 'member',
|
||||
};
|
||||
if (k) link.k = k;
|
||||
if (a) link.a = a;
|
||||
if (w) link.w = w;
|
||||
return link;
|
||||
}
|
||||
|
||||
export interface LinkParts {
|
||||
id: string;
|
||||
k?: Uint8Array;
|
||||
a?: Uint8Array;
|
||||
w?: Uint8Array;
|
||||
}
|
||||
|
||||
/** 生成链接。origin 如 "https://app.example.com",路径固定为 /c/{id}。 */
|
||||
export function buildLink(origin: string, parts: LinkParts): string {
|
||||
const params: string[] = [];
|
||||
if (parts.k) params.push(`k=${toBase64Url(parts.k)}`);
|
||||
if (parts.a) params.push(`a=${toBase64Url(parts.a)}`);
|
||||
if (parts.w) params.push(`w=${toBase64Url(parts.w)}`);
|
||||
const base = `${origin.replace(/\/+$/, '')}/c/${parts.id}`;
|
||||
return params.length > 0 ? `${base}#${params.join('&')}` : base;
|
||||
}
|
||||
Reference in new issue
Block a user