Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 28 additions & 8 deletions docs/package-distribution.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ integrations.
| `@contextualwisdomlab/cwl-editor/converter` | Framework-independent base64 and data-URI utilities |
| `@contextualwisdomlab/cwl-editor/envelope-identity` | Framework-independent identity-only envelope routing for bounded schema identity inspection; migration remains host-owned |
| `@contextualwisdomlab/cwl-editor/revision-evidence` | Framework-independent revision evidence and document-transition evidence for local content equality/lineage claims |
| `@contextualwisdomlab/cwl-editor/text-position-selector` | `implemented_on_active_pr` — React-free text-position projection core implementing W3C `TextPositionSelector`; interactive capture, revision binding, authorization, persistence, and re-anchoring remain outside this subpath |
| `@contextualwisdomlab/cwl-editor/styles.css` | Editor layout and theming |
| `@contextualwisdomlab/cwl-editor/fonts.css` | Full offline KR/EN/JP/SC/TC/VI font bundle |
| `@contextualwisdomlab/cwl-editor/fonts-latin.css` | Smaller Latin/Vietnamese font bundle |
Expand Down Expand Up @@ -55,11 +56,18 @@ embedded in the npm tarball.
and collaboration entrypoints. It is declared in Inkspan's package
dependencies so the consumer's package manager installs and resolves it; it
is not merely a type-only dependency.
- The framework-independent autosave, converter, envelope-identity, and
revision-evidence entrypoints do not require React UI, a mounted editor, naruon,
contextual-orchestrator, a database, provider credentials, or host transport.
Their individual package-consumer gates additionally prevent framework
dependencies from leaking into subpaths whose public contracts exclude them.
- The framework-independent autosave, converter, envelope-identity,
revision-evidence, and text-position-selector entrypoints do not require React
UI, a mounted editor, naruon, contextual-orchestrator, a database, provider
credentials, or host transport. Their individual package-consumer gates
additionally prevent framework dependencies from leaking into subpaths whose
public contracts exclude them.
- The text-position-selector subpath deliberately exposes only the deterministic
projection constants, error type, selector constructor, and public value
types. It does not expose the React imperative handle that captures editor
state or bind a selector to a document revision. Hosts remain responsible for
annotation identifiers/bodies, source-resource identity, authorization,
tenancy, persistence, audit, and cross-revision re-anchoring.
- Envelope identity output is routing metadata only. It does not accept an
unsupported document generation as current semantics and does not move schema
registry, migration, persistence, rollback, or authorization authority into
Expand All @@ -85,15 +93,27 @@ production library build. The verification chain:
3. confirms required licenses, declarations, styles, and font assets ship;
4. rejects internal source, tests, demos, Office files, coverage output, and
workflow files from the npm tarball;
5. imports the root, collaboration, converter, autosave, envelope-identity, and
revision-evidence surfaces through their dedicated packed-consumer checks,
including framework-free isolation where that is part of the public contract;
5. imports the root, collaboration, converter, autosave, envelope-identity,
revision-evidence, and text-position-selector surfaces through their dedicated
packed-consumer checks, including framework-free isolation where that is part
of the public contract;
6. exercises supported ESM/CommonJS entrypoints and compiles strict TypeScript
consumers against the published declaration surfaces;
7. resolves public CSS and font subpaths; and
8. fails when a declared public export is absent, mispackaged, or coupled to a
runtime graph that its public contract excludes.

The text-position-selector package check builds a real npm tarball, consumes the
public subpath through ESM and CommonJS, and compiles a strict TypeScript
consumer. Its emitted JavaScript rejects **any external runtime module import**,
so framework, network, database, credential-provider, and model-SDK clients
cannot enter the selector bundle through module dependencies. A separate check
rejects **ambient network and credential authority** such as `fetch`,
`XMLHttpRequest`, `WebSocket`, `EventSource`, `process.env`, `import.meta.env`,
`Deno.env`, and `Bun.env`. Type-only ProseMirror model/state inputs remain part
of the selector's structural contract and introduce no interactive runtime
authority.

A version is release-ready only when this package gate, repository-wide 100%
TypeScript coverage, production builds, the Python Office matrix, applicable
browser/document-fidelity evidence, and required security/review/release gates
Expand Down
9 changes: 7 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,11 @@
"import": "./dist/cwl-revision-evidence.js",
"require": "./dist/cwl-revision-evidence.cjs"
},
"./text-position-selector": {
"types": "./dist/text-position-selector/index.d.ts",
"import": "./dist/cwl-text-position-selector.js",
"require": "./dist/cwl-text-position-selector.cjs"
},
"./styles.css": "./dist/cwl-editor.css",
"./fonts.css": "./src/fonts/fonts.css",
"./fonts-latin.css": "./src/fonts/fonts-latin.css",
Expand All @@ -89,7 +94,7 @@
],
"scripts": {
"dev": "vite",
"build": "tsc --noEmit && vite build && vite build --config vite.collaboration.config.ts && vite build --config vite.converter.config.ts && vite build --config vite.envelope-identity.config.ts && vite build --config vite.revision-evidence.config.ts && vite build --config vite.autosave.config.ts && node ./scripts/copy-styles.mjs",
"build": "tsc --noEmit && vite build && vite build --config vite.collaboration.config.ts && vite build --config vite.converter.config.ts && vite build --config vite.envelope-identity.config.ts && vite build --config vite.revision-evidence.config.ts && vite build --config vite.autosave.config.ts && vite build --config vite.text-position-selector.config.ts && node ./scripts/copy-styles.mjs",
"build:demo": "vite build --config vite.demo.config.ts",
"fonts": "node ./scripts/fetch-fonts.mjs",
"preview": "vite preview",
Expand All @@ -98,7 +103,7 @@
"test:watch": "vitest",
"coverage": "vitest run --coverage",
"test:package-config": "node --test ./scripts/revision-evidence-consumer-config.test.mjs ./scripts/release-metadata.test.mjs",
"verify:package": "pnpm run test:package-config && node ./tests/package/verify-package.mjs && node ./scripts/verify-canonical-envelope-package.mjs && node ./scripts/verify-revision-evidence-package.mjs && node ./scripts/verify-framework-free-revision-evidence-package.mjs && node ./scripts/verify-framework-free-envelope-identity-package.mjs && node ./tests/package/verify-framework-free-autosave-package.mjs && node ./scripts/verify-text-position-selector-package.mjs"
"verify:package": "pnpm run test:package-config && node ./tests/package/verify-package.mjs && node ./scripts/verify-canonical-envelope-package.mjs && node ./scripts/verify-revision-evidence-package.mjs && node ./scripts/verify-framework-free-revision-evidence-package.mjs && node ./scripts/verify-framework-free-envelope-identity-package.mjs && node ./tests/package/verify-framework-free-autosave-package.mjs && node ./scripts/verify-text-position-selector-package.mjs && node ./scripts/verify-text-position-selector-subpath-package.mjs"
},
"peerDependencies": {
"react": "^18.0.0 || ^19.0.0",
Expand Down
219 changes: 219 additions & 0 deletions scripts/verify-text-position-selector-subpath-package.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,219 @@
import assert from 'node:assert/strict';
import { execFileSync } from 'node:child_process';
import {
existsSync,
mkdirSync,
mkdtempSync,
readFileSync,
renameSync,
rmSync,
symlinkSync,
writeFileSync,
} from 'node:fs';
import { tmpdir } from 'node:os';
import { dirname, join, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';

const repositoryRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..');
const packageJson = JSON.parse(
readFileSync(join(repositoryRoot, 'package.json'), 'utf8'),
);
const verificationRoot = mkdtempSync(
join(tmpdir(), 'inkspan-text-position-selector-'),
);
const extractionDirectory = join(verificationRoot, 'extracted');
const consumerDirectory = join(verificationRoot, 'consumer');
const packageDirectory = join(
consumerDirectory,
'node_modules',
...packageJson.name.split('/'),
);

// The selector bundle is intentionally self-contained. Type-only ProseMirror
// declarations are allowed, but emitted JavaScript must not acquire runtime
// authority through any external static/dynamic import, re-export, or require.
const externalRuntimeImportPattern =
/(?:\bimport\s*(?:\(\s*['"][^'"]+['"]\s*\)|(?:[^'"\n;]*?\sfrom\s*)?['"][^'"]+['"])|\bexport\s+[^'"\n;]*?\sfrom\s*['"][^'"]+['"]|\brequire\s*\(\s*['"][^'"]+['"]\s*\))/u;
Comment on lines +35 to +36

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major

문자열 리터럴이 아닌 동적 로더를 차단하십시오.

externalRuntimeImportPattern은 문자열 리터럴 인수를 사용하는 import("...")require("...")만 탐지합니다. import(specifier), import(\./${specifier}`), require(specifier)는 패턴을 통과합니다. 이러한 호출이 번들에 남아도 verifyAuthorityFreeBundles()`와 Line 215의 성공 메시지는 권한 경계가 충족되었다고 판단합니다.

모든 import()require() 호출을 거부하거나 JavaScript AST로 런타임 로더를 검사하십시오. 정적 import와 re-export의 여러 줄 형식도 처리하십시오.

#!/bin/bash
set -euo pipefail

node --input-type=module <<'NODE'
const pattern =
  /(?:\bimport\s*(?:\(\s*['"][^'"]+['"]\s*\)|(?:[^'"\n;]*?\sfrom\s*)?['"][^'"]+['"])|\bexport\s+[^'"\n;]*?\sfrom\s*['"][^'"]+['"]|\brequire\s*\(\s*['"][^'"]+['"]\s*\))/u;

for (const source of [
  'import(specifier)',
  'import(`./${specifier}`)',
  'require(specifier)',
]) {
  if (!pattern.test(source)) {
    throw new Error(`미탐지 동적 로더: ${source}`);
  }
}
NODE

Also applies to: 215-215

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/verify-text-position-selector-subpath-package.mjs` around lines 35 -
36, Update externalRuntimeImportPattern or replace it with AST-based validation
so every import() and require() call is rejected, including identifier and
template-literal arguments, while still detecting multiline static imports and
re-exports. Ensure verifyAuthorityFreeBundles() fails for these dynamic loaders
before reaching the success message at line 215.


// A self-contained bundle must also remain free of ambient network and common
// environment-backed credential authority even when no module import is needed.
const ambientAuthorityPattern =
/(?:\bfetch\s*\(|\bXMLHttpRequest\b|\bWebSocket\b|\bEventSource\b|\bprocess\.env\b|\bimport\.meta\.env\b|\bDeno\.env\b|\bBun\.env\b)/u;

/** Execute one deterministic package-consumer command. */
function run(command, argumentsList, cwd = repositoryRoot) {
return execFileSync(command, argumentsList, {
cwd,
encoding: 'utf8',
stdio: ['ignore', 'pipe', 'inherit'],
});
}

/** Build one real npm tarball and install its files without executing scripts. */
function preparePackage() {
mkdirSync(extractionDirectory, { recursive: true });
mkdirSync(dirname(packageDirectory), { recursive: true });
const packOutput = run('npm', [
'pack',
'--json',
'--ignore-scripts',
'--pack-destination',
verificationRoot,
]);
const packResult = JSON.parse(packOutput)[0];
assert.equal(packResult.name, packageJson.name);
assert.equal(packResult.version, packageJson.version);
const tarballPath = join(verificationRoot, packResult.filename);
assert.ok(existsSync(tarballPath));
run('tar', ['-xzf', tarballPath, '-C', extractionDirectory]);
renameSync(join(extractionDirectory, 'package'), packageDirectory);
writeFileSync(
join(consumerDirectory, 'package.json'),
'{"name":"inkspan-text-position-selector-consumer","private":true,"type":"module"}\n',
'utf8',
);

// The package declares @tiptap/pm as a normal dependency. The packed fixture is
// extracted without a package-manager install, so expose the already-frozen
// repository dependency only for strict declaration resolution.
const repositoryTiptap = join(repositoryRoot, 'node_modules', '@tiptap');
const consumerTiptap = join(consumerDirectory, 'node_modules', '@tiptap');
assert.ok(existsSync(repositoryTiptap));
symlinkSync(repositoryTiptap, consumerTiptap, 'dir');
}

/** Prove emitted JavaScript carries no external or ambient runtime authority. */
function verifyAuthorityFreeBundles() {
for (const filename of [
'cwl-text-position-selector.js',
'cwl-text-position-selector.cjs',
]) {
const bundlePath = join(packageDirectory, 'dist', filename);
const bundleSource = readFileSync(bundlePath, 'utf8');
assert.doesNotMatch(
bundleSource,
externalRuntimeImportPattern,
`${filename} must not import external runtime authority`,
);
assert.doesNotMatch(
bundleSource,
ambientAuthorityPattern,
`${filename} must not reference ambient network or credential authority`,
);
}
}

/** Exercise the exact public ESM and CommonJS subpath from the packed package. */
function verifyRuntimeConsumers() {
const esmPath = join(consumerDirectory, 'consumer.mjs');
writeFileSync(
esmPath,
`import assert from 'node:assert/strict';
import {
TEXT_POSITION_PROJECTION_ID,
TEXT_POSITION_PROJECTION_VERSION,
TextPositionSelectorEvidenceError,
createTextPositionSelector,
} from '${packageJson.name}/text-position-selector';
assert.equal(TEXT_POSITION_PROJECTION_ID, 'inkspan-prosemirror-text');
assert.equal(TEXT_POSITION_PROJECTION_VERSION, 1);
assert.equal(typeof TextPositionSelectorEvidenceError, 'function');
assert.equal(typeof createTextPositionSelector, 'function');
`,
'utf8',
);

const cjsPath = join(consumerDirectory, 'consumer.cjs');
writeFileSync(
cjsPath,
`const assert = require('node:assert/strict');
const selector = require('${packageJson.name}/text-position-selector');
assert.equal(selector.TEXT_POSITION_PROJECTION_ID, 'inkspan-prosemirror-text');
assert.equal(selector.TEXT_POSITION_PROJECTION_VERSION, 1);
assert.equal(typeof selector.TextPositionSelectorEvidenceError, 'function');
assert.equal(typeof selector.createTextPositionSelector, 'function');
`,
'utf8',
);

run(process.execPath, [esmPath], consumerDirectory);
run(process.execPath, [cjsPath], consumerDirectory);
}

/** Compile one strict TypeScript consumer against only the public subpath. */
function verifyDeclarationConsumer() {
const sourcePath = join(consumerDirectory, 'consumer.ts');
const configurationPath = join(consumerDirectory, 'tsconfig.json');
writeFileSync(
sourcePath,
`import {
TEXT_POSITION_PROJECTION_ID,
TEXT_POSITION_PROJECTION_VERSION,
TextPositionSelectorEvidenceError,
createTextPositionSelector,
type CwlEditorTextPositionSelector,
type CwlEditorTextProjectionIdentity,
type TextPositionSelectorEvidenceErrorCode,
} from '${packageJson.name}/text-position-selector';
import type { Node as ProseMirrorNode } from '@tiptap/pm/model';
import type { Selection } from '@tiptap/pm/state';
declare const documentNode: ProseMirrorNode;
declare const selection: Selection;
const result = createTextPositionSelector(documentNode, selection);
const selector: CwlEditorTextPositionSelector = result.selector;
const projection: CwlEditorTextProjectionIdentity = result.textProjection;
const code: TextPositionSelectorEvidenceErrorCode = 'segmenter_unavailable';
const failure = new TextPositionSelectorEvidenceError(code);
void [
selector.start,
selector.end,
projection.id === TEXT_POSITION_PROJECTION_ID,
projection.version === TEXT_POSITION_PROJECTION_VERSION,
failure.code,
];
`,
'utf8',
);
writeFileSync(
configurationPath,
`${JSON.stringify(
{
compilerOptions: {
noEmit: true,
strict: true,
skipLibCheck: false,
module: 'NodeNext',
moduleResolution: 'NodeNext',
target: 'ES2022',
lib: ['ES2022', 'DOM', 'DOM.Iterable'],
types: [],
},
files: ['./consumer.ts'],
},
null,
2,
)}\n`,
'utf8',
);
const compilerPath = join(
repositoryRoot,
'node_modules',
'typescript',
'bin',
'tsc',
);
assert.ok(existsSync(compilerPath));
run(process.execPath, [compilerPath, '--project', configurationPath], consumerDirectory);
}

try {
preparePackage();
verifyAuthorityFreeBundles();
verifyRuntimeConsumers();
verifyDeclarationConsumer();
console.log(
`Verified packed ${packageJson.name}/text-position-selector through authority-bounded ESM, CommonJS, and strict TypeScript consumers.`,
);
} finally {
rmSync(verificationRoot, { recursive: true, force: true });
}
18 changes: 18 additions & 0 deletions src/text-position-selector/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
/**
* React-free W3C text-position selector projection surface.
*
* This subpath exposes only deterministic projection primitives. Interactive
* editor-handle capture and exact revision binding remain on the root Inkspan
* editor contract.
*/
export {
TEXT_POSITION_PROJECTION_ID,
TEXT_POSITION_PROJECTION_VERSION,
TextPositionSelectorEvidenceError,
createTextPositionSelector,
} from '../textPositionSelectorEvidence.js';
export type {
CwlEditorTextPositionSelector,
CwlEditorTextProjectionIdentity,
TextPositionSelectorEvidenceErrorCode,
} from '../textPositionSelectorEvidence.js';
Loading
Loading