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
47 changes: 47 additions & 0 deletions packages/insomnia/src/common/plugins/permissions-schema.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import Ajv from 'ajv';
import { describe, expect, it } from 'vitest';

import { parsePluginPermissions } from './permissions';
import { PLUGIN_MANIFEST_SCHEMA_VERSION, PLUGIN_PERMISSIONS_SCHEMA } from './permissions-schema';

const ajv = new Ajv({ allErrors: true });
const validate = ajv.compile(PLUGIN_PERMISSIONS_SCHEMA);

// Each case is a value for `insomnia.permissions` that is PRESENT (absence is the separate "baseline"
// path the parser handles, not something this schema describes). `clean` = the parser accepts it with
// zero warnings; that must equal schema validity.
const presentPermissionsCases: { name: string; permissions: unknown; clean: boolean }[] = [
{
name: 'valid modules + capabilities',
permissions: { modules: ['events', 'crypto'], capabilities: ['storage'] },
clean: true,
},
{ name: 'declared but empty', permissions: {}, clean: true },
{ name: 'duplicate entries (deduped, allowed)', permissions: { modules: ['events', 'events', 'path'] }, clean: true },
{ name: 'unknown module name (grant != availability)', permissions: { modules: ['left-pad'] }, clean: true },
{ name: 'unknown key inside permissions (ignored)', permissions: { modules: ['events'], extra: true }, clean: true },
{ name: 'non-array axis', permissions: { modules: 'events' }, clean: false },
{ name: 'non-string / empty entries', permissions: { modules: ['events', 123, ''] }, clean: false },
{ name: 'empty-string entry', permissions: { modules: [''] }, clean: false },
{ name: 'non-string capability entry', permissions: { capabilities: ['storage', 3] }, clean: false },
{ name: 'permissions is a string', permissions: 'nope', clean: false },
{ name: 'permissions is an array', permissions: [], clean: false },
{ name: 'permissions is null', permissions: null, clean: false },
];

describe('PLUGIN_PERMISSIONS_SCHEMA', () => {
it('is a compilable JSON Schema and is version-stamped', () => {
expect(typeof validate).toBe('function');
expect(PLUGIN_MANIFEST_SCHEMA_VERSION).toBeGreaterThanOrEqual(1);
expect(PLUGIN_PERMISSIONS_SCHEMA.$id).toContain(`v${PLUGIN_MANIFEST_SCHEMA_VERSION}`);
});

// The lockstep invariant: the formal schema and the lenient runtime parser must agree on exactly
// which present `permissions` blocks are clean. If someone changes one without the other, this fails.
it.each(presentPermissionsCases)('schema validity matches parser cleanliness: $name', ({ permissions, clean }) => {
const schemaValid = validate(permissions);
const parserClean = parsePluginPermissions({ permissions }).warnings.length === 0;
expect(parserClean, 'parser cleanliness should match the case table').toBe(clean);
expect(schemaValid, 'schema validity should match parser cleanliness').toBe(clean);
});
});
42 changes: 42 additions & 0 deletions packages/insomnia/src/common/plugins/permissions-schema.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
/**
* P1-B: the versioned, canonical JSON Schema for a plugin's `insomnia.permissions` manifest block.
*
* `parsePluginPermissions` (permissions.ts) is the *lenient runtime* validator — it never throws, and
* degrades a malformed manifest to baseline access with a human-readable warning. This schema is the
* *formal contract* that same validator enforces: a manifest is "clean" (parses with zero warnings)
* exactly when it validates against this schema. It's exported so plugin authors, editor tooling, and
* docs can reference one source of truth, and it's version-stamped so the contract can evolve
* deliberately. `permissions-schema.test.ts` locks the parser and this schema in lockstep so they
* can't drift.
*
* Note the deliberate leniencies that match the parser (and are therefore NOT schema violations):
* - unknown module/capability *names* are allowed (grant vs. availability are separate concerns);
* - duplicate entries are allowed (the parser de-duplicates silently);
* - unknown keys inside `permissions` are ignored (no `additionalProperties: false`).
*/

/** Bump when the manifest contract changes in a way authors must react to. */
export const PLUGIN_MANIFEST_SCHEMA_VERSION = 1;

/** A non-empty-string array axis (`modules` / `capabilities`). */
const stringArrayAxis = {
type: 'array',
items: { type: 'string', minLength: 1 },
} as const;

/**
* JSON Schema (draft-07) for the value of `insomnia.permissions`. Validate the `permissions` object
* itself against this (not the whole `insomnia` block).
*/
export const PLUGIN_PERMISSIONS_SCHEMA = {
$schema: 'http://json-schema.org/draft-07/schema#',
$id: `https://insomnia.rest/schemas/plugin-permissions/v${PLUGIN_MANIFEST_SCHEMA_VERSION}.json`,
title: 'Insomnia plugin permissions',
type: 'object',
properties: {
modules: stringArrayAxis,
capabilities: stringArrayAxis,
},
// Intentionally no `additionalProperties: false`, `uniqueItems`, or name enums — see the module
// docstring: the runtime parser tolerates those, so the formal contract must too.
} as const;
Loading