Skip to content

Commit cde22e3

Browse files
Jake ChampionJakeChampion
authored andcommitted
feat: implement fastly:secret-store package
1 parent 04c05f5 commit cde22e3

File tree

23 files changed

+1460
-32
lines changed

23 files changed

+1460
-32
lines changed

.github/workflows/main.yml

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -165,7 +165,7 @@ jobs:
165165
sdktest:
166166
if: github.ref != 'refs/heads/main'
167167
runs-on: ubuntu-latest
168-
needs: [build]
168+
needs: [build, ensure_cargo_installs]
169169
strategy:
170170
fail-fast: false
171171
matrix:
@@ -198,6 +198,7 @@ jobs:
198198
- request-upstream
199199
- response
200200
- response-headers
201+
- secret-store
201202
- status
202203
- streaming-close
203204
- tee
@@ -213,7 +214,7 @@ jobs:
213214
uses: fastly/compute-actions/setup@v4
214215
with:
215216
token: ${{ secrets.GITHUB_TOKEN }}
216-
cli_version: '4.3.0'
217+
cli_version: '5.0.0'
217218

218219
- name: Restore Viceroy from cache
219220
uses: actions/cache@v3
Lines changed: 204 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,204 @@
1+
#include "secret-store.h"
2+
#include "host_call.h"
3+
4+
namespace builtins {
5+
6+
fastly_secret_handle_t SecretStoreEntry::secret_handle(JSObject *obj) {
7+
JS::Value val = JS::GetReservedSlot(obj, SecretStoreEntry::Slots::Handle);
8+
return static_cast<fastly_secret_handle_t>(val.toInt32());
9+
}
10+
11+
bool SecretStoreEntry::plaintext(JSContext *cx, unsigned argc, JS::Value *vp) {
12+
METHOD_HEADER(0)
13+
14+
fastly_option_string_t ret;
15+
fastly_error_t err;
16+
// Ensure that we throw an exception for all unexpected host errors.
17+
if (!xqd_fastly_secret_store_plaintext(SecretStoreEntry::secret_handle(self), &ret, &err)) {
18+
HANDLE_ERROR(cx, err);
19+
return false;
20+
}
21+
22+
JS::RootedString text(cx, JS_NewStringCopyUTF8N(cx, JS::UTF8Chars(ret.val.ptr, ret.val.len)));
23+
JS_free(cx, ret.val.ptr);
24+
if (!text) {
25+
return false;
26+
}
27+
28+
args.rval().setString(text);
29+
return true;
30+
}
31+
32+
const JSFunctionSpec SecretStoreEntry::methods[] = {
33+
JS_FN("plaintext", plaintext, 0, JSPROP_ENUMERATE), JS_FS_END};
34+
35+
const JSPropertySpec SecretStoreEntry::properties[] = {JS_PS_END};
36+
37+
bool SecretStoreEntry::constructor(JSContext *cx, unsigned argc, JS::Value *vp) {
38+
JS_ReportErrorUTF8(cx, "SecretStoreEntry can't be instantiated directly");
39+
return false;
40+
}
41+
42+
JSObject *SecretStoreEntry::create(JSContext *cx, fastly_secret_handle_t handle) {
43+
JS::RootedObject entry(
44+
cx, JS_NewObjectWithGivenProto(cx, &SecretStoreEntry::class_, SecretStoreEntry::proto_obj));
45+
if (!entry) {
46+
return nullptr;
47+
}
48+
49+
JS::SetReservedSlot(entry, Slots::Handle, JS::Int32Value(handle));
50+
51+
return entry;
52+
}
53+
54+
bool SecretStoreEntry::init_class(JSContext *cx, JS::HandleObject global) {
55+
return BuiltinImpl<SecretStoreEntry>::init_class_impl(cx, global);
56+
}
57+
58+
fastly_secret_store_handle_t SecretStore::secret_store_handle(JSObject *obj) {
59+
JS::Value val = JS::GetReservedSlot(obj, SecretStore::Slots::Handle);
60+
return static_cast<fastly_secret_store_handle_t>(val.toInt32());
61+
}
62+
63+
bool SecretStore::get(JSContext *cx, unsigned argc, JS::Value *vp) {
64+
METHOD_HEADER(1)
65+
66+
JS::RootedObject result_promise(cx, JS::NewPromiseObject(cx, nullptr));
67+
if (!result_promise) {
68+
return ReturnPromiseRejectedWithPendingError(cx, args);
69+
}
70+
71+
size_t length;
72+
JS::UniqueChars key = encode(cx, args[0], &length);
73+
if (!key) {
74+
return false;
75+
}
76+
// If the converted string has a length of 0 then we throw an Error
77+
// because keys have to be at-least 1 character.
78+
if (length == 0) {
79+
JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_SECRET_STORE_KEY_EMPTY);
80+
return ReturnPromiseRejectedWithPendingError(cx, args);
81+
}
82+
83+
// key has to be less than 256
84+
if (length > 255) {
85+
JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_SECRET_STORE_KEY_TOO_LONG);
86+
return ReturnPromiseRejectedWithPendingError(cx, args);
87+
}
88+
89+
std::string_view keyView(key.get(), length);
90+
91+
// key must contain only letters, numbers, dashes (-), underscores (_), and periods (.).
92+
auto is_valid_key = std::all_of(keyView.begin(), keyView.end(), [&](auto character) {
93+
return std::isalnum(character) || character == '_' || character == '-' || character == '.';
94+
});
95+
96+
if (!is_valid_key) {
97+
JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
98+
JSMSG_SECRET_STORE_KEY_CONTAINS_INVALID_CHARACTER);
99+
return ReturnPromiseRejectedWithPendingError(cx, args);
100+
}
101+
102+
xqd_world_string_t key_str;
103+
key_str.len = length;
104+
key_str.ptr = key.get();
105+
fastly_option_secret_handle_t secret;
106+
fastly_error_t err;
107+
// Ensure that we throw an exception for all unexpected host errors.
108+
if (!xqd_fastly_secret_store_get(SecretStore::secret_store_handle(self), &key_str, &secret,
109+
&err)) {
110+
HANDLE_ERROR(cx, err);
111+
return ReturnPromiseRejectedWithPendingError(cx, args);
112+
}
113+
114+
// When no entry is found, we are going to resolve the Promise with `null`.
115+
if (!secret.is_some) {
116+
JS::RootedValue result(cx);
117+
result.setNull();
118+
JS::ResolvePromise(cx, result_promise, result);
119+
} else {
120+
JS::RootedObject entry(cx, SecretStoreEntry::create(cx, secret.val));
121+
if (!entry) {
122+
return ReturnPromiseRejectedWithPendingError(cx, args);
123+
}
124+
JS::RootedValue result(cx);
125+
result.setObject(*entry);
126+
JS::ResolvePromise(cx, result_promise, result);
127+
}
128+
129+
args.rval().setObject(*result_promise);
130+
131+
return true;
132+
}
133+
134+
const JSFunctionSpec SecretStore::methods[] = {JS_FN("get", get, 1, JSPROP_ENUMERATE), JS_FS_END};
135+
136+
const JSPropertySpec SecretStore::properties[] = {JS_PS_END};
137+
138+
bool SecretStore::constructor(JSContext *cx, unsigned argc, JS::Value *vp) {
139+
REQUEST_HANDLER_ONLY("The SecretStore builtin");
140+
CTOR_HEADER("SecretStore", 1);
141+
142+
size_t length;
143+
JS::UniqueChars name_chars = encode(cx, args[0], &length);
144+
if (!name_chars) {
145+
return false;
146+
}
147+
148+
// If the converted string has a length of 0 then we throw an Error
149+
// because names have to be at-least 1 character.
150+
if (length == 0) {
151+
JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_SECRET_STORE_NAME_EMPTY);
152+
return false;
153+
}
154+
155+
// If the converted string has a length of more than 255 then we throw an Error
156+
// because names have to be less than 255 characters.
157+
if (length > 255) {
158+
JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_SECRET_STORE_NAME_TOO_LONG);
159+
return false;
160+
}
161+
162+
std::string_view name(name_chars.get(), length);
163+
164+
// Name must contain only letters, numbers, dashes (-), underscores (_), and periods (.).
165+
auto is_valid_name = std::all_of(name.begin(), name.end(), [&](auto character) {
166+
return std::isalnum(character) || character == '_' || character == '-' || character == '.';
167+
});
168+
169+
if (!is_valid_name) {
170+
JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
171+
JSMSG_SECRET_STORE_NAME_CONTAINS_INVALID_CHARACTER);
172+
return false;
173+
}
174+
175+
JS::RootedObject secret_store(cx, JS_NewObjectForConstructor(cx, &class_, args));
176+
if (!secret_store) {
177+
return false;
178+
}
179+
xqd_world_string_t name_str;
180+
name_str.ptr = name_chars.get();
181+
name_str.len = length;
182+
fastly_secret_store_handle_t handle = INVALID_HANDLE;
183+
fastly_error_t err;
184+
if (!xqd_fastly_secret_store_open(&name_str, &handle, &err)) {
185+
if (err == FASTLY_ERROR_OPTIONAL_NONE) {
186+
JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_SECRET_STORE_DOES_NOT_EXIST,
187+
name.data());
188+
return false;
189+
} else {
190+
HANDLE_ERROR(cx, err);
191+
return false;
192+
}
193+
}
194+
195+
JS::SetReservedSlot(secret_store, SecretStore::Slots::Handle, JS::Int32Value(handle));
196+
args.rval().setObject(*secret_store);
197+
return true;
198+
}
199+
200+
bool SecretStore::init_class(JSContext *cx, JS::HandleObject global) {
201+
return BuiltinImpl<SecretStore>::init_class_impl(cx, global);
202+
}
203+
204+
} // namespace builtins
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
#ifndef JS_COMPUTE_RUNTIME_SECRET_STORE_H
2+
#define JS_COMPUTE_RUNTIME_SECRET_STORE_H
3+
4+
#include "builtin.h"
5+
6+
namespace builtins {
7+
8+
class SecretStoreEntry : public BuiltinImpl<SecretStoreEntry> {
9+
private:
10+
public:
11+
static constexpr const char *class_name = "SecretStoreEntry";
12+
static const int ctor_length = 0;
13+
enum Slots { Handle, Count };
14+
15+
static const JSFunctionSpec methods[];
16+
static const JSPropertySpec properties[];
17+
18+
static bool plaintext(JSContext *cx, unsigned argc, JS::Value *vp);
19+
20+
static fastly_secret_handle_t secret_handle(JSObject *obj);
21+
static bool constructor(JSContext *cx, unsigned argc, JS::Value *vp);
22+
static JSObject *create(JSContext *cx, fastly_secret_handle_t handle);
23+
24+
static bool init_class(JSContext *cx, JS::HandleObject global);
25+
};
26+
27+
class SecretStore : public BuiltinImpl<SecretStore> {
28+
private:
29+
public:
30+
static constexpr const char *class_name = "SecretStore";
31+
static const int ctor_length = 1;
32+
enum Slots { Handle, Count };
33+
34+
static const JSFunctionSpec methods[];
35+
static const JSPropertySpec properties[];
36+
37+
static bool get(JSContext *cx, unsigned argc, JS::Value *vp);
38+
39+
static fastly_secret_store_handle_t secret_store_handle(JSObject *obj);
40+
static bool constructor(JSContext *cx, unsigned argc, JS::Value *vp);
41+
42+
static bool init_class(JSContext *cx, JS::HandleObject global);
43+
};
44+
45+
} // namespace builtins
46+
47+
#endif

c-dependencies/js-compute-runtime/error-numbers.msg

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,14 @@ MSG_DEF(JSMSG_OBJECT_STORE_KEY_ACME, 0, JSEXN_TYPEERR,
7272
MSG_DEF(JSMSG_OBJECT_STORE_KEY_RELATIVE, 0, JSEXN_TYPEERR, "ObjectStore key can not be '.' or '..'")
7373
MSG_DEF(JSMSG_OBJECT_STORE_PUT_CONTENT_STREAM, 0, JSEXN_TYPEERR, "Content-provided streams are not yet supported for streaming into ObjectStore")
7474
MSG_DEF(JSMSG_OBJECT_STORE_PUT_OVER_30_MB, 0, JSEXN_TYPEERR, "ObjectStore value can not be more than 30 Megabytes in size")
75+
MSG_DEF(JSMSG_SECRET_STORE_DOES_NOT_EXIST, 1, JSEXN_TYPEERR, "SecretStore constructor: No SecretStore named '{0}' exists")
76+
MSG_DEF(JSMSG_SECRET_STORE_KEY_EMPTY, 0, JSEXN_TYPEERR, "SecretStore key can not be an empty string")
77+
MSG_DEF(JSMSG_SECRET_STORE_KEY_TOO_LONG, 0, JSEXN_TYPEERR, "SecretStore key can not be more than 255 characters")
78+
MSG_DEF(JSMSG_SECRET_STORE_KEY_CONTAINS_INVALID_CHARACTER, 0, JSEXN_TYPEERR, "SecretStore key can contain only ascii alphanumeric characters, underscores, dashes, and ascii whitespace")
79+
MSG_DEF(JSMSG_SECRET_STORE_NAME_CONTAINS_INVALID_CHARACTER, 0, JSEXN_TYPEERR, "SecretStore constructor: name can contain only ascii alphanumeric characters, underscores, dashes, and ascii whitespace")
80+
MSG_DEF(JSMSG_SECRET_STORE_NAME_EMPTY, 0, JSEXN_TYPEERR, "SecretStore constructor: name can not be an empty string")
81+
MSG_DEF(JSMSG_SECRET_STORE_NAME_START_WITH_ASCII_ALPHA, 0, JSEXN_TYPEERR, "SecretStore constructor: name must start with an ascii alpabetical character")
82+
MSG_DEF(JSMSG_SECRET_STORE_NAME_TOO_LONG, 0, JSEXN_TYPEERR, "SecretStore constructor: name can not be more than 255 characters")
7583
MSG_DEF(JSMSG_READABLE_STREAM_LOCKED_OR_DISTRUBED, 0, JSEXN_TYPEERR, "Can't use a ReadableStream that's locked or has ever been read from or canceled")
7684
MSG_DEF(JSMSG_INVALID_CHARACTER_ERROR, 0, JSEXN_ERR, "String contains an invalid character")
7785
MSG_DEF(JSMSG_BACKEND_PARAMETER_NOT_OBJECT, 0, JSEXN_TYPEERR, "Backend constructor: configuration parameter must be an Object")

c-dependencies/js-compute-runtime/js-compute-builtins.cpp

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@
5555
#include "builtins/native-stream-source.h"
5656
#include "builtins/object-store.h"
5757
#include "builtins/shared/console.h"
58+
#include "builtins/secret-store.h"
5859
#include "builtins/subtle-crypto.h"
5960
#include "builtins/transform-stream-default-controller.h"
6061
#include "builtins/transform-stream.h"
@@ -5314,6 +5315,10 @@ bool define_fastly_sys(JSContext *cx, HandleObject global) {
53145315
return false;
53155316
if (!ObjectStoreEntry::init_class(cx, global))
53165317
return false;
5318+
if (!builtins::SecretStore::init_class(cx, global))
5319+
return false;
5320+
if (!builtins::SecretStoreEntry::init_class(cx, global))
5321+
return false;
53175322

53185323
pending_async_tasks = new JS::PersistentRootedObjectVector(cx);
53195324

c-dependencies/js-compute-runtime/xqd-world/xqd_world_adapter.cpp

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -594,6 +594,43 @@ bool xqd_fastly_dictionary_get(fastly_dictionary_handle_t h, xqd_world_string_t
594594
return true;
595595
}
596596

597+
bool xqd_fastly_secret_store_open(xqd_world_string_t *name, fastly_secret_store_handle_t *ret,
598+
fastly_error_t *err) {
599+
return convert_result(xqd_secret_store_open(name->ptr, name->len, ret), err);
600+
}
601+
602+
bool xqd_fastly_secret_store_get(fastly_secret_store_handle_t store, xqd_world_string_t *key,
603+
fastly_option_secret_handle_t *ret, fastly_error_t *err) {
604+
ret->val = INVALID_HANDLE;
605+
bool ok = convert_result(xqd_secret_store_get(store, key->ptr, key->len, &ret->val), err);
606+
if ((!ok && *err == FASTLY_ERROR_OPTIONAL_NONE) || ret->val == INVALID_HANDLE) {
607+
ret->is_some = false;
608+
return true;
609+
}
610+
ret->is_some = true;
611+
return ok;
612+
}
613+
614+
bool xqd_fastly_secret_store_plaintext(fastly_dictionary_handle_t h, fastly_option_string_t *ret,
615+
fastly_error_t *err) {
616+
ret->val.ptr = static_cast<char *>(JS_malloc(CONTEXT, DICTIONARY_ENTRY_MAX_LEN));
617+
if (!convert_result(
618+
xqd_secret_store_plaintext(h, ret->val.ptr, DICTIONARY_ENTRY_MAX_LEN, &ret->val.len),
619+
err)) {
620+
if (*err == FASTLY_ERROR_OPTIONAL_NONE) {
621+
ret->is_some = false;
622+
return true;
623+
} else {
624+
JS_free(CONTEXT, ret->val.ptr);
625+
return false;
626+
}
627+
}
628+
ret->is_some = true;
629+
ret->val.ptr = static_cast<char *>(
630+
JS_realloc(CONTEXT, ret->val.ptr, DICTIONARY_ENTRY_MAX_LEN, ret->val.len));
631+
return true;
632+
}
633+
597634
bool xqd_fastly_geo_lookup(fastly_list_u8_t *addr_octets, xqd_world_string_t *ret,
598635
fastly_error_t *err) {
599636
ret->ptr = static_cast<char *>(cabi_malloc(HOSTCALL_BUFFER_LEN, 1));

c-dependencies/js-compute-runtime/xqd.h

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -314,6 +314,19 @@ WASM_IMPORT("fastly_dictionary", "get")
314314
int xqd_dictionary_get(fastly_dictionary_handle_t dict_handle, const char *key, size_t key_len,
315315
char *value, size_t value_max_len, size_t *nwritten);
316316

317+
// Module fastly_secret_store
318+
WASM_IMPORT("fastly_secret_store", "open")
319+
int xqd_secret_store_open(const char *name, size_t name_len,
320+
fastly_secret_store_handle_t *dict_handle_out);
321+
322+
WASM_IMPORT("fastly_secret_store", "get")
323+
int xqd_secret_store_get(fastly_secret_store_handle_t dict_handle, const char *key, size_t key_len,
324+
fastly_secret_handle_t *opt_secret_handle_out);
325+
326+
WASM_IMPORT("fastly_secret_store", "plaintext")
327+
int xqd_secret_store_plaintext(fastly_secret_handle_t secret_handle, char *buf, size_t buf_len,
328+
size_t *nwritten);
329+
317330
// Module fastly_object_store
318331
WASM_IMPORT("fastly_object_store", "open")
319332
int xqd_object_store_open(const char *name, size_t name_len,

0 commit comments

Comments
 (0)