@model-ts/dynamodb 0.2.0 → 1.1.0
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.
- package/CHANGELOG.md +13 -0
- package/dist/cjs/__test__/client-with-cursor-encryption.test.d.ts +1 -0
- package/dist/cjs/__test__/client-with-cursor-encryption.test.js +1911 -0
- package/dist/cjs/__test__/client-with-cursor-encryption.test.js.map +1 -0
- package/dist/cjs/client.d.ts +7 -0
- package/dist/cjs/client.js +10 -7
- package/dist/cjs/client.js.map +1 -1
- package/dist/cjs/dynamodb-model.d.ts +4 -0
- package/dist/cjs/pagination.d.ts +48 -3
- package/dist/cjs/pagination.js +65 -23
- package/dist/cjs/pagination.js.map +1 -1
- package/dist/cjs/provider.d.ts +21 -20
- package/dist/cjs/provider.js +5 -1
- package/dist/cjs/provider.js.map +1 -1
- package/dist/esm/__test__/client-with-cursor-encryption.test.d.ts +1 -0
- package/dist/esm/__test__/client-with-cursor-encryption.test.js +1909 -0
- package/dist/esm/__test__/client-with-cursor-encryption.test.js.map +1 -0
- package/dist/esm/client.d.ts +7 -0
- package/dist/esm/client.js +10 -7
- package/dist/esm/client.js.map +1 -1
- package/dist/esm/dynamodb-model.d.ts +4 -0
- package/dist/esm/pagination.d.ts +48 -3
- package/dist/esm/pagination.js +64 -23
- package/dist/esm/pagination.js.map +1 -1
- package/dist/esm/provider.d.ts +21 -20
- package/dist/esm/provider.js +5 -1
- package/dist/esm/provider.js.map +1 -1
- package/package.json +2 -2
- package/src/__test__/client-with-cursor-encryption.test.ts +2445 -0
- package/src/client.ts +20 -9
- package/src/dynamodb-model.ts +4 -0
- package/src/pagination.ts +76 -19
- package/src/provider.ts +5 -2
package/src/client.ts
CHANGED
|
@@ -79,6 +79,12 @@ export interface ClientProps
|
|
|
79
79
|
ServiceConfigurationOptions,
|
|
80
80
|
ClientApiVersions {
|
|
81
81
|
tableName: string
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* The encryption key used to encrypt cursors using AES-256-CTR.
|
|
85
|
+
* Must be a 32 character string, 256 bits.
|
|
86
|
+
*/
|
|
87
|
+
cursorEncryptionKey?: Buffer
|
|
82
88
|
}
|
|
83
89
|
|
|
84
90
|
export interface Key {
|
|
@@ -90,9 +96,11 @@ export class Client {
|
|
|
90
96
|
tableName: string
|
|
91
97
|
documentClient: DocumentClient
|
|
92
98
|
dataLoader: DataLoader<GetOperation<Decodable>, DynamoDBModelInstance, string>
|
|
99
|
+
cursorEncryptionKey?: Buffer
|
|
93
100
|
|
|
94
101
|
constructor(props: ClientProps) {
|
|
95
102
|
this.tableName = props?.tableName
|
|
103
|
+
this.cursorEncryptionKey = props?.cursorEncryptionKey
|
|
96
104
|
this.documentClient = new DocumentClient(props)
|
|
97
105
|
this.dataLoader = new DataLoader<
|
|
98
106
|
GetOperation<Decodable>,
|
|
@@ -189,7 +197,7 @@ export class Client {
|
|
|
189
197
|
const { Item } = await this.documentClient
|
|
190
198
|
.get({
|
|
191
199
|
TableName: this.tableName,
|
|
192
|
-
Key: key,
|
|
200
|
+
Key: { PK: key.PK, SK: key.SK },
|
|
193
201
|
...params,
|
|
194
202
|
})
|
|
195
203
|
.promise()
|
|
@@ -494,7 +502,16 @@ export class Client {
|
|
|
494
502
|
...params,
|
|
495
503
|
// Fetch one additional item to test for a next page
|
|
496
504
|
Limit: limit + 1,
|
|
497
|
-
ExclusiveStartKey: cursor
|
|
505
|
+
ExclusiveStartKey: cursor
|
|
506
|
+
? decodeDDBCursor(
|
|
507
|
+
cursor,
|
|
508
|
+
// GSI1 is the inverse index and uses PK and SK (switched around)
|
|
509
|
+
params.IndexName && params.IndexName !== "GSI1"
|
|
510
|
+
? (params.IndexName as "GSI2" | "GSI3" | "GSI4" | "GSI5")
|
|
511
|
+
: undefined,
|
|
512
|
+
this.cursorEncryptionKey
|
|
513
|
+
)
|
|
514
|
+
: undefined,
|
|
498
515
|
ScanIndexForward: direction === PaginationDirection.FORWARD,
|
|
499
516
|
},
|
|
500
517
|
{ results: model }
|
|
@@ -509,13 +526,7 @@ export class Client {
|
|
|
509
526
|
// Build edges
|
|
510
527
|
const edges = slice.map((item: any) => ({
|
|
511
528
|
node: item,
|
|
512
|
-
cursor: encodeDDBCursor(
|
|
513
|
-
item,
|
|
514
|
-
// GSI1 is the inverse index and uses PK and SK (switched around)
|
|
515
|
-
params.IndexName && params.IndexName !== "GSI1"
|
|
516
|
-
? (params.IndexName as "GSI2" | "GSI3")
|
|
517
|
-
: undefined
|
|
518
|
-
),
|
|
529
|
+
cursor: encodeDDBCursor(item, this.cursorEncryptionKey),
|
|
519
530
|
}))
|
|
520
531
|
|
|
521
532
|
return {
|
package/src/dynamodb-model.ts
CHANGED
package/src/pagination.ts
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
|
+
import crypto from "crypto"
|
|
1
2
|
import { PaginationError } from "./errors"
|
|
2
3
|
|
|
4
|
+
const SIV = "Q05yyCR+0tyWl6glrZhlNw=="
|
|
5
|
+
const ENCRYPTION_ALG = "aes-256-ctr"
|
|
6
|
+
|
|
3
7
|
export interface PageInfo {
|
|
4
8
|
hasPreviousPage: boolean
|
|
5
9
|
hasNextPage: boolean
|
|
@@ -71,6 +75,47 @@ export function decodePagination(pagination: PaginationInput): {
|
|
|
71
75
|
}
|
|
72
76
|
}
|
|
73
77
|
|
|
78
|
+
/**
|
|
79
|
+
* Utility function to encrypt a cursor with AES-256-CTR, but uses a
|
|
80
|
+
* synthetic initialization vector (SIV) to ensure that the same cursor
|
|
81
|
+
* produces the same encrypted value.
|
|
82
|
+
*/
|
|
83
|
+
const encryptCursor = (key: Buffer, cursor: string) => {
|
|
84
|
+
const cipher = crypto.createCipheriv(
|
|
85
|
+
ENCRYPTION_ALG,
|
|
86
|
+
key,
|
|
87
|
+
Buffer.from(SIV, "base64")
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
const encrypted = Buffer.concat([cipher.update(cursor), cipher.final()])
|
|
91
|
+
|
|
92
|
+
return encrypted.toString("base64")
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Utility function to decrypt a cursor with AES-256-CTR, but uses a
|
|
97
|
+
* synthetic initialization vector (SIV) to ensure that the same cursor
|
|
98
|
+
* produces the same encrypted value.
|
|
99
|
+
*/
|
|
100
|
+
const decryptCursor = (key: Buffer, encryptedCursor: string) => {
|
|
101
|
+
try {
|
|
102
|
+
const decipher = crypto.createDecipheriv(
|
|
103
|
+
ENCRYPTION_ALG,
|
|
104
|
+
key,
|
|
105
|
+
Buffer.from(SIV, "base64")
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
const decrypted = Buffer.concat([
|
|
109
|
+
decipher.update(Buffer.from(encryptedCursor, "base64")),
|
|
110
|
+
decipher.final(),
|
|
111
|
+
]).toString()
|
|
112
|
+
|
|
113
|
+
return decrypted
|
|
114
|
+
} catch (error) {
|
|
115
|
+
return null
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
74
119
|
export const encodeDDBCursor = (
|
|
75
120
|
{
|
|
76
121
|
PK,
|
|
@@ -95,21 +140,10 @@ export const encodeDDBCursor = (
|
|
|
95
140
|
GSI5PK?: string
|
|
96
141
|
GSI5SK?: string
|
|
97
142
|
},
|
|
98
|
-
|
|
99
|
-
) =>
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
: index === "GSI3"
|
|
103
|
-
? Buffer.from(JSON.stringify({ PK, SK, GSI3PK, GSI3SK })).toString("base64")
|
|
104
|
-
: index === "GSI4"
|
|
105
|
-
? Buffer.from(JSON.stringify({ PK, SK, GSI4PK, GSI4SK })).toString("base64")
|
|
106
|
-
: index === "GSI5"
|
|
107
|
-
? Buffer.from(JSON.stringify({ PK, SK, GSI5PK, GSI5SK })).toString("base64")
|
|
108
|
-
: Buffer.from(JSON.stringify({ PK, SK })).toString("base64")
|
|
109
|
-
|
|
110
|
-
export const decodeDDBCursor = (encoded: string) => {
|
|
111
|
-
try {
|
|
112
|
-
const {
|
|
143
|
+
encryptionKey?: Buffer
|
|
144
|
+
) => {
|
|
145
|
+
const cursor = Buffer.from(
|
|
146
|
+
JSON.stringify({
|
|
113
147
|
PK,
|
|
114
148
|
SK,
|
|
115
149
|
GSI2PK,
|
|
@@ -120,11 +154,26 @@ export const decodeDDBCursor = (encoded: string) => {
|
|
|
120
154
|
GSI4SK,
|
|
121
155
|
GSI5PK,
|
|
122
156
|
GSI5SK,
|
|
123
|
-
}
|
|
157
|
+
})
|
|
158
|
+
).toString("base64")
|
|
124
159
|
|
|
125
|
-
|
|
160
|
+
if (encryptionKey) return encryptCursor(encryptionKey, cursor)
|
|
161
|
+
|
|
162
|
+
return cursor
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export const decodeDDBCursor = (
|
|
166
|
+
encoded: string,
|
|
167
|
+
index?: "GSI2" | "GSI3" | "GSI4" | "GSI5",
|
|
168
|
+
encryptionKey?: Buffer
|
|
169
|
+
) => {
|
|
170
|
+
try {
|
|
171
|
+
const json = encryptionKey ? decryptCursor(encryptionKey, encoded) : encoded
|
|
172
|
+
// const json = encoded
|
|
173
|
+
|
|
174
|
+
if (!json) throw new Error("Couldn't decrypt cursor")
|
|
126
175
|
|
|
127
|
-
|
|
176
|
+
const {
|
|
128
177
|
PK,
|
|
129
178
|
SK,
|
|
130
179
|
GSI2PK,
|
|
@@ -135,7 +184,15 @@ export const decodeDDBCursor = (encoded: string) => {
|
|
|
135
184
|
GSI4SK,
|
|
136
185
|
GSI5PK,
|
|
137
186
|
GSI5SK,
|
|
138
|
-
}
|
|
187
|
+
} = JSON.parse(Buffer.from(json, "base64").toString())
|
|
188
|
+
|
|
189
|
+
if (typeof PK !== "string" || typeof SK !== "string") throw new Error()
|
|
190
|
+
|
|
191
|
+
if (!index) return { PK, SK }
|
|
192
|
+
if (index === "GSI2") return { PK, SK, GSI2PK, GSI2SK }
|
|
193
|
+
if (index === "GSI3") return { PK, SK, GSI3PK, GSI3SK }
|
|
194
|
+
if (index === "GSI4") return { PK, SK, GSI4PK, GSI4SK }
|
|
195
|
+
if (index === "GSI5") return { PK, SK, GSI5PK, GSI5SK }
|
|
139
196
|
} catch (error) {
|
|
140
197
|
throw new PaginationError("Couldn't decode cursor")
|
|
141
198
|
}
|
package/src/provider.ts
CHANGED
|
@@ -17,7 +17,7 @@ import {
|
|
|
17
17
|
import { OutputOf, TypeOf, ModelOf } from "@model-ts/core"
|
|
18
18
|
import { RaceConditionError } from "./errors"
|
|
19
19
|
import { absurd } from "fp-ts/lib/function"
|
|
20
|
-
import { PaginationInput } from "./pagination"
|
|
20
|
+
import { encodeDDBCursor, PaginationInput } from "./pagination"
|
|
21
21
|
|
|
22
22
|
export interface DynamoDBInternals<M extends Decodable> {
|
|
23
23
|
__dynamoDBDecode(
|
|
@@ -524,6 +524,9 @@ export const getProvider = (client: Client) => {
|
|
|
524
524
|
GSI5SK: this.GSI5SK,
|
|
525
525
|
}
|
|
526
526
|
},
|
|
527
|
+
cursor<T extends DynamoDBModelInstance>(this: T) {
|
|
528
|
+
return encodeDDBCursor(this.keys(), client.cursorEncryptionKey)
|
|
529
|
+
},
|
|
527
530
|
put<T extends DynamoDBModelInstance>(
|
|
528
531
|
this: T,
|
|
529
532
|
params?: Omit<
|
|
@@ -621,7 +624,7 @@ export const getProvider = (client: Client) => {
|
|
|
621
624
|
return client.get<M>({
|
|
622
625
|
_model: this,
|
|
623
626
|
_operation: "get",
|
|
624
|
-
key,
|
|
627
|
+
key: { PK: key.PK, SK: key.SK },
|
|
625
628
|
...params,
|
|
626
629
|
})
|
|
627
630
|
},
|