@ampless/backend 1.0.0-alpha.44 → 1.0.0-alpha.46

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,7 @@
1
1
  // src/events/processor-trusted.ts
2
2
  import { S3Client, PutObjectCommand } from "@aws-sdk/client-s3";
3
- import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
3
+ import { DynamoDBClient, GetItemCommand } from "@aws-sdk/client-dynamodb";
4
+ import { marshall, unmarshall } from "@aws-sdk/util-dynamodb";
4
5
  import {
5
6
  DynamoDBDocumentClient,
6
7
  DeleteCommand,
@@ -85,7 +86,9 @@ function createProcessorTrustedHandler(opts) {
85
86
  const POST_TABLE = requireEnv("AMPLESS_POST_TABLE");
86
87
  const KV_TABLE = requireEnv("AMPLESS_KV_TABLE");
87
88
  const POSTTAG_TABLE = requireEnv("AMPLESS_POSTTAG_TABLE");
89
+ const PLUGIN_SECRET_TABLE = requireEnv("AMPLESS_PLUGIN_SECRET_TABLE");
88
90
  const REGION = requireEnv("AWS_REGION");
91
+ const rawDdb = new DynamoDBClient({});
89
92
  async function listPublished() {
90
93
  const items = [];
91
94
  let exclusiveStartKey;
@@ -121,6 +124,7 @@ function createProcessorTrustedHandler(opts) {
121
124
  function makeContext(plugin) {
122
125
  const namespace = plugin.instanceId ?? plugin.name;
123
126
  const label = plugin.instanceId ? `${plugin.name}#${plugin.instanceId}` : plugin.name;
127
+ const secretCache = /* @__PURE__ */ new Map();
124
128
  return {
125
129
  site: opts.site,
126
130
  listPublishedPosts: () => listPublished(),
@@ -146,6 +150,30 @@ function createProcessorTrustedHandler(opts) {
146
150
  })
147
151
  );
148
152
  return formatPublicAssetUrl(BUCKET, REGION, objectKey);
153
+ },
154
+ async secret(key) {
155
+ const cacheKey = `${namespace}:${key}`;
156
+ if (secretCache.has(cacheKey)) {
157
+ return secretCache.get(cacheKey);
158
+ }
159
+ const sk = `plugins.${namespace}.${key}`;
160
+ try {
161
+ const result = await rawDdb.send(
162
+ new GetItemCommand({
163
+ TableName: PLUGIN_SECRET_TABLE,
164
+ Key: marshall({ siteId: "default", sk })
165
+ })
166
+ );
167
+ const value = result.Item ? unmarshall(result.Item).value : void 0;
168
+ secretCache.set(cacheKey, value);
169
+ return value;
170
+ } catch (err) {
171
+ console.error(
172
+ `[trusted-processor] ${label}: ctx.secret("${key}") DDB read failed`,
173
+ err
174
+ );
175
+ return void 0;
176
+ }
149
177
  }
150
178
  };
151
179
  }
package/dist/index.d.ts CHANGED
@@ -175,6 +175,7 @@ declare function amplessSchemaModels(a: any, opts?: AmplessSchemaModelsOpts): {
175
175
  PostTag: any;
176
176
  KvStore: any;
177
177
  McpToken: any;
178
+ PluginSecret: any;
178
179
  PublicPost: any;
179
180
  PublicPostConnection: any;
180
181
  PublicMedia: any;
package/dist/index.js CHANGED
@@ -396,6 +396,47 @@ function amplessSchemaModels(a, opts = {}) {
396
396
  }).identifier(["hash"]).authorization((allow) => [
397
397
  allow.groups(["ampless-admin"])
398
398
  ]),
399
+ // Isolated secret storage for trusted plugins (Phase 6a).
400
+ //
401
+ // This is a COMPLETELY SEPARATE model from KvStore. KvStore grants
402
+ // admin/editor full read access through AppSync, which means any
403
+ // value stored there can be read by anyone with admin/editor
404
+ // credentials. PluginSecret has NO read authorization for any
405
+ // Cognito group — read is exclusively granted to IAM-authenticated
406
+ // principals (i.e. the trusted-processor Lambda role).
407
+ //
408
+ // Authorization design:
409
+ // - admin / editor: CREATE, UPDATE, DELETE only. The AppSync
410
+ // schema will not generate getPluginSecret / listPluginSecrets
411
+ // queries for these groups because 'read' is absent.
412
+ // - IAM (Lambda): read only. The trusted-processor Lambda uses
413
+ // DDB SDK GetItem directly (not AppSync) to read secrets.
414
+ //
415
+ // Storage key convention:
416
+ // siteId = 'default' (single-site architecture)
417
+ // sk = `plugins.${instanceId ?? name}.${fieldKey}`
418
+ //
419
+ // DynamoDB auto-encrypts at rest (AWS-managed KMS key). Secrets
420
+ // never flow to the S3 site-settings mirror because the trusted
421
+ // processor only queries KvStore (pk='siteconfig') for that path —
422
+ // PluginSecret is a structurally separate table.
423
+ PluginSecret: a.model({
424
+ // Single-site architecture: siteId is always 'default'.
425
+ siteId: a.string().required(),
426
+ // Composite sort key: `plugins.<instanceId>.<fieldKey>`
427
+ sk: a.string().required(),
428
+ // The secret value (plaintext string). DynamoDB's server-side
429
+ // encryption (SSE) with AWS-owned KMS protects the value at
430
+ // rest. IAM controls Lambda read; AppSync controls admin write.
431
+ value: a.string().required()
432
+ }).identifier(["siteId", "sk"]).authorization((allow) => [
433
+ // admin/editor can create/update/delete but NOT read.
434
+ // 'read' is intentionally omitted so AppSync does not generate
435
+ // getPluginSecret / listPluginSecrets queries for these groups.
436
+ allow.groups(["ampless-admin", "ampless-editor"]).to(["create", "update", "delete"]),
437
+ // Trusted Lambda reads via IAM-signed requests (SigV4).
438
+ allow.authenticated("iam").to(["read"])
439
+ ]),
399
440
  // Custom return type for public post reads. Decoupling from `Post` lets
400
441
  // AppSync skip the model-level (admin-only) auth check on fields.
401
442
  //
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ampless/backend",
3
- "version": "1.0.0-alpha.44",
3
+ "version": "1.0.0-alpha.46",
4
4
  "description": "Amplify Gen 2 backend factories for ampless: auth, data, storage, event processors, API key renewer",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -65,8 +65,8 @@
65
65
  "@smithy/protocol-http": "^5.4.4",
66
66
  "@smithy/signature-v4": "^5.4.4",
67
67
  "fflate": "^0.8.3",
68
- "ampless": "1.0.0-alpha.27",
69
- "@ampless/mcp-server": "1.0.0-alpha.33"
68
+ "ampless": "1.0.0-alpha.29",
69
+ "@ampless/mcp-server": "1.0.0-alpha.35"
70
70
  },
71
71
  "peerDependencies": {
72
72
  "@aws-amplify/backend": "^1",