//! 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(); var stdout_buf: [4096]u8 = undefined; var stderr_buf: [4096]u8 = undefined; var stdout_writer = std.fs.File.stdout().writer(&stdout_buf); var stderr_writer = std.fs.File.stderr().writer(&stderr_buf); const stdout = &stdout_writer.interface; const stderr = &stderr_writer.interface; const args = try std.process.argsAlloc(allocator); defer std.process.argsFree(allocator, args); if (args.len < 3) { try stderr.print("Usage: {s} [--profile ] [--region ]\n", .{args[0]}); try stderr.flush(); return 1; } // Parse optional arguments var profile: ?[]const u8 = null; var region_override: ?[]const u8 = null; var i: usize = 3; while (i < args.len) : (i += 1) { if (std.mem.eql(u8, args[i], "--profile") and i + 1 < args.len) { profile = args[i + 1]; i += 1; } else if (std.mem.eql(u8, args[i], "--region") and i + 1 < args.len) { region_override = args[i + 1]; i += 1; } } // Read deploy output to get function name and region const deploy_output = std.fs.cwd().readFileAlloc(allocator, args[1], 1024 * 1024) catch |err| { try stderr.print("Failed to read deploy output '{s}': {}\n", .{ args[1], err }); try stderr.flush(); return 1; }; defer allocator.free(deploy_output); const deploy_parsed = json.parseFromSlice(json.Value, allocator, deploy_output, .{}) catch |err| { try stderr.print("Failed to parse deploy output: {}\n", .{err}); try stderr.flush(); return 1; }; defer deploy_parsed.deinit(); const function_name = deploy_parsed.value.object.get("function_name").?.string; const region = region_override orelse 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| { try stderr.print("Failed to read ask-states.json '{s}': {}\n", .{ args[2], err }); try stderr.flush(); return 1; }; defer allocator.free(ask_states); const ask_parsed = json.parseFromSlice(json.Value, allocator, ask_states, .{}) catch |err| { try stderr.print("Failed to parse ask-states.json: {}\n", .{err}); try stderr.flush(); return 1; }; defer ask_parsed.deinit(); // Skill ID is always under "default" profile in ask-states.json // (ASK CLI profile is separate from AWS profile) const profiles_obj = ask_parsed.value.object.get("profiles") orelse { try stderr.print("No 'profiles' field in ask-states.json\n", .{}); try stderr.flush(); return 1; }; const profile_obj = profiles_obj.object.get("default") orelse { try stderr.print("No 'default' profile in ask-states.json\n", .{}); try stderr.flush(); return 1; }; const skill_id = profile_obj.object.get("skillId").?.string; try stdout.print("Adding Alexa permission for skill {s} to function {s} in {s}", .{ skill_id, function_name, region }); if (profile) |p| try stdout.print(" (profile: {s})", .{p}); try stdout.print("\n", .{}); try stdout.flush(); // 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 { try stderr.print("Failed to build statement ID\n", .{}); try stderr.flush(); 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, .credential_options = .{ .profile = .{ .profile_name = profile, .prefer_profile_from_file = profile != null, }, }, }; // Add permission with skill ID as event source token const services = aws.Services(.{.lambda}){}; const result = 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) { try stdout.print("Permission already exists for skill: {s}\n", .{skill_id}); try stdout.flush(); return 0; } try stderr.print("AddPermission failed: {} (HTTP {})\n", .{ err, diagnostics.response_status }); try stderr.flush(); return 1; }; result.deinit(); try stdout.print("Added Alexa permission for skill: {s}\n", .{skill_id}); try stdout.flush(); return 0; }