Skip to content
Draft
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
2 changes: 2 additions & 0 deletions packages/kernel-utils/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Added

- Add `pathUnder(segments)`, which builds an `@endo/patterns` pattern matching segment arrays under a prefix, with `..` excluded past the prefix so that traversal out of it is unrepresentable. Empty `segments` matches every `..`-free segment array ([#1049](https://github.com/MetaMask/ocap-kernel/pull/1049))
- Add `narrow({ name, base, delta })` and `join({ name, refs })`, plus the `NarrowingDelta`, `NarrowOptions`, and `JoinOptions` types, for deriving a capability that forwards to a base under a narrower interface guard. **Both functions throw for now** — this release fixes their signatures so callers and deltas can be written against them, and the implementations follow ([#1049](https://github.com/MetaMask/ocap-kernel/pull/1049))
- Add `getInterfaceMethodGuards`, `getMethodPayload`, `getGuardAt`, `buildMethodGuard`, and `asyncifyMethodGuards`, plus the `MethodGuardPayload` type, for reading an `@endo/patterns` interface guard by argument position — required arguments, then optionals, then the rest guard — and reassembling it ([#1048](https://github.com/MetaMask/ocap-kernel/pull/1048))
- Add `makeGuardedFetch` and the `FetchGuard` type, which wrap a `fetch` so that a guard runs before every request it makes, redirect hops included ([#1026](https://github.com/MetaMask/ocap-kernel/pull/1026))
- `redirect: 'follow'`, in the caller's `init` or on a `Request`, is overridden so that each hop can be checked; `manual` and `error` are honoured. `baseFetch` is therefore always called with `redirect: 'manual'` and must honour it
Expand Down
3 changes: 3 additions & 0 deletions packages/kernel-utils/src/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ describe('index', () => {
'isTypedArray',
'isTypedObject',
'isVatBundle',
'join',
'jsonSchemaToStruct',
'makeCounter',
'makeDefaultExo',
Expand All @@ -39,6 +40,8 @@ describe('index', () => {
'makeGuardedFetch',
'mergeDisjointRecords',
'methodArgsToStruct',
'narrow',
'pathUnder',
'prettifySmallcaps',
'resolveFetchInput',
'retry',
Expand Down
6 changes: 6 additions & 0 deletions packages/kernel-utils/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,12 @@ export {
getMethodPayload,
} from './guard-algebra.ts';
export type { MethodGuardPayload } from './guard-algebra.ts';
export { join, narrow, pathUnder } from './narrowing.ts';
export type {
JoinOptions,
NarrowOptions,
NarrowingDelta,
} from './narrowing.ts';
export { GET_DESCRIPTION, makeDiscoverableExo } from './discoverable.ts';
export type { DiscoverableExo } from './discoverable.ts';
export { S } from './described.ts';
Expand Down
56 changes: 56 additions & 0 deletions packages/kernel-utils/src/narrowing.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { matches } from '@endo/patterns';
import { describe, it, expect } from 'vitest';

import { join, narrow, pathUnder } from './narrowing.ts';

const makeBase = (): object => ({ readFile: () => 'contents' });

describe('narrow', () => {
it('is not implemented', async () => {
await expect(
narrow({ name: 'Scoped', base: makeBase(), delta: { readFile: [] } }),
).rejects.toThrow('narrow is not implemented');
});
});

describe('join', () => {
it('is not implemented', async () => {
await expect(
join({ name: 'Joined', refs: [makeBase(), makeBase()] }),
).rejects.toThrow('join is not implemented');
});
});

describe('pathUnder', () => {
it.each([
{ specimen: ['srv', 'data'], expected: true },
{ specimen: ['srv', 'data', 'x'], expected: true },
{ specimen: ['srv', 'data', 'a', 'b'], expected: true },
{ specimen: ['srv'], expected: false },
{ specimen: ['srv', 'logs'], expected: false },
{ specimen: ['var', 'data', 'x'], expected: false },
{ specimen: ['srv', 'data', '..', 'logs'], expected: false },
{ specimen: ['srv', 'data', 1], expected: false },
{ specimen: 'srv/data', expected: false },
])(
'matches $specimen under a prefix: $expected',
({ specimen, expected }) => {
expect(matches(specimen, pathUnder(['srv', 'data']))).toBe(expected);
},
);

it.each([
{ specimen: [], expected: true },
{ specimen: ['etc', 'passwd'], expected: true },
{ specimen: ['..'], expected: false },
])(
'matches $specimen under no prefix: $expected',
({ specimen, expected }) => {
expect(matches(specimen, pathUnder([]))).toBe(expected);
},
);

it('excludes .. only past the prefix', () => {
expect(matches(['..', 'etc'], pathUnder(['..']))).toBe(true);
});
});
80 changes: 80 additions & 0 deletions packages/kernel-utils/src/narrowing.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import type { Guarded, Methods } from '@endo/exo';
import { M } from '@endo/patterns';
import type { Pattern } from '@endo/patterns';

/**
* A narrowing, addressed by method name and then by argument position.
*
* A method absent from the delta is dropped from the narrowing. An `undefined`
* slot leaves that argument position as the base has it.
*/
export type NarrowingDelta = Record<string, (Pattern | undefined)[]>;

/**
* `base` is `object` rather than `Methods` because an `@endo/exo` carries no
* index signature and so does not satisfy `Methods`. A promise for a base is
* acceptable too, since the forward goes through `E()`.
*/
export type NarrowOptions = {
name: string;
base: object;
delta: NarrowingDelta;
};

export type JoinOptions = {
name: string;
refs: object[];
};

/**
* Not yet implemented; throws.
*
* Conjoin each pattern of `delta` onto the corresponding argument position of
* `base`'s interface guard, and return an exo under that derived guard whose
* methods forward to `base`.
*
* `Narrowed` describes the resulting method set, which is derived at runtime and
* so cannot be inferred; supply it to call the result through `E()`.
*
* @param _options - The narrowing to mint.
* @returns The narrowing of the base.
*/
export const narrow = async <Narrowed extends Methods = Methods>(
_options: NarrowOptions,
): Promise<Guarded<Narrowed>> => {
throw new Error('narrow is not implemented');
};

/**
* Not yet implemented; throws.
*
* Return a narrowing of `refs`' common base admitting exactly what any of them
* admits. Every ref must be one this module minted from that base, or the base
* itself.
*
* @param _options - The join to mint.
* @returns The join of the refs.
*/
export const join = async <Joined extends Methods = Methods>(
_options: JoinOptions,
): Promise<Guarded<Joined>> => {
throw new Error('join is not implemented');
};

/**
* Build a pattern matching segment arrays under a prefix, excluding `..` so that
* traversal out of the prefix is unrepresentable.
*
* Empty `segments` matches every `..`-free segment array, which is the top of
* the prefix lattice. A capability for which unbounded authority is a
* configuration mistake rejects it at its own config boundary, not here.
*
* @param segments - The prefix the matched arrays must start with.
* @returns A pattern over segment arrays.
*/
export const pathUnder = (segments: string[]): Pattern =>
M.splitArray(
segments.map((segment) => M.eq(segment)),
[],
M.arrayOf(M.and(M.string(), M.not(M.eq('..')))),
);
Loading