alexa-house-control/tools/add-alexa-permission.zig

101 lines
3.7 KiB
Zig

//! Adds Alexa skill-specific Lambda permission.
//! Alexa requires the Lambda policy to include the skill ID as an event source token condition.
const std = @import("std");
const aws = @import("aws");
const json = std.json;
pub fn main() !u8 {
var gpa: std.heap.GeneralPurposeAllocator(.{}) = .init;
defer _ = gpa.deinit();
const allocator = gpa.allocator();
const args = try std.process.argsAlloc(allocator);
defer std.process.argsFree(allocator, args);
if (args.len != 3) {
std.debug.print("Usage: {s} <deploy-output.json> <ask-states.json>\n", .{args[0]});
return 1;
}
// Read deploy output to get function name and region
const deploy_output = std.fs.cwd().readFileAlloc(allocator, args[1], 1024 * 1024) catch |err| {
std.debug.print("Failed to read deploy output '{s}': {}\n", .{ args[1], err });
return 1;
};
defer allocator.free(deploy_output);
const deploy_parsed = json.parseFromSlice(json.Value, allocator, deploy_output, .{}) catch |err| {
std.debug.print("Failed to parse deploy output: {}\n", .{err});
return 1;
};
defer deploy_parsed.deinit();
const function_name = deploy_parsed.value.object.get("function_name").?.string;
const region = deploy_parsed.value.object.get("region").?.string;
// Read ask-states.json to get skill ID
const ask_states = std.fs.cwd().readFileAlloc(allocator, args[2], 1024 * 1024) catch |err| {
std.debug.print("Failed to read ask-states.json '{s}': {}\n", .{ args[2], err });
return 1;
};
defer allocator.free(ask_states);
const ask_parsed = json.parseFromSlice(json.Value, allocator, ask_states, .{}) catch |err| {
std.debug.print("Failed to parse ask-states.json: {}\n", .{err});
return 1;
};
defer ask_parsed.deinit();
const skill_id = ask_parsed.value.object.get("profiles").?.object.get("default").?.object.get("skillId").?.string;
std.debug.print("Adding Alexa permission for skill {s} to function {s} in {s}\n", .{ skill_id, function_name, region });
// Build statement ID from skill ID (use last 12 chars to keep it short but unique)
var statement_id_buf: [64]u8 = undefined;
const statement_id = std.fmt.bufPrint(&statement_id_buf, "alexa-skill-{s}", .{skill_id[skill_id.len - 12 ..]}) catch {
std.debug.print("Failed to build statement ID\n", .{});
return 1;
};
// Create AWS client and options
var client = aws.Client.init(allocator, .{});
defer client.deinit();
var diagnostics: aws.Diagnostics = .{
.response_status = undefined,
.response_body = undefined,
.allocator = allocator,
};
const opts = aws.Options{
.client = client,
.region = region,
.diagnostics = &diagnostics,
};
// Add permission with skill ID as event source token
const services = aws.Services(.{.lambda}){};
_ = aws.Request(services.lambda.add_permission).call(.{
.function_name = function_name,
.statement_id = statement_id,
.action = "lambda:InvokeFunction",
.principal = "alexa-appkit.amazon.com",
.event_source_token = skill_id,
}, opts) catch |err| {
defer diagnostics.deinit();
// 409 Conflict means permission already exists - that's fine
if (diagnostics.response_status == .conflict) {
std.debug.print("Permission already exists for skill: {s}\n", .{skill_id});
return 0;
}
std.debug.print("AddPermission failed: {} (HTTP {})\n", .{ err, diagnostics.response_status });
return 1;
};
std.debug.print("Added Alexa permission for skill: {s}\n", .{skill_id});
return 0;
}