@naanlang/naan 1.2.1 → 1.3.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.
Files changed (35) hide show
  1. package/LICENSE.md +1 -1
  2. package/README.md +1 -1
  3. package/bin/index.js +5 -2
  4. package/dist/env_web.js +11 -1
  5. package/dist/naan.min.js +4 -4
  6. package/frameworks/browser/https_request.nlg +2 -0
  7. package/frameworks/browser/sworker.js +23 -23
  8. package/frameworks/browser/terminals.nlg +123 -21
  9. package/frameworks/browser/workers.nlg +4 -1
  10. package/frameworks/browser/ws_client.nlg +5 -3
  11. package/frameworks/client/apiclient.nlg +8 -6
  12. package/frameworks/client/relaycon.nlg +3 -1
  13. package/frameworks/node/apiserver.nlg +190 -4
  14. package/frameworks/node/worker.nlg +112 -10
  15. package/frameworks/node/ws_client.nlg +5 -3
  16. package/frameworks/project/build.nlg +3 -1
  17. package/frameworks/project/projects.nlg +1 -1
  18. package/frameworks/running/executors.nlg +3 -1
  19. package/frameworks/running/taskexec.nlg +5 -1
  20. package/lib/browser/env_web.js +17 -7
  21. package/lib/browser/env_webworker.js +22 -3
  22. package/lib/core/naanlib.js +4 -4
  23. package/lib/env_node.js +15 -4
  24. package/lib/env_nodeworker.js +21 -1
  25. package/package.json +8 -5
  26. package/plugins/serviceAws/aws/aws-sdk-node.min.js +2 -2
  27. package/plugins/serviceAws/aws/aws-sdk.min.js +3 -2
  28. package/plugins/serviceAws/aws_dynamo.nlg +19 -19
  29. package/plugins/serviceYC/generic_api.yaml +36 -0
  30. package/plugins/serviceYC/naanide-relay-index.js +148 -0
  31. package/plugins/serviceYC/serviceYC.nlg +60 -0
  32. package/plugins/serviceYC/yc_apigateway.nlg +205 -0
  33. package/plugins/serviceYC/yc_function.nlg +163 -0
  34. package/plugins/serviceYC/yc_vm.nlg +207 -0
  35. package/test/test_07_lingo.nlg +1 -1
@@ -191,21 +191,23 @@ closure DynaConverter(local conv) {
191
191
  }
192
192
 
193
193
  // datumNaanToDyn
194
- // Convert a Naan datum to DynamoDB format.
194
+ // Convert a Naan datum to DynamoDB format. If no valid conversion is known then it becomes NULL
195
+ // in DynamoDB and a debug warning is logged. Note that this is not called for arbitrary symbols
196
+ // from genAttributeValueAdder() because it uses symbols to refer to actual attributes by name.
195
197
  //
196
198
  conv.datumNaanToDyn = function datumNaanToDyn(datum, local output, ntype, dtype) {
197
199
  output = { }
198
200
  if tuple(datum)
199
201
  datum = datum.toarray
200
- if array(datum) {
202
+ if Array.isArray(datum) {
201
203
  dtype = "L" // heterogenous list
202
204
  datum = datum.map(function(item) {
203
205
  datumNaanToDyn(item)
204
206
  })
205
207
  } else {
206
208
  ntype = typeof(datum)
207
- if ntype == xobject
208
- dtype = "B" // assume binary
209
+ if ntype == xobject && jsTypedArray(datum)
210
+ dtype = "B" // binary/blob
209
211
  else if ntype == dictionary {
210
212
  dtype = "M"
211
213
  datum = recordNaanToDyn(datum)
@@ -217,21 +219,19 @@ closure DynaConverter(local conv) {
217
219
  else if ntype == string
218
220
  dtype = "S"
219
221
  else if ntype == symbol {
220
- if datum === null {
221
- dtype = "NULL"
222
- datum = "true"
223
- }
224
- else if datum === true || datum === false
222
+ if datum === true || datum === false
225
223
  dtype = "BOOL"
226
- else if datum === undefined
227
- return (undefined) // Zen!
228
- else
229
- return (datum.tostring)
230
-
231
- } else
224
+ else if datum !== null {
225
+ debuglog("DynaConverter.datumNaanToDyn: nonstandard symbol", datum)
226
+ datum = null }
227
+ } else {
232
228
  debuglog("DynaConverter.datumNaanToDyn: invalid type", ntype)
229
+ datum = null }
233
230
  }
234
- output[dtype] = datum
231
+ if datum === null
232
+ output["NULL"] = true
233
+ else
234
+ output[dtype] = datum
235
235
  output
236
236
  }
237
237
 
@@ -277,7 +277,7 @@ closure DynaConverter(local conv) {
277
277
  // Convert a DynamoDB typed item dictionary to Naan format.
278
278
  //
279
279
  conv.recordDynToNaan = function recordDynToNaan(ditem, local output, key, data, dtype, dvalue) {
280
- if ditem.constructor === Array.prototype.constructor || array(ditem) { // process an array
280
+ if Array.isArray(ditem) { // process an array
281
281
  output = []
282
282
  for data in ditem
283
283
  output.push(recordDynToNaan(data))
@@ -429,7 +429,7 @@ closure DynaConverter(local conv) {
429
429
  attval = genAttributeValueAdder(params)
430
430
  attname = genAttributeNameAdder(params)
431
431
  for `(rangename, ranges) in conds {
432
- if !array(ranges)
432
+ if !Array.isArray(ranges)
433
433
  ranges = [ranges]
434
434
  for range in ranges {
435
435
  rangex = false
@@ -930,7 +930,7 @@ closure DynaTable(dyna, tablename, options, local table) {
930
930
  remaining -= processed
931
931
  params.ExclusiveStartKey = data.LastEvaluatedKey
932
932
  if processed && remaining > 0 && data.LastEvaluatedKey {
933
- params.limit = remaining // set up for next iteration
933
+ params.Limit = remaining // set up for next iteration
934
934
  true // continue looping
935
935
  }
936
936
  })
@@ -0,0 +1,36 @@
1
+ openapi: 3.0.0
2
+ info:
3
+ title: tutor-backend API
4
+ version: 1.0.0
5
+ servers:
6
+ - url: https://d5dhrgakpe39e11lqefr.apigw.yandexcloud.net
7
+ paths:
8
+ /{proxy+}:
9
+ options:
10
+ parameters:
11
+ - name: proxy
12
+ in: path
13
+ required: true
14
+ schema:
15
+ type: string
16
+ x-yc-apigateway-integration:
17
+ type: dummy
18
+ http_code: 200
19
+ http_headers:
20
+ Access-Control-Allow-Headers: "*"
21
+ Access-Control-Allow-Methods: "DELETE,GET,HEAD,OPTIONS,PATCH,POST,PUT"
22
+ Access-Control-Allow-Origin: "*"
23
+ content:
24
+ "application/json": ""
25
+ x-yc-apigateway-any-method:
26
+ parameters:
27
+ - name: proxy
28
+ in: path
29
+ required: true
30
+ schema:
31
+ type: string
32
+ x-yc-apigateway-integration:
33
+ type: cloud_functions
34
+ function_id: d4ehjjcfk8ffe43o5t7j
35
+ tag: $latest
36
+ service_account_id: ajeuftvbtshn348ert80
@@ -0,0 +1,148 @@
1
+ /*
2
+ * index.js
3
+ * naanide-relay
4
+ *
5
+ * Yandex Cloud Function for proxying API requests from NaanIDE in the browser.
6
+ *
7
+ * This function acts as a relay (mitm) proxy so that a browser may access Yandex Cloud APIs without
8
+ * needing a transient access token and without CORS problems. Security is provided by a pre-shared
9
+ * static key, configured as an environment variable in the YCF and known to the browser code.
10
+ *
11
+ * The request to the proxy should be precisely what is intended for the target API, except that the
12
+ * following headers should be included:
13
+ *
14
+ * x-naanide-auth: <pre-shared-key>
15
+ * x-naanide-ep: <target-url-with-path-and-query-variables>
16
+ * x-naanide-hdrs: <optional headers>
17
+ *
18
+ * The response in the browser contains some remapped headers, but is otherwise the API's response.
19
+ *
20
+ * Assuming the pre-shared key is "foo", the following gets a list of YDB databases in a folder:
21
+
22
+ curl -H "x-naanide-auth: foo" \
23
+ -H "x-naanide-ep: https://ydb.api.cloud.yandex.net/ydb/v1/databases?folderId=b1gjjahqm5u9tjolom9l" \
24
+ 'https://functions.yandexcloud.net/d4e5gicpajcqdbj91jt8'
25
+
26
+ *
27
+ * column positioning: // // !
28
+ *
29
+ * Copyright (c) 2022 by Richard C. Zulch
30
+ *
31
+ */
32
+
33
+
34
+ /*
35
+ * index.handler
36
+ *
37
+ * Forward a request and respond with the result.
38
+ *
39
+ */
40
+
41
+ module.exports.handler = async function (event, context) {
42
+ "use strict";
43
+ const authorizationKey = process.env.NAANIDE_AUTH_KEY;
44
+
45
+ //
46
+ // OPTIONS request
47
+ //
48
+ // Allow our headers for CORS
49
+ //
50
+
51
+ if (event.httpMethod == "OPTIONS")
52
+ return {
53
+ statusCode: 200,
54
+ headers: {
55
+ "Access-Control-Allow-Headers": "*"
56
+ }
57
+ };
58
+
59
+ //
60
+ // Failed authentication
61
+ //
62
+
63
+ if (event.headers["X-Naanide-Auth"] != authorizationKey) {
64
+ return {
65
+ statusCode: 401
66
+ };
67
+ }
68
+
69
+ //
70
+ // Authenticated
71
+ //
72
+
73
+ let https = require("https");
74
+ let url = event.headers["X-Naanide-Ep"];
75
+ let headers = event.headers["X-Naanide-Hdrs"];
76
+ if (headers) {
77
+ try {
78
+ headers = JSON.parse(headers);
79
+ } catch (e) {
80
+ console.log("X-Naanide-Hdrs parse error", e);
81
+ headers = false;
82
+ }
83
+ }
84
+ if (!headers)
85
+ headers = { };
86
+ if (!headers[Object.keys(headers).find(key => key.toLowerCase() === "authorization")])
87
+ headers.Authorization = context.token.token_type.concat(" ", context.token.access_token);
88
+ let options = {
89
+ method: event.httpMethod,
90
+ headers: headers
91
+ };
92
+ let chunks = [];
93
+ let content = false;
94
+ let finalResponse = {
95
+ statusCode: 500
96
+ };
97
+
98
+ //
99
+ // forward_request
100
+ //
101
+ async function forward_request() {
102
+ return new Promise(function(resolve, reject) {
103
+ let req = https.request(url, options, function(resp) {
104
+ finalResponse.statusCode = resp.statusCode;
105
+ // data event
106
+ resp.on("data", function (chunk) {
107
+ chunks.push(chunk);
108
+ });
109
+ // end event
110
+ resp.on("end", function () {
111
+ if (resp.complete)
112
+ {
113
+ let content = Buffer.concat(chunks);
114
+ let contentType = resp.headers["content-type"];
115
+ finalResponse.body = content.toString();
116
+ if (resp.headers["x-request-id"]) {
117
+ resp.headers["x-upstream-request-id"] = resp.headers["x-request-id"];
118
+ delete resp.headers["x-request-id"];
119
+ }
120
+ finalResponse.headers = resp.headers;
121
+ }
122
+ else {
123
+ console.log("request terminated before full response received");
124
+ finalResponse.statusCode = 502;
125
+ }
126
+ resolve();
127
+ });
128
+ });
129
+ // error event
130
+ req.on("error", function (err) {
131
+ console.log("upstream request error:", err);
132
+ finalResponse.statusCode = 502;
133
+ reject(err);
134
+ });
135
+ console.log("event.body", event.body, event.isBase64Encoded);
136
+ if (event.body) {
137
+ if (event.isBase64Encoded)
138
+ req.write(decodeURIComponent(escape(atob(event.body))));
139
+ else
140
+ req.write(event.body);
141
+ }
142
+ req.end();
143
+ });
144
+ }
145
+
146
+ await forward_request();
147
+ return finalResponse;
148
+ }
@@ -0,0 +1,60 @@
1
+ /*
2
+ * serviceYC.nlg
3
+ * serviceYC
4
+ *
5
+ * ServiceYC module configuration and management for browser and NodeJS.
6
+ *
7
+ * column positioning: // // !
8
+ *
9
+ * Copyright (c) 2022 by Richard C. Zulch
10
+ *
11
+ */
12
+
13
+
14
+ /*
15
+ * YcRequest
16
+ *
17
+ * Make a request to a Yandex Cloud API using a Yandex Cloud Function acting as a relay proxy.
18
+ * This works like frameworks/browser/HttpsApiRequest and frameworks/node/HttpsApiRequest except
19
+ * that it takes a credentials argument for access to the relay. Technically this is not required
20
+ * when we are going from node, but I'm keeping it for consistency.
21
+ *
22
+ */
23
+
24
+ closure YcRequest(url, creds, options, local headers) {
25
+ if !options
26
+ options = { }
27
+ else
28
+ options = new(options)
29
+ headers = [
30
+ list("x-naanide-auth", creds.relayAuthKey)
31
+ list("x-naanide-ep", url)
32
+ ]
33
+ if options.headers
34
+ headers.push(list("x-naanide-hdrs", options.headers.join("\n")))
35
+ options.headers = headers
36
+ https.HttpsApiRequest(creds.relayURL, options)
37
+ };
38
+
39
+
40
+ /*
41
+ * seycInit
42
+ *
43
+ * Initialize the YC module.
44
+ *
45
+ */
46
+
47
+ function seycInit(local manifest) {
48
+ manifest = `(YcRequest, seycInit)
49
+
50
+ Naan.module.build(module.id, "serviceYC", function(modobj, compobj) {
51
+ compobj.manifest = manifest
52
+ require("naanlib:frameworks/common").LiveImport()
53
+ if js.g {
54
+ nodeHttp = js.r("http")
55
+ https = require("naanlib:frameworks/node/https_request.nlg") }
56
+ else
57
+ https = require("naanlib:frameworks/browser/https_request.nlg")
58
+ module.exports.YcRequest = YcRequest
59
+ })
60
+ } ();
@@ -0,0 +1,205 @@
1
+ /*
2
+ * yc_apigateway.nlg
3
+ * serviceYC
4
+ *
5
+ * Support for YC API Gateway and websockets.
6
+ *
7
+ * column positioning: // // !
8
+ *
9
+ * Copyright (c) 2024 by Richard C. Zulch
10
+ *
11
+ */
12
+
13
+
14
+ /*
15
+ * Gateway
16
+ *
17
+ * Handler for incoming websocket messages from the YC API Gateway.
18
+ *
19
+ */
20
+
21
+ closure Gateway(local apig) {
22
+ global(https)
23
+ apig = new(object, this)
24
+
25
+ // open
26
+ //
27
+ // Initialize for a new gateway, but we don't know anything about it yet. This should ensure we
28
+ // can talk with the cloud, returning a result tuple. The first message that comes in will have
29
+ // headers that identify the gateway resource specifically.
30
+ //
31
+ apig.open = closure open() {
32
+ apig.ycvm = VmControl()
33
+ }
34
+
35
+ // close
36
+ //
37
+ // Close this handler because no more requests will come in.
38
+ //
39
+ apig.close = closure close() {
40
+ apig.ycvm = false
41
+ }
42
+
43
+ // requested
44
+ //
45
+ // A new websocket operation was requested. The ExpressJS request/response structures are passed.
46
+ //
47
+ // The requested return value for connect:
48
+ // {
49
+ // op: "connect"
50
+ // connID: <string>
51
+ // connAt: <integer>
52
+ // }
53
+ // The requested return value for disconnect:
54
+ // {
55
+ // op: "disconnect"
56
+ // connID: <string>
57
+ // code: <integer>
58
+ // reason: <string>
59
+ // }
60
+ // The requested return value for message:
61
+ // {
62
+ // op: "message"
63
+ // connID: <string>
64
+ // message: <string>
65
+ // contType: <string>
66
+ // }
67
+ //
68
+ // Example request headers from /wsgw endpoint:
69
+ // {
70
+ // user-agent : "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.0.0 Safari/537.36",
71
+ // accept-encoding : "gzip",
72
+ // accept-language : "en-US,en;q=0.9,ru;q=0.8",
73
+ // cache-control : "no-cache",
74
+ // host : "tutorbe-1.ru-central1.internal",
75
+ // origin : "https://d5di07c52f5hpi7h8kb8.apigw.yandexcloud.net",
76
+ // pragma : "no-cache",
77
+ // sec-websocket-extensions: "permessage-deflate; client_max_window_bits",
78
+ // uber-trace-id : "0000000000000000014a60f3326789e4:1a4ee8a332acff59:0:01",
79
+ // x-api-gateway-function-id: "d4e16asgl4t3i2j1m7t4",
80
+ // x-envoy-external-address: "64.201.120.56",
81
+ // x-envoy-original-path: "/?guid=1a5d070b-6aaf-4421-aa66-b13d15a7eaa3",
82
+ // x-forwarded-for : "64.201.120.56",
83
+ // x-forwarded-proto : "https",
84
+ // x-real-remote-address: "64.201.120.56",
85
+ // x-request-id : "ceaa9f57-4cb8-4fce-8081-c065b8073130",
86
+ // x-serverless-certificate-ids: "{}",
87
+ // x-serverless-gateway-id: "d5di07c52f5hpi7h8kb8",
88
+ // x-trace-id : "3e393eb7-4680-4d71-8b3d-824e5b768255",
89
+ // x-yc-apigateway-websocket-connected-at: "1725131521366",
90
+ // x-yc-apigateway-websocket-connection-id: "c05188b1jdifd7gsn7bqqochiv94l622u",
91
+ // x-yc-apigateway-websocket-event-type: "CONNECT",
92
+ // x-naanide-guid : "1a5d070b-6aaf-4421-aa66-b13d15a7eaa3",
93
+ // connection : "close"
94
+ // }
95
+ //
96
+ apig.requested = closure requested(req, res, local data, eventType) {
97
+ if !apig.gatewayID
98
+ apig.gatewayID = req.get("x-serverless-gateway-id")
99
+ data = {
100
+ connID: req.get("x-yc-apigateway-websocket-connection-id")
101
+ connAt: toint(req.get("x-yc-apigateway-websocket-connected-at"))
102
+ guid: req.get("x-naanide-guid") || undefined
103
+ }
104
+ eventType = req.get("x-yc-apigateway-websocket-event-type")
105
+ if eventType == "CONNECT" {
106
+ data.op = "connect"
107
+ }
108
+ else if eventType == "DISCONNECT" {
109
+ data.op = "disconnect"
110
+ data.code = req.get("X-Yc-Apigateway-Websocket-Disconnect-Status-Code")
111
+ data.reason = req.get("X-Yc-Apigateway-Websocket-Disconnect-Reason")
112
+ }
113
+ else if eventType == "MESSAGE" {
114
+ data.op = "message"
115
+ data.message = new(req.body)
116
+ data.contType = req.get('content-type')
117
+ } else
118
+ debuglog("serviceYC::Gateway.requested: invalid event type", eventType)
119
+ list(false, data)
120
+ }
121
+
122
+ // send
123
+ //
124
+ // Send a websocket message via the API Gateway.
125
+ //
126
+ apig.send = closure send(connID, msg, local error, authHeader, data) {
127
+ `(error, authHeader) = apig.ycvm.authHeader()
128
+ if !string(msg)
129
+ msg = JSONstringify(msg)
130
+ data = {
131
+ data: EncodeBase64(msg)
132
+ type: "TEXT" // allegedly "BINARY" is an option
133
+ }
134
+ if !error
135
+ `(error, data) = https.HttpsApiRequest("https://apigateway-connections.api.cloud.yandex.net/apigateways/websocket/v1/connections/${connID}:send", {
136
+ method: "POST"
137
+ headers: [ authHeader ]
138
+ putdata: data
139
+ debug: true
140
+ })
141
+ if error {
142
+ ErrorDebuglog("serviceYC::Gateway.send failed:", error)
143
+ list(error)
144
+ } else
145
+ list(false, data)
146
+ }
147
+
148
+ // get
149
+ //
150
+ // Get websocket connection status from the API Gateway.
151
+ //
152
+ apig.get = closure get(connID, local error, authHeader, data) {
153
+ `(error, authHeader) = apig.ycvm.authHeader()
154
+ if !error
155
+ `(error, data) = https.HttpsApiRequest("https://apigateway-connections.api.cloud.yandex.net/apigateways/websocket/v1/connections/${connID}", {
156
+ method: "GET"
157
+ headers: [ authHeader ]
158
+ })
159
+ if error {
160
+ ErrorDebuglog("serviceYC::Gateway.get failed:", error)
161
+ list(error)
162
+ } else
163
+ list(false, data)
164
+ }
165
+
166
+ // disconnect
167
+ //
168
+ // Disconnect a websocket connection from the API Gateway.
169
+ //
170
+ apig.disconnect = closure disconnect(connID, local error, authHeader, data) {
171
+ `(error, authHeader) = apig.ycvm.authHeader()
172
+ if !error
173
+ `(error, data) = https.HttpsApiRequest("https://apigateway-connections.api.cloud.yandex.net/apigateways/websocket/v1/connections/${connID}", {
174
+ method: "DELETE"
175
+ headers: [ authHeader ]
176
+ })
177
+ if error {
178
+ ErrorDebuglog("serviceYC::Gateway.disconnect failed:", error)
179
+ list(error)
180
+ } else
181
+ list(false, data)
182
+ }
183
+
184
+ // finis
185
+
186
+ apig
187
+ };
188
+
189
+
190
+ /*
191
+ * ycagInit
192
+ *
193
+ * Initialize the YC module.
194
+ *
195
+ */
196
+
197
+ function ycagInit(local manifest) {
198
+ manifest = `(Gateway, ycagInit)
199
+
200
+ Naan.module.build(module.id, "yc_apigateway", function(modobj, compobj) {
201
+ require("./yc_vm.nlg")
202
+ compobj.manifest = manifest
203
+ module.exports.Gateway = Gateway
204
+ })
205
+ } ();