diff --git a/.oxlintrc.json b/.oxlintrc.json index 6ec1ad0d82..1f11a9e989 100644 --- a/.oxlintrc.json +++ b/.oxlintrc.json @@ -57,6 +57,14 @@ } ], "no-useless-constructor": "error", + "no-restricted-properties": [ + "error", + { + "object": "crypto", + "property": "randomUUID", + "message": "browsers withhold crypto.randomUUID outside secure contexts (plain-HTTP LAN pages); mint through @deepseek-ai/dsh-util-crypto instead" + } + ], "typescript/await-thenable": "error", "typescript/ban-ts-comment": [ "error", diff --git a/packages/client/tsdown.client.ts b/packages/client/tsdown.client.ts index 44aab8cf1b..9b3621f8c9 100644 --- a/packages/client/tsdown.client.ts +++ b/packages/client/tsdown.client.ts @@ -58,7 +58,7 @@ function styleInjectionModule( * Everything else under @deepseek-ai/* is either a module-table entry * (external) or a leak the purity gate rejects. */ -export const INLINE_SAFE = /^@deepseek-ai\/dsh-(host-apiproxy|file-reference|session|llm|tools|brand)(\/|$)/ +export const INLINE_SAFE = /^@deepseek-ai\/dsh-(host-apiproxy|file-reference|session|llm|tools|brand|util-crypto)(\/|$)/ /** * Vendored framework libraries: rescoped into @deepseek-ai, so the gate below diff --git a/packages/client/ui-conversation/package.json b/packages/client/ui-conversation/package.json index fb00dd319c..f06804790b 100644 --- a/packages/client/ui-conversation/package.json +++ b/packages/client/ui-conversation/package.json @@ -48,8 +48,8 @@ }, "license": "MIT", "dependencies": { - "clsx": "^2.0.0", - "@deepseek-ai/schemastery": "workspace:^" + "@deepseek-ai/schemastery": "workspace:^", + "clsx": "^2.0.0" }, "peerDependencies": { "@deepseek-ai/cordis": "workspace:^", @@ -60,21 +60,22 @@ "@deepseek-ai/dsh-client-connection": "workspace:^", "@deepseek-ai/dsh-client-locale": "workspace:^", "@deepseek-ai/dsh-client-runtime": "workspace:^", - "@deepseek-ai/dsh-client-ui-settings": "workspace:^", "@deepseek-ai/dsh-client-ui-input-trigger": "workspace:^", + "@deepseek-ai/dsh-client-ui-layout": "workspace:^", + "@deepseek-ai/dsh-client-ui-settings": "workspace:^", "@deepseek-ai/dsh-commands": "workspace:^", "@deepseek-ai/dsh-compaction": "workspace:^", + "@deepseek-ai/dsh-goal": "workspace:^", "@deepseek-ai/dsh-invariants": "workspace:^", "@deepseek-ai/dsh-llm-retry": "workspace:^", - "@deepseek-ai/dsh-session-stats": "workspace:^", - "@deepseek-ai/dsh-token-meter": "workspace:^", - "@deepseek-ai/dsh-tools": "workspace:^", - "@deepseek-ai/dsh-settings": "workspace:^", - "@deepseek-ai/dsh-client-ui-layout": "workspace:^", - "@deepseek-ai/dsh-goal": "workspace:^", "@deepseek-ai/dsh-permission-presets": "workspace:^", "@deepseek-ai/dsh-plan-mode": "workspace:^", - "@deepseek-ai/dsh-tool-todo": "workspace:^" + "@deepseek-ai/dsh-session-stats": "workspace:^", + "@deepseek-ai/dsh-settings": "workspace:^", + "@deepseek-ai/dsh-token-meter": "workspace:^", + "@deepseek-ai/dsh-tool-todo": "workspace:^", + "@deepseek-ai/dsh-tools": "workspace:^", + "@deepseek-ai/dsh-util-crypto": "workspace:^" }, "devDependencies": { "@deepseek-ai/cordis": "workspace:^", @@ -86,10 +87,10 @@ "@deepseek-ai/dsh-client-locale": "workspace:^", "@deepseek-ai/dsh-client-runtime": "workspace:^", "@deepseek-ai/dsh-client-test-runtime": "workspace:^", + "@deepseek-ai/dsh-client-ui-input-trigger": "workspace:^", "@deepseek-ai/dsh-client-ui-layout": "workspace:^", "@deepseek-ai/dsh-client-ui-primitives": "workspace:^", "@deepseek-ai/dsh-client-ui-settings": "workspace:^", - "@deepseek-ai/dsh-client-ui-input-trigger": "workspace:^", "@deepseek-ai/dsh-client-ui-slots": "workspace:^", "@deepseek-ai/dsh-commands": "workspace:^", "@deepseek-ai/dsh-compaction": "workspace:^", @@ -100,12 +101,13 @@ "@deepseek-ai/dsh-plan-mode": "workspace:^", "@deepseek-ai/dsh-session-projection": "workspace:^", "@deepseek-ai/dsh-session-stats": "workspace:^", + "@deepseek-ai/dsh-settings": "workspace:^", "@deepseek-ai/dsh-token-meter": "workspace:^", "@deepseek-ai/dsh-tool-todo": "workspace:^", "@deepseek-ai/dsh-tools": "workspace:^", + "@deepseek-ai/dsh-util-crypto": "workspace:^", "@types/react": "~18.3.1", - "react": "^18.2.0", - "@deepseek-ai/dsh-settings": "workspace:^" + "react": "^18.2.0" }, "files": [ "lib/index.js", diff --git a/packages/client/ui-conversation/src/client/service.ts b/packages/client/ui-conversation/src/client/service.ts index a37816472c..866ae53b61 100644 --- a/packages/client/ui-conversation/src/client/service.ts +++ b/packages/client/ui-conversation/src/client/service.ts @@ -9,6 +9,7 @@ */ import { Service } from '@deepseek-ai/cordis' import type { Context } from '@deepseek-ai/cordis' +import { randomUUID } from '@deepseek-ai/dsh-util-crypto' // Type-only imports: a plugin-to-plugin value import is a bundle purity // error, so scope resolution goes through the sessions service (scopeOf // method) instead of the standalone helper. @@ -63,7 +64,7 @@ export interface IConversation { function browserDraftAttachment(file: File): ComposerAttachment { return { kind: 'image', - id: crypto.randomUUID() as DraftAttachmentId, + id: randomUUID() as DraftAttachmentId, previewUrl: URL.createObjectURL(file), file, } diff --git a/packages/client/ui-conversation/tsconfig.json b/packages/client/ui-conversation/tsconfig.json index 97ce021339..e3dcebfad7 100644 --- a/packages/client/ui-conversation/tsconfig.json +++ b/packages/client/ui-conversation/tsconfig.json @@ -76,6 +76,9 @@ }, { "path": "../ui-settings" + }, + { + "path": "../../util/crypto" } ], "exclude": [ diff --git a/packages/experimental/webworker-runtime/package.json b/packages/experimental/webworker-runtime/package.json index 8cac2bd9f7..0d23e85c13 100644 --- a/packages/experimental/webworker-runtime/package.json +++ b/packages/experimental/webworker-runtime/package.json @@ -30,6 +30,7 @@ }, "license": "MIT", "dependencies": { + "@deepseek-ai/dsh-util-crypto": "workspace:^", "@noble/hashes": "^2.3.0", "@yarnpkg/parsers": "^3.1.0", "acorn": "^8.17.0", diff --git a/packages/experimental/webworker-runtime/src/node/builtin_modules/implemented/crypto.ts b/packages/experimental/webworker-runtime/src/node/builtin_modules/implemented/crypto.ts index 24b1c3be2a..15473542d4 100644 --- a/packages/experimental/webworker-runtime/src/node/builtin_modules/implemented/crypto.ts +++ b/packages/experimental/webworker-runtime/src/node/builtin_modules/implemented/crypto.ts @@ -5,6 +5,7 @@ */ import { sha1 } from '@noble/hashes/legacy.js' import { sha256, sha512 } from '@noble/hashes/sha2.js' +import { randomUUID as mintUUID } from '@deepseek-ai/dsh-util-crypto' import { Buffer } from 'buffer' type Hasher = (input: Uint8Array) => Uint8Array @@ -73,11 +74,13 @@ export function randomBytes(size: number): Buffer { } /** - * Random v4 UUID. + * Random v4 UUID. Delegated to the repository's own mint rather than to + * `crypto.randomUUID`, which browsers expose only in secure contexts — a + * preview served over plain HTTP on a LAN address has no `randomUUID`. * @returns the UUID string. */ export function randomUUID(): import('node:crypto').UUID { - return globalThis.crypto.randomUUID() + return mintUUID() } /** diff --git a/packages/experimental/webworker-runtime/src/node/builtin_modules/implemented/fs.ts b/packages/experimental/webworker-runtime/src/node/builtin_modules/implemented/fs.ts index 33737ed903..f5c4835cbd 100644 --- a/packages/experimental/webworker-runtime/src/node/builtin_modules/implemented/fs.ts +++ b/packages/experimental/webworker-runtime/src/node/builtin_modules/implemented/fs.ts @@ -200,7 +200,8 @@ export function mkdirSync(path: PathArg, options?: { recursive?: boolean }): str * @returns the created directory path. */ export function mkdtempSync(prefix: string): string { - const suffix = globalThis.crypto.randomUUID().replaceAll('-', '').slice(0, 6) + // Not crypto.randomUUID: browsers expose that only in secure contexts. + const suffix = Array.from(globalThis.crypto.getRandomValues(new Uint8Array(3)), byte => byte.toString(16).padStart(2, '0')).join('') const target = `${prefix}${suffix}` vfs().mkdirSync(target, { recursive: true }) return target diff --git a/packages/experimental/webworker-runtime/src/node/globals/crypto.ts b/packages/experimental/webworker-runtime/src/node/globals/crypto.ts new file mode 100644 index 0000000000..31235a0124 --- /dev/null +++ b/packages/experimental/webworker-runtime/src/node/globals/crypto.ts @@ -0,0 +1,19 @@ +/** + * Fill the `crypto.randomUUID` gap on insecure origins. Browsers expose + * `randomUUID` only in secure contexts, and a preview served over plain HTTP + * on a LAN address is not one — while product code (bundled and VFS-loaded + * alike) reaches the global directly, Node-style. The worker patches the one + * `crypto` instance instead of teaching every caller. + */ +import { randomUUID } from '@deepseek-ai/dsh-util-crypto' + +/** Install `crypto.randomUUID` when the context withholds it. */ +export function installCryptoGlobals(): void { + // In a secure context the platform method is present and stays untouched. + if (typeof globalThis.crypto.randomUUID === 'function') return + Object.defineProperty(globalThis.crypto, 'randomUUID', { + value: randomUUID, + configurable: true, + writable: true, + }) +} diff --git a/packages/experimental/webworker-runtime/src/worker.ts b/packages/experimental/webworker-runtime/src/worker.ts index 2940bd360b..f82c5fd1fb 100644 --- a/packages/experimental/webworker-runtime/src/worker.ts +++ b/packages/experimental/webworker-runtime/src/worker.ts @@ -21,12 +21,14 @@ import { createNodeBuiltins, REPLACED_PREFIXES } from './node/builtins.ts' import { whenRequestListener } from './node/builtin_modules/implemented/http.ts' import { installTimerGlobals } from './node/globals/timers.ts' import { installProcessGlobal } from './node/globals/process.ts' +import { installCryptoGlobals } from './node/globals/crypto.ts' import { isShellStartFrame } from './shell/process/protocol.ts' import { runShellProcess } from './shell/process/host.ts' // Before the timer globals, so the wrappers close over the patched platform. installAsyncContextHooks() installTimerGlobals() +installCryptoGlobals() let host: { handleMessage(data: unknown): void } | undefined let shellRole = false diff --git a/packages/experimental/webworker-runtime/tests/node/crypto-globals.spec.ts b/packages/experimental/webworker-runtime/tests/node/crypto-globals.spec.ts new file mode 100644 index 0000000000..cc46f2fa86 --- /dev/null +++ b/packages/experimental/webworker-runtime/tests/node/crypto-globals.spec.ts @@ -0,0 +1,36 @@ +/** + * The crypto global patch and the shim UUID it installs: on an insecure + * origin the platform withholds `crypto.randomUUID` while product code calls + * it off the global, so the worker fills the one missing method — and leaves + * a platform that already has it untouched. + */ +import { afterEach, describe, expect, it, vi } from 'vitest' +import { installCryptoGlobals } from '../../src/node/globals/crypto.ts' +import { randomUUID } from '../../src/node/builtin_modules/implemented/crypto.ts' + +const V4_SHAPE = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/ + +describe('the shim randomUUID', () => { + it('emits RFC 9562 v4 ids without touching the platform method', () => { + for (let round = 0; round < 32; round += 1) expect(randomUUID()).toMatch(V4_SHAPE) + expect(new Set(Array.from({ length: 32 }, () => randomUUID())).size).toBe(32) + }) +}) + +describe('installCryptoGlobals', () => { + afterEach(() => { vi.unstubAllGlobals() }) + + it('fills randomUUID on a crypto that lacks it, the insecure-origin shape', () => { + const bare = { getRandomValues: globalThis.crypto.getRandomValues.bind(globalThis.crypto) } + vi.stubGlobal('crypto', bare) + installCryptoGlobals() + expect((globalThis.crypto as Crypto).randomUUID()).toMatch(V4_SHAPE) + }) + + it('leaves a platform that already provides randomUUID untouched', () => { + const platform = (): string => 'platform-owned' + vi.stubGlobal('crypto', { randomUUID: platform }) + installCryptoGlobals() + expect(globalThis.crypto.randomUUID).toBe(platform) + }) +}) diff --git a/packages/experimental/webworker-runtime/tsconfig.json b/packages/experimental/webworker-runtime/tsconfig.json index be98b203dc..b2419b626a 100644 --- a/packages/experimental/webworker-runtime/tsconfig.json +++ b/packages/experimental/webworker-runtime/tsconfig.json @@ -31,6 +31,9 @@ }, { "path": "../../runtime-diagnostics/invariants" + }, + { + "path": "../../util/crypto" } ] } diff --git a/packages/host/apiproxy/package.json b/packages/host/apiproxy/package.json index 93a9e92a2c..272269eb99 100644 --- a/packages/host/apiproxy/package.json +++ b/packages/host/apiproxy/package.json @@ -45,15 +45,16 @@ ], "license": "MIT", "dependencies": { - "@deepseek-ai/dsh-attachment": "workspace:^", "@deepseek-ai/dsh-agent": "workspace:^", "@deepseek-ai/dsh-agent-default-model": "workspace:^", "@deepseek-ai/dsh-api-remotes": "workspace:^", + "@deepseek-ai/dsh-attachment": "workspace:^", "@deepseek-ai/dsh-brand": "workspace:^", "@deepseek-ai/dsh-commands": "workspace:^", "@deepseek-ai/dsh-credentials": "workspace:^", "@deepseek-ai/dsh-goal": "workspace:^", "@deepseek-ai/dsh-host-directory-picker": "workspace:^", + "@deepseek-ai/dsh-jobs": "workspace:^", "@deepseek-ai/dsh-llm": "workspace:^", "@deepseek-ai/dsh-native-command": "workspace:^", "@deepseek-ai/dsh-session": "workspace:^", @@ -65,10 +66,10 @@ "@deepseek-ai/dsh-settings": "workspace:^", "@deepseek-ai/dsh-skill": "workspace:^", "@deepseek-ai/dsh-subagent": "workspace:^", - "@deepseek-ai/dsh-jobs": "workspace:^", "@deepseek-ai/dsh-tools": "workspace:^", "@deepseek-ai/dsh-user-approval": "workspace:^", "@deepseek-ai/dsh-user-questions": "workspace:^", + "@deepseek-ai/dsh-util-crypto": "workspace:^", "@deepseek-ai/dsh-workspace": "workspace:^", "@deepseek-ai/schemastery": "workspace:^", "fflate": "^0.8.2", @@ -88,7 +89,6 @@ "@deepseek-ai/dsh-storage": "workspace:^", "@deepseek-ai/dsh-storage-domain": "workspace:^", "@deepseek-ai/dsh-typert-protocol": "workspace:^", - "@deepseek-ai/dsh-typert-registry": "workspace:^", - "@deepseek-ai/cordis": "workspace:^" + "@deepseek-ai/dsh-typert-registry": "workspace:^" } } diff --git a/packages/host/apiproxy/src/fetch/client.ts b/packages/host/apiproxy/src/fetch/client.ts index 70e3ece58f..002e2e9b09 100644 --- a/packages/host/apiproxy/src/fetch/client.ts +++ b/packages/host/apiproxy/src/fetch/client.ts @@ -6,6 +6,7 @@ */ import type { z } from 'zod' +import { randomUUID } from '@deepseek-ai/dsh-util-crypto' import type { ApiProxy, HostFrame, MuxFrame } from '../api/index.ts' import type { RequestPayload, ResponseValue, RpcMethodMap } from '../api/rpc-map.ts' import type { ClientRequest, ClientResponse, RpcMessage, RpcReceipt, RpcRequest, RpcResponse, ServerRequest } from '../api/rpc.ts' @@ -296,8 +297,9 @@ export abstract class AbstractApiClient implements IApiClient { } protected mintRpcId(): RpcId { - // crypto.randomUUID is a Web API (browser + Node ≥19): keeps this base platform-neutral. - return RpcId(crypto.randomUUID()) + // Not crypto.randomUUID: browsers withhold it outside secure contexts, + // and this base also mints on pages served over plain HTTP. + return RpcId(randomUUID()) } /** diff --git a/packages/host/apiproxy/tsconfig.json b/packages/host/apiproxy/tsconfig.json index 3ed38b3d77..4ae705f1fa 100644 --- a/packages/host/apiproxy/tsconfig.json +++ b/packages/host/apiproxy/tsconfig.json @@ -100,6 +100,9 @@ }, { "path": "../../util/native-command" + }, + { + "path": "../../util/crypto" } ] } diff --git a/packages/interaction/commands/package.json b/packages/interaction/commands/package.json index 935930386c..f5eebc76d1 100644 --- a/packages/interaction/commands/package.json +++ b/packages/interaction/commands/package.json @@ -64,6 +64,7 @@ "@deepseek-ai/cordis": "workspace:^" }, "dependencies": { + "@deepseek-ai/dsh-util-crypto": "workspace:^", "zod": "^4.4.3" }, "devDependencies": { diff --git a/packages/interaction/commands/src/index.ts b/packages/interaction/commands/src/index.ts index 9e078938ed..b0b3a0dd45 100644 --- a/packages/interaction/commands/src/index.ts +++ b/packages/interaction/commands/src/index.ts @@ -4,6 +4,7 @@ */ import { Context } from '@deepseek-ai/cordis' +import { randomUUID } from '@deepseek-ai/dsh-util-crypto' import type { Agent } from '@deepseek-ai/dsh-agent' import { AttachmentError, admitEncodedImages } from '@deepseek-ai/dsh-attachment' import type { EncodedImageAttachment } from '@deepseek-ai/dsh-attachment/types' @@ -256,7 +257,7 @@ export class CommandRuntime extends TypertRemoteService { /** Monotonic per-instance counter behind {@link mintCommandId}. */ private commandSeq = 0 /** Instance token keeping minted ids unique across process restarts over one resumed log. */ - private readonly instanceToken = crypto.randomUUID().slice(0, 8) + private readonly instanceToken = randomUUID().slice(0, 8) constructor(ctx: Context) { super(ctx, 'commands') diff --git a/packages/interaction/commands/tsconfig.json b/packages/interaction/commands/tsconfig.json index 7f7bfd9ac0..436f5c9472 100644 --- a/packages/interaction/commands/tsconfig.json +++ b/packages/interaction/commands/tsconfig.json @@ -37,6 +37,9 @@ }, { "path": "../../typert/protocol" + }, + { + "path": "../../util/crypto" } ] } diff --git a/packages/llm/llm/package.json b/packages/llm/llm/package.json index 6bd6fc2df7..cf032be43e 100644 --- a/packages/llm/llm/package.json +++ b/packages/llm/llm/package.json @@ -52,6 +52,7 @@ "@deepseek-ai/cordis": "workspace:^" }, "dependencies": { + "@deepseek-ai/dsh-util-crypto": "workspace:^", "@deepseek-ai/schemastery": "workspace:^" }, "devDependencies": { diff --git a/packages/llm/llm/src/message.ts b/packages/llm/llm/src/message.ts index 608b56475d..30df8157a8 100644 --- a/packages/llm/llm/src/message.ts +++ b/packages/llm/llm/src/message.ts @@ -1,5 +1,6 @@ /** Message value types, identity, and immutable construction helpers. */ +import { randomUUID } from '@deepseek-ai/dsh-util-crypto' import { MessageId, type CallId } from './brand.ts' import { deepFreeze } from './call-config.ts' import type { ContentBlock, StreamChunk, ToolResultBlock } from './types.ts' @@ -180,7 +181,7 @@ export function createMessage( ): T & Pick { return freezeMessage({ ...input, - id: MessageId(crypto.randomUUID()), + id: MessageId(randomUUID()), }) } diff --git a/packages/llm/llm/tsconfig.json b/packages/llm/llm/tsconfig.json index 21044b821b..2206a6028e 100644 --- a/packages/llm/llm/tsconfig.json +++ b/packages/llm/llm/tsconfig.json @@ -25,6 +25,9 @@ }, { "path": "../../util/timeout" + }, + { + "path": "../../util/crypto" } ] } diff --git a/packages/util/crypto/README.i18n.yaml b/packages/util/crypto/README.i18n.yaml new file mode 100644 index 0000000000..abc01b6fae --- /dev/null +++ b/packages/util/crypto/README.i18n.yaml @@ -0,0 +1,6 @@ +# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each +# side as of the last confirmed-consistent state. Both languages carry equal authority; +# after editing either side, bring the other along and re-record with: +# pnpm run verify-translation-pairing --write packages/util/crypto/README.md +README.md: 456f7b8dc9476b6e2dc4844964912410ec934c2d +README.zh.md: 9b9836b4bef528b3aeae5531ceba549b1963d4bd diff --git a/packages/util/crypto/README.md b/packages/util/crypto/README.md new file mode 100644 index 0000000000..456f7b8dc9 --- /dev/null +++ b/packages/util/crypto/README.md @@ -0,0 +1,31 @@ +# dsh-util-crypto + +English | [中文](README.zh.md) + +Zero-dependency v4 UUID minting over `crypto.getRandomValues` — the one random primitive every shipped context provides. `crypto.randomUUID` is a secure-context Web API: a page or worker served over plain HTTP on a LAN address (the browser preview deployment) has no such method, so code that must run there cannot call it. The repository-wide `no-restricted-properties` lint rule points `crypto.randomUUID` callers here; Node-only code importing `randomUUID` from `node:crypto` stays as it is. + +It is a **library, not a service or plugin**: no `ctx`, registers nothing, holds no state. + +## API + +```ts +import { randomUUID, type Uuid } from '@deepseek-ai/dsh-util-crypto' +``` + +| Export | Role | +|---|---| +| `randomUUID()` | Random RFC 9562 v4 UUID string, minted from `crypto.getRandomValues`. Drop-in for `crypto.randomUUID()`. | +| `Uuid` | The five-group UUID string type, matching `crypto.randomUUID`'s declared return shape. | + +## Model Experience + +None directly; consumers mint request, session, and attachment identifiers with it, none of which enter prompts as semantic content. + +#### KV Cache effect + +No direct invalidation; identifier-minting consumers own any request changes. + +## Known Limitations and Deferred Work + +- **v4 only** — no other UUID versions, namespaces, or parsing; consumers needing more should take a real UUID dependency. +- **Uniqueness is probabilistic** — 122 random bits, the same guarantee `crypto.randomUUID` gives; nothing here detects collisions. diff --git a/packages/util/crypto/README.zh.md b/packages/util/crypto/README.zh.md new file mode 100644 index 0000000000..9b9836b4be --- /dev/null +++ b/packages/util/crypto/README.zh.md @@ -0,0 +1,31 @@ +# dsh-util-crypto + +[English](README.md) | 中文 + +零依赖的 v4 UUID 铸造,基于 `crypto.getRandomValues`——所有发布上下文都提供的那个随机原语。`crypto.randomUUID` 是安全上下文限定的 Web API:经普通 HTTP 在局域网地址上提供的页面或 worker(浏览器预览部署)根本没有这个方法,必须在那里运行的代码不能调它。全仓 `no-restricted-properties` lint 规则把 `crypto.randomUUID` 的调用者指到这里;只跑在 Node 的代码从 `node:crypto` 导入 `randomUUID` 维持原样。 + +它是**库,不是服务也不是插件**:无 `ctx`、不注册任何东西、不持有状态。 + +## API + +```ts +import { randomUUID, type Uuid } from '@deepseek-ai/dsh-util-crypto' +``` + +| 导出 | 角色 | +|---|---| +| `randomUUID()` | 随机 RFC 9562 v4 UUID 字符串,由 `crypto.getRandomValues` 铸造。可原位替换 `crypto.randomUUID()`。 | +| `Uuid` | 五段式 UUID 字符串类型,与 `crypto.randomUUID` 声明的返回形状一致。 | + +## Model Experience + +无直接影响;消费方用它铸造请求、会话与附件标识符,均不作为语义内容进入提示词。 + +#### KV Cache effect + +无直接失效;铸造标识符的消费方自行负责其请求变化。 + +## Known Limitations and Deferred Work + +- **仅 v4**——不提供其他 UUID 版本、命名空间或解析;需要更多能力的消费方应引入真正的 UUID 依赖。 +- **唯一性是概率性的**——122 位随机,与 `crypto.randomUUID` 同级保证;此处不做碰撞检测。 diff --git a/packages/util/crypto/package.json b/packages/util/crypto/package.json new file mode 100644 index 0000000000..46ccd8a6b7 --- /dev/null +++ b/packages/util/crypto/package.json @@ -0,0 +1,42 @@ +{ + "name": "@deepseek-ai/dsh-util-crypto", + "description": "Zero-dependency crypto-adjacent helpers usable in every context (browser, worker, Node) including insecure origins: v4 UUID minting over crypto.getRandomValues today, room for siblings", + "version": "0.1.0-rc.8", + "publishConfig": { + "access": "public" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/deepseek-ai/deepseek-harness.git", + "directory": "packages/util/crypto" + }, + "type": "module", + "main": "lib/index.js", + "types": "lib/types/index.d.ts", + "exports": { + ".": { + "types": "./lib/types/index.d.ts", + "default": "./lib/index.js" + }, + "./invariant": { + "types": "./lib/types/invariant.d.ts", + "default": "./lib/invariant.js" + }, + "./src/*": "./src/*", + "./package.json": "./package.json" + }, + "files": [ + "lib/index.js", + "lib/invariant.js", + "lib/types/**/*.d.ts" + ], + "license": "MIT", + "peerDependencies": { + "@deepseek-ai/dsh-invariants": "workspace:^", + "@deepseek-ai/cordis": "workspace:^" + }, + "devDependencies": { + "@deepseek-ai/dsh-invariants": "workspace:^", + "@deepseek-ai/cordis": "workspace:^" + } +} diff --git a/packages/util/crypto/src/index.ts b/packages/util/crypto/src/index.ts new file mode 100644 index 0000000000..13e66c357c --- /dev/null +++ b/packages/util/crypto/src/index.ts @@ -0,0 +1,26 @@ +/** + * UUID minting that works in every JavaScript context this repository ships + * to. `crypto.randomUUID` is a secure-context Web API — a page or worker + * served over plain HTTP on a LAN address has no such method — while + * `crypto.getRandomValues` is unrestricted everywhere (browsers, workers, + * Node ≥ 19). One implementation here replaces per-caller polyfills; the + * `no-restricted-properties` lint rule points `crypto.randomUUID` callers at + * this module. + * @module @deepseek-ai/dsh-util-crypto + */ + +/** RFC 9562 UUID string, the shape `crypto.randomUUID` declares. */ +export type Uuid = `${string}-${string}-${string}-${string}-${string}` + +/** + * Random v4 UUID, minted from `crypto.getRandomValues`. + * @returns the UUID string. + */ +export function randomUUID(): Uuid { + const bytes = globalThis.crypto.getRandomValues(new Uint8Array(16)) + // RFC 9562 §5.4: version 4 in the high nibble of byte 6, variant 10 in byte 8. + bytes[6] = ((bytes[6] ?? 0) & 0x0f) | 0x40 + bytes[8] = ((bytes[8] ?? 0) & 0x3f) | 0x80 + const hex = Array.from(bytes, byte => byte.toString(16).padStart(2, '0')).join('') + return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}` +} diff --git a/packages/util/crypto/src/invariant.ts b/packages/util/crypto/src/invariant.ts new file mode 100644 index 0000000000..e889ba1023 --- /dev/null +++ b/packages/util/crypto/src/invariant.ts @@ -0,0 +1,30 @@ +/** + * Package-owned invariant companion for `@deepseek-ai/dsh-util-crypto`. + * @module @deepseek-ai/dsh-util-crypto/invariant + */ + +/* jscpd:ignore-start */ +import type { Context } from '@deepseek-ai/cordis' +import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants' + +const PACKAGE_NAME = '@deepseek-ai/dsh-util-crypto' + +/** Cordis companion plugin name. */ +export const name = 'util-crypto-invariant' +/** Service required before the companion can reserve package ownership. */ +export const inject = ['invariants'] + +/** + * No runtime invariant: this pure utility owns no event stream or mutable runtime data; its value + * algebra is enforced by unit tests. + */ +const install: InvariantInstaller = () => {} + +/** + * Register this package's invariant companion. + * @param ctx - Cordis context carrying the invariant service. + * @returns the installed registration's disposer after setup succeeds. + */ +export const apply = (ctx: Context): Promise<() => void> => + Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install)) +/* jscpd:ignore-end */ diff --git a/packages/util/crypto/tests/uuid.spec.ts b/packages/util/crypto/tests/uuid.spec.ts new file mode 100644 index 0000000000..9a2f261648 --- /dev/null +++ b/packages/util/crypto/tests/uuid.spec.ts @@ -0,0 +1,29 @@ +/** + * The minted UUID's contract: RFC 9562 v4 shape (version and variant bits + * pinned), uniqueness across calls, and no dependence on the secure-context + * `crypto.randomUUID` — the reason this package exists. + */ +import { describe, expect, it, vi } from 'vitest' +import { randomUUID } from '../src/index.ts' + +const V4_SHAPE = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/ + +describe('randomUUID', () => { + it('emits RFC 9562 v4 ids', () => { + for (let round = 0; round < 64; round += 1) expect(randomUUID()).toMatch(V4_SHAPE) + }) + + it('emits distinct ids across calls', () => { + expect(new Set(Array.from({ length: 64 }, () => randomUUID())).size).toBe(64) + }) + + it('mints without the platform randomUUID, the insecure-origin shape', () => { + const bare = { getRandomValues: globalThis.crypto.getRandomValues.bind(globalThis.crypto) } + vi.stubGlobal('crypto', bare) + try { + expect(randomUUID()).toMatch(V4_SHAPE) + } finally { + vi.unstubAllGlobals() + } + }) +}) diff --git a/packages/util/crypto/tsconfig.json b/packages/util/crypto/tsconfig.json new file mode 100644 index 0000000000..779effc3cc --- /dev/null +++ b/packages/util/crypto/tsconfig.json @@ -0,0 +1,15 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "rootDir": "src", + "outDir": "lib/types" + }, + "include": [ + "src" + ], + "references": [ + { + "path": "../../runtime-diagnostics/invariants" + } + ] +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 13ba79ef9a..c7d8429289 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1864,6 +1864,9 @@ importers: packages/client/ui-conversation: dependencies: + '@deepseek-ai/dsh-util-crypto': + specifier: workspace:^ + version: link:../../util/crypto '@deepseek-ai/schemastery': specifier: link:../../../vendor/schemastery version: link:../../../vendor/schemastery @@ -4251,6 +4254,9 @@ importers: packages/experimental/webworker-runtime: dependencies: + '@deepseek-ai/dsh-util-crypto': + specifier: workspace:^ + version: link:../../util/crypto '@noble/hashes': specifier: ^2.3.0 version: 2.3.0 @@ -5127,6 +5133,9 @@ importers: '@deepseek-ai/dsh-user-questions': specifier: workspace:^ version: link:../../interaction/user-questions + '@deepseek-ai/dsh-util-crypto': + specifier: workspace:^ + version: link:../../util/crypto '@deepseek-ai/dsh-workspace': specifier: workspace:^ version: link:../../workspace/workspace @@ -5316,6 +5325,9 @@ importers: packages/interaction/commands: dependencies: + '@deepseek-ai/dsh-util-crypto': + specifier: workspace:^ + version: link:../../util/crypto zod: specifier: ^4.4.3 version: 4.4.3 @@ -5552,6 +5564,9 @@ importers: packages/llm/llm: dependencies: + '@deepseek-ai/dsh-util-crypto': + specifier: workspace:^ + version: link:../../util/crypto '@deepseek-ai/schemastery': specifier: link:../../../vendor/schemastery version: link:../../../vendor/schemastery @@ -8374,6 +8389,15 @@ importers: specifier: workspace:^ version: link:../../runtime-diagnostics/invariants + packages/util/crypto: + devDependencies: + '@deepseek-ai/cordis': + specifier: workspace:^ + version: link:../../../vendor/cordis + '@deepseek-ai/dsh-invariants': + specifier: workspace:^ + version: link:../../runtime-diagnostics/invariants + packages/util/home-paths: devDependencies: '@deepseek-ai/cordis': diff --git a/scripts/lint-rule-fingerprint.spec.ts b/scripts/lint-rule-fingerprint.spec.ts index ffe079e5ab..4aecd9e985 100644 --- a/scripts/lint-rule-fingerprint.spec.ts +++ b/scripts/lint-rule-fingerprint.spec.ts @@ -18,19 +18,19 @@ interface Profile { // snapshot; they do not re-evaluate that deleted baseline or track its preset. const profiles = { source: { - count: 88, + count: 89, indexes: [0, 1, 4, 5], - sha256: 'da1dfd77cb6eb66be93d8d3820f9b9b68b7aa391c24680f8851c0910298f9e3b', + sha256: '3e8b8455827254d6da59afb803d30f062b9fe1264b4df75ce4c8cd6e742b93dd', }, example: { - count: 87, + count: 88, indexes: [0, 1, 2, 4, 5], - sha256: '6a2606053bc1ec1de3b02611de88ea51d201dac13a1f193e4934d33c08b95f08', + sha256: 'e2b944c028ef1971302d47b2eb0e95c05e740623c7cbaebb4b1312dbf73bf156', }, test: { - count: 83, + count: 84, indexes: [0, 3, 4, 5], - sha256: '7995e14926a36c40bd65c474637735222a95fb030395681685f03060e50a7b78', + sha256: '7800bf57f0d689a5623e4ad60315b10173158fb9cf988d3f005cb115f2245691', }, } as const satisfies Record diff --git a/tsconfig.base.json b/tsconfig.base.json index da071a9409..8712442c46 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -106,6 +106,7 @@ "@deepseek-ai/dsh-experimental-tool-agent-team/invariant": ["./packages/experimental/tool-agent-team/src/invariant.ts"], "@deepseek-ai/dsh-experimental-webworker-runtime/invariant": ["./packages/experimental/webworker-runtime/src/invariant.ts"], "@deepseek-ai/dsh-experimental-webworker-packer/invariant": ["./packages/experimental/webworker-packer/src/invariant.ts"], + "@deepseek-ai/dsh-util-crypto/invariant": ["./packages/util/crypto/src/invariant.ts"], "@deepseek-ai/dsh-*/invariant": [ "./packages/core/*/src/invariant.ts", "./packages/prompt/*/src/invariant.ts", @@ -241,6 +242,7 @@ "@deepseek-ai/dsh-experimental-tool-agent-team": ["./packages/experimental/tool-agent-team/src"], "@deepseek-ai/dsh-experimental-webworker-runtime": ["./packages/experimental/webworker-runtime/src"], "@deepseek-ai/dsh-experimental-webworker-packer": ["./packages/experimental/webworker-packer/src"], + "@deepseek-ai/dsh-util-crypto": ["./packages/util/crypto/src"], "@deepseek-ai/dsh-*": [ "./packages/core/*/src", "./packages/prompt/*/src", diff --git a/tsconfig.host.json b/tsconfig.host.json index 687a5fda1b..22274b6471 100644 --- a/tsconfig.host.json +++ b/tsconfig.host.json @@ -129,6 +129,7 @@ { "path": "./packages/util/native-command" }, { "path": "./packages/util/home-paths" }, { "path": "./packages/util/timeout" }, + { "path": "./packages/util/crypto" }, { "path": "./packages/util/output-retention" }, { "path": "./packages/util/atomic-write" }, { "path": "./packages/attachment/attachment" },