fix(typert): validate and mount remote contributions safely
This commit is contained in:
@@ -2810,7 +2810,9 @@ function stringLiteralValue(node: ts.Node | undefined): string | undefined {
|
||||
}
|
||||
|
||||
function isRemoteSegment(value: string): boolean {
|
||||
return /^[A-Za-z0-9_$.-]+$/.test(value)
|
||||
// Generation bootstraps workspace artifacts before dsh-type-meta is built,
|
||||
// so this extraction-only copy must mirror isTypeRTRemoteSegment().
|
||||
return value !== '.' && value !== '..' && /^[A-Za-z0-9_$.-]+$/.test(value)
|
||||
}
|
||||
|
||||
function expressionName(node: ts.Expression): string | undefined {
|
||||
|
||||
@@ -90,6 +90,7 @@ export class WorkspaceTypertGenerator {
|
||||
throw new TypertAnalysisError(`typert(${artifact.face}): ${artifact.package} package files must include ${file}`)
|
||||
}
|
||||
}
|
||||
if (artifact.face !== 'host') return
|
||||
const remoteExpected = {
|
||||
types: './lib/typert.remote-client.d.ts',
|
||||
default: './lib/typert.remote-client.js',
|
||||
|
||||
@@ -274,7 +274,7 @@ export interface BoxPayload {
|
||||
assertRemoteConsumerTypechecks(artifact?.remote?.dts, artifact?.remote?.dtsMap, root)
|
||||
})
|
||||
|
||||
it.each(['create#v2', 'create goal'])('rejects untransportable Remote alias %s', (alias) => {
|
||||
it.each(['create#v2', 'create goal', '.', '..'])('rejects untransportable Remote alias %s', (alias) => {
|
||||
const root = copyFixture()
|
||||
editFile(root, 'packages/remote/src/index.ts', source => source.replace(
|
||||
' @Remote\n async create(',
|
||||
@@ -301,6 +301,37 @@ export interface RemainingSchema {
|
||||
.toThrow('publishes Remote artifacts but has no Remote methods')
|
||||
})
|
||||
|
||||
it('validates Remote artifacts only on the host face of a dual-face package', () => {
|
||||
const root = copyFixture()
|
||||
const manifestPath = join(root, 'packages/remote/package.json')
|
||||
const manifest = JSON.parse(readFileSync(manifestPath, 'utf8')) as {
|
||||
dshClient?: object
|
||||
exports: Record<string, unknown>
|
||||
files: string[]
|
||||
}
|
||||
manifest.dshClient = {}
|
||||
manifest.exports['./client'] = './src/client.ts'
|
||||
manifest.exports['./client/typert'] = {
|
||||
types: './lib/typert.client.d.ts',
|
||||
default: './lib/typert.client.js',
|
||||
}
|
||||
manifest.files.push('lib/typert.client.js', 'lib/typert.client.d.ts')
|
||||
writeFileSync(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`)
|
||||
writeFileSync(join(root, 'tsconfig.client.json'), `${JSON.stringify({
|
||||
extends: './tsconfig.base.json',
|
||||
files: [],
|
||||
references: [{ path: './packages/remote' }],
|
||||
}, null, 2)}\n`)
|
||||
writeFileSync(join(root, 'packages/remote/src/client.ts'), `/** @typert schema */
|
||||
export interface ClientMarker {
|
||||
readonly ready: boolean
|
||||
}
|
||||
`)
|
||||
|
||||
expect(new WorkspaceTypertGenerator(root).generate().map(artifact => artifact.face))
|
||||
.toEqual(['host', 'client'])
|
||||
})
|
||||
|
||||
it.each([
|
||||
{
|
||||
name: 'missing binding',
|
||||
|
||||
@@ -600,7 +600,7 @@ function validateCodec(codec: InvocationDescriptor['result'], subject: string):
|
||||
}
|
||||
|
||||
function validateWireName(subject: string, value: string): void {
|
||||
if (!/^[A-Za-z0-9_$.-]+$/.test(value)) {
|
||||
if (value === '.' || value === '..' || !/^[A-Za-z0-9_$.-]+$/.test(value)) {
|
||||
throw new Error(`typert: invalid ${subject} "${value}" — must contain only RPC endpoint segment characters`)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -247,7 +247,7 @@ describe('TypertRegistry', () => {
|
||||
})).toThrow('endpoint "goals/create" is already registered')
|
||||
})
|
||||
|
||||
it.each(['create#v2', 'create goal'])('rejects untransportable invocation method %s', async (method) => {
|
||||
it.each(['create#v2', 'create goal', '.', '..'])('rejects untransportable invocation method %s', async (method) => {
|
||||
const ctx = await makeCtx()
|
||||
expect(() => ctx.typert.remotes.register({
|
||||
package: '@fixture/invalid-endpoint',
|
||||
|
||||
@@ -15,7 +15,7 @@ const TYPERT_REMOTE_SEGMENT_PATTERN = /^[A-Za-z0-9_$.-]+$/
|
||||
* @returns whether the value can cross the shared RPC carrier unchanged.
|
||||
*/
|
||||
export function isTypeRTRemoteSegment(value: string): boolean {
|
||||
return TYPERT_REMOTE_SEGMENT_PATTERN.test(value)
|
||||
return value !== '.' && value !== '..' && TYPERT_REMOTE_SEGMENT_PATTERN.test(value)
|
||||
}
|
||||
|
||||
export type {
|
||||
|
||||
@@ -164,6 +164,8 @@ describe('type-meta Remote declarations', () => {
|
||||
expect(() => Remote('bad/name')).toThrow('export name')
|
||||
expect(() => Remote('bad#name')).toThrow('export name')
|
||||
expect(() => Remote('bad name')).toThrow('export name')
|
||||
expect(() => Remote('.')).toThrow('export name')
|
||||
expect(() => Remote('..')).toThrow('export name')
|
||||
expect(() => RemoteContext('' as 'metaFixture')).toThrow('Context key')
|
||||
expect(() => RemoteContext('metaFixture', 'bad/name')).toThrow('export name')
|
||||
|
||||
|
||||
Reference in New Issue
Block a user