Skip to content

Permissions-Policy Builder — Zig source

Build a Permissions-Policy header interactively. Control which browser features (camera, microphone, geolocation, etc.) your site can use.

This is the Zig implementation — the same logic the interactive tool runs, in a shareable, citable form.

//! permissions-policy — Permissions-Policy header builder / parser.
//!
//! Language: Zig 0.13+ (standard library only)
//! Ported from: src/lib/permissions-policy.ts (the canonical TypeScript implementation).
//! display source — part of CosmoDev's polyglot tool pages.
//!
//! The Permissions-Policy header is a comma-separated list of directives:
//!   Permissions-Policy: geolocation=(self), camera=(), microphone=*, usb=(https://a.example)
//! Each directive maps a browser feature to an allowlist. An empty allowlist
//! `()` disables the feature outright; `*` allows it everywhere; `self` limits
//! it to the page's own origin; anything else is a space-separated origin list.
//!
//! A policy is modeled here as a map of feature -> allowlist slice:
//!   geolocation -> ["self"], camera -> [], usb -> ["https://a.example"]
//!   - []            => camera=()        (disabled)
//!   - ["*"]         => microphone=*     (every origin)
//!   - ["self"]      => geolocation=(self)
//!   - [origins...]  => usb=(https://a.example https://b.example)
//! Features absent from the map are absent from the header (browser default).
//!
//! The TS reference leans on regular expressions; Zig has no regex in the
//! standard library, so the two patterns are hand-rolled as predicates:
//!   FEATURE_NAME_RE  /^[a-z][a-z0-9-]*$/          -> isValidFeatureName
//!   ORIGIN_RE        /^(https?:\/\/|https?:)[\w.-]+(:\d+)?$/i -> isValidOrigin

const std = @import("std");

/// Whitespace set used for trimming. Mirrors what JS `String.prototype.trim`
/// strips for the inputs this tool sees.
const WS = " \t\r\n\x0b\x0c";

// MARK: - Types

pub const PrivacyImpact = enum {
    high,
    medium,
    low,

    /// Scoring weight: high 3, medium 2, low 1.
    pub fn weight(self: PrivacyImpact) f64 {
        return switch (self) {
            .high => 3,
            .medium => 2,
            .low => 1,
        };
    }
};

pub const FeatureInfo = struct {
    /// The directive token used in the header, e.g. `geolocation`.
    name: []const u8,
    description: []const u8,
    privacy_impact: PrivacyImpact,
    /// What browsers do when the feature is absent from the policy.
    default_browser_behavior: []const u8,
};

// MARK: - Feature catalog

pub const FEATURES = [_]FeatureInfo{
    // --- high privacy impact -------------------------------------------------
    .{ .name = "camera", .description = "Access the device camera for photos / video calls.", .privacy_impact = .high, .default_browser_behavior = "Same-origin only; prompts the user." },
    .{ .name = "microphone", .description = "Capture audio from the device microphone.", .privacy_impact = .high, .default_browser_behavior = "Same-origin only; prompts the user." },
    .{ .name = "geolocation", .description = "Read the precise GPS location of the visitor.", .privacy_impact = .high, .default_browser_behavior = "Same-origin only; prompts the user." },
    .{ .name = "display-capture", .description = "Screen / window sharing via getDisplayMedia.", .privacy_impact = .high, .default_browser_behavior = "Same-origin only; prompts the user." },
    .{ .name = "idle-detection", .description = "Detects when the user is away from the device — reveals usage patterns.", .privacy_impact = .high, .default_browser_behavior = "Disabled; prompts the user." },
    .{ .name = "serial", .description = "Talk to serial devices (Arduinos, POS terminals) over a physical port.", .privacy_impact = .high, .default_browser_behavior = "Disabled; prompts the user." },
    .{ .name = "usb", .description = "WebUSB — direct access to connected USB devices.", .privacy_impact = .high, .default_browser_behavior = "Disabled; prompts the user." },
    .{ .name = "hid", .description = "Human Interface Devices — raw access to unusual keyboards, gamepads, sensors.", .privacy_impact = .high, .default_browser_behavior = "Disabled; prompts the user." },
    .{ .name = "xr-spatial-tracking", .description = "Tracks head / hand position in WebXR sessions.", .privacy_impact = .high, .default_browser_behavior = "Same-origin only; prompts the user." },
    // --- medium privacy impact -----------------------------------------------
    .{ .name = "accelerometer", .description = "Device motion sensor — can fingerprint and infer behaviour.", .privacy_impact = .medium, .default_browser_behavior = "Same-origin only." },
    .{ .name = "ambient-light-sensor", .description = "Reads ambient light level around the device.", .privacy_impact = .medium, .default_browser_behavior = "Same-origin only." },
    .{ .name = "battery", .description = "Battery Status API — a classic fingerprinting vector.", .privacy_impact = .medium, .default_browser_behavior = "Same-origin only." },
    .{ .name = "gyroscope", .description = "Device orientation sensor — fingerprinting and behaviour inference.", .privacy_impact = .medium, .default_browser_behavior = "Same-origin only." },
    .{ .name = "magnetometer", .description = "Compass readings — can leak details of the user's surroundings.", .privacy_impact = .medium, .default_browser_behavior = "Same-origin only." },
    .{ .name = "keyboard-map", .description = "Reads the physical keyboard layout — a small but real fingerprint.", .privacy_impact = .medium, .default_browser_behavior = "Same-origin only." },
    .{ .name = "gamepad", .description = "Enumerates connected controllers and their button state.", .privacy_impact = .medium, .default_browser_behavior = "Same-origin only." },
    .{ .name = "midi", .description = "Web MIDI — access to attached music hardware.", .privacy_impact = .medium, .default_browser_behavior = "Same-origin only; prompts the user." },
    .{ .name = "payment", .description = "Payment Request API — can expose stored payment handles.", .privacy_impact = .medium, .default_browser_behavior = "Same-origin only." },
    .{ .name = "publickey-credentials-get", .description = "WebAuthn credential requests.", .privacy_impact = .medium, .default_browser_behavior = "Same-origin only." },
    .{ .name = "screen-wake-lock", .description = "Keeps the screen awake — drains battery and signals intent.", .privacy_impact = .medium, .default_browser_behavior = "Same-origin only." },
    .{ .name = "speaker-selection", .description = "Enumerates and switches audio output devices.", .privacy_impact = .medium, .default_browser_behavior = "Same-origin only." },
    .{ .name = "web-share", .description = "Invokes the OS share sheet with chosen content.", .privacy_impact = .medium, .default_browser_behavior = "Same-origin only." },
    .{ .name = "encrypted-media", .description = "DRM (EME) — playback identity can be correlated.", .privacy_impact = .medium, .default_browser_behavior = "Same-origin only." },
    .{ .name = "document-domain", .description = "Let frames relax the same-origin policy via document.domain.", .privacy_impact = .medium, .default_browser_behavior = "Allowed in same-origin pages; deprecated." },
    // --- low privacy impact --------------------------------------------------
    .{ .name = "autoplay", .description = "Autoplay media with/without sound — not a data leak, an annoyance knob.", .privacy_impact = .low, .default_browser_behavior = "Muted autoplay allowed; audible blocked." },
    .{ .name = "cross-origin-isolated", .description = "COOP/COEP isolation for SharedArrayBuffer — hardens the page.", .privacy_impact = .low, .default_browser_behavior = "Not isolated." },
    .{ .name = "fullscreen", .description = "Element.requestFullscreen().", .privacy_impact = .low, .default_browser_behavior = "Same-origin only." },
    .{ .name = "navigation-override", .description = "Lets a frame intercept its own top-level navigations.", .privacy_impact = .low, .default_browser_behavior = "Disabled." },
    .{ .name = "picture-in-picture", .description = "Floating always-on-top video window.", .privacy_impact = .low, .default_browser_behavior = "Same-origin only." },
};

/// Look up a feature in the catalog by directive token.
pub fn findFeature(name: []const u8) ?FeatureInfo {
    for (FEATURES) |feature| {
        if (std.mem.eql(u8, feature.name, name)) return feature;
    }
    return null;
}

// MARK: - Policy (feature -> allowlist)

/// A feature map. Backed by an insertion-ordered hash map so it behaves like
/// the plain JS object the TS reference uses: re-assigning a feature replaces
/// the previous allowlist rather than appending a second directive.
///
/// All keys and tokens are owned by an internal arena — `deinit` frees the lot.
pub const Policy = struct {
    arena: std.heap.ArenaAllocator,
    map: std.StringArrayHashMapUnmanaged([]const []const u8) = .{},

    pub fn init(child_allocator: std.mem.Allocator) Policy {
        return .{ .arena = std.heap.ArenaAllocator.init(child_allocator) };
    }

    pub fn deinit(self: *Policy) void {
        self.arena.deinit();
        self.* = undefined;
    }

    /// Copy `name` and `allowlist` into the policy. Replaces any existing entry.
    pub fn put(self: *Policy, name: []const u8, allowlist: []const []const u8) !void {
        const a = self.arena.allocator();
        const owned_name = try a.dupe(u8, name);
        const owned_tokens = try a.alloc([]const u8, allowlist.len);
        for (allowlist, 0..) |token, i| owned_tokens[i] = try a.dupe(u8, token);
        try self.map.put(a, owned_name, owned_tokens);
    }

    /// `null` when the feature is absent (browser default applies).
    /// An empty (non-null) slice means `feature=()` — explicitly disabled.
    pub fn get(self: Policy, name: []const u8) ?[]const []const u8 {
        return self.map.get(name);
    }

    pub fn count(self: Policy) usize {
        return self.map.count();
    }

    /// Feature names in insertion order.
    pub fn names(self: Policy) []const []const u8 {
        return self.map.keys();
    }
};

// MARK: - Token predicates (the hand-rolled regexes)

/// `/^[a-z][a-z0-9-]*$/` — a lowercase directive token.
pub fn isValidFeatureName(name: []const u8) bool {
    if (name.len == 0) return false;
    if (!std.ascii.isLower(name[0])) return false;
    for (name[1..]) |c| {
        if (!std.ascii.isLower(c) and !std.ascii.isDigit(c) and c != '-') return false;
    }
    return true;
}

fn isHostChar(c: u8) bool {
    // `\w` plus `.` and `-`; `\w` is [A-Za-z0-9_].
    return std.ascii.isAlphanumeric(c) or c == '_' or c == '.' or c == '-';
}

fn stripPrefixIgnoreCase(text: []const u8, prefix: []const u8) ?[]const u8 {
    if (text.len < prefix.len) return null;
    if (!std.ascii.eqlIgnoreCase(text[0..prefix.len], prefix)) return null;
    return text[prefix.len..];
}

/// `/^(https?:\/\/|https?:)[\w.-]+(:\d+)?$/i` — a scheme-qualified origin.
/// The alternation is ordered exactly as in the TS regex: the `//` forms are
/// tried first, so `https://a.example` consumes the slashes rather than
/// treating them as host characters.
pub fn isValidOrigin(token: []const u8) bool {
    const after_scheme =
        stripPrefixIgnoreCase(token, "https://") orelse
        stripPrefixIgnoreCase(token, "http://") orelse
        stripPrefixIgnoreCase(token, "https:") orelse
        stripPrefixIgnoreCase(token, "http:") orelse
        return false;

    var i: usize = 0;
    while (i < after_scheme.len and isHostChar(after_scheme[i])) : (i += 1) {}
    if (i == 0) return false; // `[\w.-]+` needs at least one character
    if (i == after_scheme.len) return true; // no port

    if (after_scheme[i] != ':') return false;
    i += 1;
    if (i == after_scheme.len) return false; // `:` with no digits
    while (i < after_scheme.len) : (i += 1) {
        if (!std.ascii.isDigit(after_scheme[i])) return false;
    }
    return true;
}

/// A `self` / `*` / origin allowlist token is valid; anything else is not.
fn isValidAllowlistToken(token: []const u8) bool {
    return std.mem.eql(u8, token, "self") or std.mem.eql(u8, token, "*") or isValidOrigin(token);
}

/// `/^"(.*)"$/` — unwrap a quoted origin, leaving anything else untouched.
fn unquote(token: []const u8) []const u8 {
    if (token.len >= 2 and token[0] == '"' and token[token.len - 1] == '"') {
        return token[1 .. token.len - 1];
    }
    return token;
}

fn trim(text: []const u8) []const u8 {
    return std.mem.trim(u8, text, WS);
}

/// Drop an optional `Permissions-Policy:` prefix — `/^permissions-policy\s*:\s*/i`.
fn stripHeaderName(raw: []const u8) []const u8 {
    const trimmed = trim(raw);
    const after_name = stripPrefixIgnoreCase(trimmed, "permissions-policy") orelse return trimmed;
    const after_ws = std.mem.trimLeft(u8, after_name, WS);
    if (after_ws.len == 0 or after_ws[0] != ':') return trimmed; // no colon — not the prefix
    return std.mem.trimLeft(u8, after_ws[1..], WS);
}

// MARK: - Build

/// Render one allowlist: `*` stays bare, everything else is parenthesised.
fn writeAllowlist(writer: anytype, tokens: []const []const u8) !void {
    if (tokens.len == 1 and std.mem.eql(u8, tokens[0], "*")) {
        try writer.writeAll("*");
        return;
    }
    try writer.writeByte('(');
    for (tokens, 0..) |token, i| {
        if (i > 0) try writer.writeByte(' ');
        try writer.writeAll(token);
    }
    try writer.writeByte(')');
}

fn lessThanAscii(_: void, a: []const u8, b: []const u8) bool {
    return std.mem.lessThan(u8, a, b);
}

/// Assemble a Permissions-Policy header value from a feature map.
/// Directives are emitted in sorted order, matching `Object.keys().sort()`.
/// Caller owns the returned slice.
pub fn buildPermissionsPolicy(allocator: std.mem.Allocator, policy: Policy) ![]u8 {
    const sorted = try allocator.dupe([]const u8, policy.names());
    defer allocator.free(sorted);
    std.mem.sort([]const u8, sorted, {}, lessThanAscii);

    var out = std.ArrayList(u8).init(allocator);
    errdefer out.deinit();
    const writer = out.writer();

    for (sorted, 0..) |name, i| {
        if (i > 0) try writer.writeAll(", ");
        try writer.writeAll(name);
        try writer.writeByte('=');
        try writeAllowlist(writer, policy.get(name).?);
    }
    return out.toOwnedSlice();
}

// MARK: - Parse

/// Parse a Permissions-Policy header value back into a feature map.
/// Accepts an optional `Permissions-Policy:` prefix. Returns `null` when the
/// syntax is malformed (bad directive, missing allowlist, bad token) — the
/// same contract as the TS reference returning `null`.
/// Caller owns the returned policy and must `deinit` it.
pub fn parsePermissionsPolicy(allocator: std.mem.Allocator, header: []const u8) !?Policy {
    const cleaned = stripHeaderName(header);
    if (cleaned.len == 0) return null;

    var policy = Policy.init(allocator);
    errdefer policy.deinit();

    var scratch = std.ArrayList([]const u8).init(allocator);
    defer scratch.deinit();

    var directives = std.mem.splitScalar(u8, cleaned, ',');
    while (directives.next()) |raw_directive| {
        const directive = trim(raw_directive);
        if (directive.len == 0) return null;

        const eq = std.mem.indexOfScalar(u8, directive, '=') orelse return null;
        if (eq == 0) return null; // `=allowlist` has no feature name

        const name = trim(directive[0..eq]);
        const value = trim(directive[eq + 1 ..]);
        if (!isValidFeatureName(name)) return null;

        if (std.mem.eql(u8, value, "*") or std.mem.eql(u8, value, "self")) {
            try policy.put(name, &[_][]const u8{value});
            continue;
        }

        if (value.len < 2 or value[0] != '(' or value[value.len - 1] != ')') return null;
        const inner = trim(value[1 .. value.len - 1]);
        if (inner.len == 0) {
            try policy.put(name, &[_][]const u8{}); // () = disabled
            continue;
        }

        scratch.clearRetainingCapacity();
        var tokens = std.mem.tokenizeAny(u8, inner, WS);
        while (tokens.next()) |raw_token| {
            const token = unquote(raw_token);
            if (!isValidAllowlistToken(token)) return null;
            try scratch.append(token);
        }
        try policy.put(name, scratch.items);
    }
    return policy;
}

// MARK: - Validate

pub const Validation = struct {
    allocator: std.mem.Allocator,
    errors: []const []const u8,

    pub fn valid(self: Validation) bool {
        return self.errors.len == 0;
    }

    pub fn deinit(self: *Validation) void {
        for (self.errors) |message| self.allocator.free(message);
        self.allocator.free(self.errors);
        self.* = undefined;
    }
};

/// Syntax-check a header the same way `parsePermissionsPolicy` does, but
/// collect human-readable messages instead of bailing out on the first fault.
/// Caller owns the returned validation and must `deinit` it.
pub fn validatePermissionsPolicy(allocator: std.mem.Allocator, header: []const u8) !Validation {
    var errors = std.ArrayList([]const u8).init(allocator);
    errdefer {
        for (errors.items) |message| allocator.free(message);
        errors.deinit();
    }

    const cleaned = stripHeaderName(header);
    if (cleaned.len == 0) {
        try errors.append(try allocator.dupe(u8, "Header is empty."));
        return .{ .allocator = allocator, .errors = try errors.toOwnedSlice() };
    }

    var index: usize = 0;
    var directives = std.mem.splitScalar(u8, cleaned, ',');
    while (directives.next()) |raw_directive| : (index += 1) {
        const directive = trim(raw_directive);
        const where = index + 1;

        if (directive.len == 0) {
            try errors.append(try std.fmt.allocPrint(allocator, "Directive {d}: empty (stray comma?).", .{where}));
            continue;
        }

        const eq = std.mem.indexOfScalar(u8, directive, '=') orelse {
            try errors.append(try std.fmt.allocPrint(allocator, "Directive {d}: expected `feature=allowlist`, got `{s}`.", .{ where, directive }));
            continue;
        };
        if (eq == 0) {
            try errors.append(try std.fmt.allocPrint(allocator, "Directive {d}: expected `feature=allowlist`, got `{s}`.", .{ where, directive }));
            continue;
        }

        const name = trim(directive[0..eq]);
        const value = trim(directive[eq + 1 ..]);

        if (!isValidFeatureName(name)) {
            try errors.append(try std.fmt.allocPrint(allocator, "Directive {d}: `{s}` is not a valid feature name.", .{ where, name }));
            continue;
        }
        if (std.mem.eql(u8, value, "*") or std.mem.eql(u8, value, "self")) continue;

        if (value.len < 2 or value[0] != '(' or value[value.len - 1] != ')') {
            try errors.append(try std.fmt.allocPrint(allocator, "Directive {d}: `{s}` allowlist must be `*`, `self`, or `(...)` — got `{s}`.", .{ where, name, value }));
            continue;
        }

        const inner = trim(value[1 .. value.len - 1]);
        if (inner.len == 0) continue; // () = disabled, valid

        var tokens = std.mem.tokenizeAny(u8, inner, WS);
        while (tokens.next()) |raw_token| {
            if (!isValidAllowlistToken(unquote(raw_token))) {
                try errors.append(try std.fmt.allocPrint(allocator, "Directive {d}: `{s}` has an invalid allowlist token `{s}`.", .{ where, name, raw_token }));
            }
        }
    }

    return .{ .allocator = allocator, .errors = try errors.toOwnedSlice() };
}

// MARK: - Score

/// Privacy score 0-100 for a policy: how much it locks down the catalog.
/// Each feature is weighted by privacy impact (high 3, medium 2, low 1).
/// Disabled `()` earns full credit, `self` or an origin list half, `*` or
/// "absent from the policy" none (the browser default stays in force).
pub fn privacyScore(policy: Policy) u32 {
    var earned: f64 = 0;
    var possible: f64 = 0;

    for (FEATURES) |feature| {
        const w = feature.privacy_impact.weight();
        possible += w;

        const allowlist = policy.get(feature.name) orelse continue; // absent — no credit
        if (allowlist.len == 0) {
            earned += w; // () disabled
        } else if (!(allowlist.len == 1 and std.mem.eql(u8, allowlist[0], "*"))) {
            earned += w / 2; // self / origins
        }
    }

    return @intFromFloat(@round((earned / possible) * 100));
}

// MARK: - Demo

pub fn main() !void {
    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
    defer _ = gpa.deinit();
    const allocator = gpa.allocator();

    var policy = Policy.init(allocator);
    defer policy.deinit();

    try policy.put("geolocation", &[_][]const u8{"self"});
    try policy.put("camera", &[_][]const u8{});
    try policy.put("microphone", &[_][]const u8{"*"});
    try policy.put("usb", &[_][]const u8{"https://a.example"});

    const header = try buildPermissionsPolicy(allocator, policy);
    defer allocator.free(header);

    const stdout = std.io.getStdOut().writer();
    try stdout.print("Permissions-Policy: {s}\n", .{header});
    try stdout.print("privacy score: {d}/100\n", .{privacyScore(policy)});

    var round_trip = (try parsePermissionsPolicy(allocator, header)) orelse {
        try stdout.print("parse failed\n", .{});
        return;
    };
    defer round_trip.deinit();
    try stdout.print("parsed {d} directives\n", .{round_trip.count()});

    var check = try validatePermissionsPolicy(allocator, "camera=(), geolocation=(self, usb=*");
    defer check.deinit();
    try stdout.print("valid: {}\n", .{check.valid()});
    for (check.errors) |message| try stdout.print("  - {s}\n", .{message});
}

// MARK: - Tests (shared vectors with src/lib/permissions-policy.test.ts)

test "buildPermissionsPolicy sorts directives and formats allowlists" {
    const allocator = std.testing.allocator;
    var policy = Policy.init(allocator);
    defer policy.deinit();

    try policy.put("geolocation", &[_][]const u8{"self"});
    try policy.put("camera", &[_][]const u8{});
    try policy.put("microphone", &[_][]const u8{"*"});

    const header = try buildPermissionsPolicy(allocator, policy);
    defer allocator.free(header);
    try std.testing.expectEqualStrings("camera=(), geolocation=(self), microphone=*", header);
}

test "parsePermissionsPolicy round-trips and strips the header name" {
    const allocator = std.testing.allocator;
    var policy = (try parsePermissionsPolicy(allocator, "Permissions-Policy: camera=(), usb=(https://a.example)")).?;
    defer policy.deinit();

    try std.testing.expectEqual(@as(usize, 0), policy.get("camera").?.len);
    try std.testing.expectEqualStrings("https://a.example", policy.get("usb").?[0]);
}

test "parsePermissionsPolicy rejects malformed input" {
    const allocator = std.testing.allocator;
    try std.testing.expect((try parsePermissionsPolicy(allocator, "")) == null);
    try std.testing.expect((try parsePermissionsPolicy(allocator, "camera")) == null);
    try std.testing.expect((try parsePermissionsPolicy(allocator, "=()")) == null);
    try std.testing.expect((try parsePermissionsPolicy(allocator, "Camera=()")) == null); // uppercase name
    try std.testing.expect((try parsePermissionsPolicy(allocator, "camera=(nope)")) == null);
    try std.testing.expect((try parsePermissionsPolicy(allocator, "camera=(), ")) == null); // stray comma
}

test "isValidOrigin mirrors ORIGIN_RE" {
    try std.testing.expect(isValidOrigin("https://a.example"));
    try std.testing.expect(isValidOrigin("http://a.example:8080"));
    try std.testing.expect(isValidOrigin("https:a.example"));
    try std.testing.expect(!isValidOrigin("a.example")); // no scheme
    try std.testing.expect(!isValidOrigin("https://")); // no host
    try std.testing.expect(!isValidOrigin("https://a.example:")); // port with no digits
    try std.testing.expect(!isValidOrigin("https://a.example:80x")); // non-digit port
}

test "validatePermissionsPolicy reports every fault" {
    const allocator = std.testing.allocator;
    var check = try validatePermissionsPolicy(allocator, "camera=(), Bad=(), usb=nope");
    defer check.deinit();
    try std.testing.expect(!check.valid());
    try std.testing.expectEqual(@as(usize, 2), check.errors.len);

    var empty = try validatePermissionsPolicy(allocator, "   ");
    defer empty.deinit();
    try std.testing.expect(!empty.valid());
    try std.testing.expectEqualStrings("Header is empty.", empty.errors[0]);
}

test "privacyScore weights by impact" {
    const allocator = std.testing.allocator;

    var empty = Policy.init(allocator);
    defer empty.deinit();
    try std.testing.expectEqual(@as(u32, 0), privacyScore(empty));

    // Every catalog feature disabled => full credit.
    var locked = Policy.init(allocator);
    defer locked.deinit();
    for (FEATURES) |feature| try locked.put(feature.name, &[_][]const u8{});
    try std.testing.expectEqual(@as(u32, 100), privacyScore(locked));

    // `*` earns nothing; `self` earns half.
    var mixed = Policy.init(allocator);
    defer mixed.deinit();
    try mixed.put("camera", &[_][]const u8{"*"});
    try std.testing.expectEqual(@as(u32, 0), privacyScore(mixed));
    try mixed.put("camera", &[_][]const u8{"self"});
    try std.testing.expect(privacyScore(mixed) > 0);
}

Also available in 8 other languages

Every CosmoDev tool ships its pure logic in TypeScript (web) and Go (CLI), with authored implementations in a dozen-plus languages — the same contract, ported. Compare all languages side by side →