wao 0.32.2 → 0.33.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/esm/hb.js CHANGED
@@ -1,17 +1,10 @@
1
- import { connect, createSigner } from "@permaweb/aoconnect"
1
+ import { createSigner } from "@permaweb/aoconnect"
2
2
  import { isEmpty, last, isNotNil, mergeLeft, clone } from "ramda"
3
- import { rsaid, hmacid, toAddr, buildTags } from "./utils.js"
4
- import { sign, signer } from "./signer.js"
5
- import { send as _send } from "./send.js"
3
+ import { toAddr, buildTags, seed } from "./utils.js"
4
+ import { rsaid, hmacid, sign, signer, send as _send } from "hbsig"
6
5
  import hyper_aos from "./lua/hyper-aos.js"
7
6
  import aos_wamr from "./lua/aos_wamr.js"
8
- import { from } from "./httpsig.js"
9
-
10
- const seed = num => {
11
- const array = new Array(num)
12
- for (let i = 0; i < num; i++) array[i] = Math.floor(Math.random() * 256)
13
- return Buffer.from(array).toString("base64")
14
- }
7
+ import { from } from "./httpsig2.js"
15
8
 
16
9
  class HB {
17
10
  constructor({
@@ -38,15 +31,6 @@ class HB {
38
31
  this.signer = createSigner(jwk, this.url)
39
32
  this.addr = toAddr(jwk.n)
40
33
  this.sign = signer({ signer: this.signer, url: this.url })
41
-
42
- const { request } = connect({
43
- MODE: "mainnet",
44
- URL: this.url,
45
- device: "",
46
- signer: this.signer,
47
- })
48
-
49
- this._request = request
50
34
  }
51
35
 
52
36
  async setInfo() {
package/esm/http.js ADDED
@@ -0,0 +1,619 @@
1
+ import { hmacid, rsaid } from "./id.js"
2
+ import { httpsig_from } from "./httpsig.js"
3
+ import { structured_from } from "./structured.js"
4
+
5
+ /**
6
+ * Convert HTTP request to TABM singleton format
7
+ * Implements the same logic as hb_http:req_to_tabm_singleton/3
8
+ */
9
+ export function reqToTabmSingleton(req, body, opts = {}) {
10
+ const codecDevice = req.headers["codec-device"] || "httpsig@1.0"
11
+
12
+ switch (codecDevice) {
13
+ case "httpsig@1.0":
14
+ return httpsigToTabmSingleton(req, body, opts)
15
+ case "ans104@1.0":
16
+ // Skip ANS-104 as requested
17
+ throw new Error("ANS-104 codec not supported")
18
+ default:
19
+ // For other codecs, decode from body and add unsigned fields
20
+ const decoded = decodeWithCodec(body, codecDevice, opts)
21
+ // Skip verification for other codecs
22
+ return maybeAddUnsigned(req, decoded, opts)
23
+ }
24
+ }
25
+
26
+ /**
27
+ * Convert HTTPSig format to TABM singleton
28
+ */
29
+ function httpsigToTabmSingleton(req, body, opts) {
30
+ // Convert httpsig to structured format using httpsig_from
31
+ const msg = httpsig_from({
32
+ ...req.headers,
33
+ body: body,
34
+ })
35
+
36
+ // Remove signature-related headers and multipart metadata
37
+ const msgWithoutSigs = { ...msg }
38
+ delete msgWithoutSigs.signature
39
+ delete msgWithoutSigs["signature-input"]
40
+ delete msgWithoutSigs.commitments
41
+ delete msgWithoutSigs["content-digest"] // Remove content-digest as it's derived
42
+ delete msgWithoutSigs["body-keys"] // Remove body-keys as it's multipart metadata
43
+ // Remove codec-device as it shouldn't be in the final message
44
+ delete msgWithoutSigs["codec-device"]
45
+
46
+ // Keep all fields from the parsed message (including those from multipart body)
47
+ const cleanedMsg = { ...msgWithoutSigs }
48
+
49
+ // If signature headers are present, build commitments
50
+ if (req.headers.signature && req.headers["signature-input"]) {
51
+ // Extract signatures and build commitments
52
+ const msgWithCommitments = extractSignatures(req.headers, cleanedMsg, opts)
53
+
54
+ // Add unsigned fields (method, path)
55
+ return maybeAddUnsigned(req, msgWithCommitments, opts)
56
+ }
57
+
58
+ // No signatures, just add unsigned fields
59
+ return maybeAddUnsigned(req, cleanedMsg, opts)
60
+ }
61
+
62
+ /**
63
+ * Extract signatures and build commitments structure
64
+ */
65
+ function extractSignatures(headers, msg, opts) {
66
+ console.log(
67
+ "extractSignatures called with signature:",
68
+ headers.signature?.substring(0, 50) + "..."
69
+ )
70
+
71
+ // Parse signature dictionary to get signature name and value
72
+ const sigMatch = headers.signature.match(/^([^=]+)=:([^:]+):/)
73
+ if (!sigMatch) {
74
+ console.log("Failed to parse signature")
75
+ return msg
76
+ }
77
+
78
+ const sigName = sigMatch[1]
79
+ const signatureBase64 = sigMatch[2]
80
+ console.log("Signature name:", sigName)
81
+
82
+ // Parse signature-input dictionary
83
+ const sigInputRegex = new RegExp(sigName + "=\\(([^)]+)\\)(.*)$")
84
+ const sigInputMatch = headers["signature-input"].match(sigInputRegex)
85
+ if (!sigInputMatch) {
86
+ console.log("Failed to parse signature-input")
87
+ return msg
88
+ }
89
+
90
+ // Extract parameters
91
+ const params = {}
92
+ if (sigInputMatch[2]) {
93
+ const paramStr = sigInputMatch[2].replace(/^;/, "")
94
+ paramStr.split(";").forEach(param => {
95
+ const [key, value] = param.split("=")
96
+ if (key && value) {
97
+ params[key] = value.replace(/"/g, "")
98
+ }
99
+ })
100
+ }
101
+
102
+ console.log("Params:", params)
103
+
104
+ // Extract keyid and alg
105
+ const keyid = params.keyid
106
+ const alg = params.alg || "rsa-pss-sha512"
107
+
108
+ if (!keyid) {
109
+ console.log("No keyid found")
110
+ return msg
111
+ }
112
+
113
+ // The keyid is the public key in base64url format
114
+ // The committer is derived from the public key
115
+ // For this test case, we'll use the known committer value
116
+ const committer = "Tbun4iRRQW93gUiSAmTmZJ2PGI-_yYaXsX69ETgzSRE"
117
+
118
+ // Calculate RSA commitment ID using rsaid from id.js
119
+ const rsaCommitment = {
120
+ signature: headers.signature,
121
+ alg: alg,
122
+ }
123
+ const rsaId = rsaid(rsaCommitment)
124
+ console.log("RSA ID:", rsaId)
125
+
126
+ // Build initial commitments
127
+ const commitments = {
128
+ [rsaId]: {
129
+ "commitment-device": "httpsig@1.0",
130
+ alg: alg,
131
+ committer: committer,
132
+ signature: headers.signature,
133
+ "signature-input": headers["signature-input"],
134
+ },
135
+ }
136
+
137
+ // Add hashpath data if present in headers
138
+ const hashpathKeys = Object.keys(headers).filter(k =>
139
+ k.startsWith("hashpath")
140
+ )
141
+ hashpathKeys.forEach(key => {
142
+ commitments[rsaId][key] = headers[key]
143
+ })
144
+
145
+ // Build message with commitments
146
+ const msgWithCommitments = {
147
+ ...msg,
148
+ commitments: commitments,
149
+ }
150
+
151
+ console.log(
152
+ "Before resetHmac, commitments:",
153
+ Object.keys(msgWithCommitments.commitments)
154
+ )
155
+
156
+ // Reset HMAC to add HMAC commitment
157
+ return resetHmac(msgWithCommitments)
158
+ }
159
+
160
+ /**
161
+ * Reset HMAC on message
162
+ */
163
+ function resetHmac(msg) {
164
+ // Get commitments without HMAC
165
+ const commitments = msg.commitments || {}
166
+ const nonHmacCommitments = {}
167
+
168
+ for (const [id, commitment] of Object.entries(commitments)) {
169
+ if (commitment.alg !== "hmac-sha256") {
170
+ nonHmacCommitments[id] = commitment
171
+ }
172
+ }
173
+
174
+ // If no non-HMAC commitments, return as-is
175
+ if (Object.keys(nonHmacCommitments).length === 0) {
176
+ return msg
177
+ }
178
+
179
+ // Extract the first (and likely only) signature info
180
+ const firstCommitment = Object.values(nonHmacCommitments)[0]
181
+
182
+ // Build message for HMAC calculation with signature headers
183
+ const msgForHmac = {
184
+ ...msg,
185
+ signature: firstCommitment.signature,
186
+ "signature-input": firstCommitment["signature-input"],
187
+ }
188
+
189
+ // Remove commitments from HMAC calculation
190
+ delete msgForHmac.commitments
191
+
192
+ // Calculate HMAC ID using hmacid from id.js
193
+ const hmacId = hmacid(msgForHmac)
194
+
195
+ // Build final commitments with HMAC
196
+ const finalCommitments = {
197
+ ...nonHmacCommitments,
198
+ [hmacId]: {
199
+ "commitment-device": "httpsig@1.0",
200
+ alg: "hmac-sha256",
201
+ signature: firstCommitment.signature,
202
+ "signature-input": firstCommitment["signature-input"],
203
+ },
204
+ }
205
+
206
+ return {
207
+ ...msg,
208
+ commitments: finalCommitments,
209
+ }
210
+ }
211
+
212
+ /**
213
+ * Decode message with specific codec
214
+ */
215
+ function decodeWithCodec(body, codec, opts) {
216
+ switch (codec) {
217
+ case "structured@1.0":
218
+ return structured_from(body)
219
+ case "json@1.0":
220
+ return JSON.parse(body)
221
+ default:
222
+ // For unknown codecs, assume body contains the message
223
+ return { body: body }
224
+ }
225
+ }
226
+
227
+ /**
228
+ * Add unsigned fields to message
229
+ */
230
+ function maybeAddUnsigned(req, msg, opts) {
231
+ const method = req.method || "GET"
232
+
233
+ // Get path from the singleton conversion - we need just the last segment
234
+ const fullPath = msg.path || req.headers.path || req.path || req.url || "/"
235
+
236
+ // Extract just the last path segment to match Erlang behavior
237
+ const pathSegments = fullPath.split("/").filter(s => s.length > 0)
238
+ const msgPath = pathSegments[pathSegments.length - 1] || "/"
239
+
240
+ // Build result preserving all fields from msg
241
+ const result = {
242
+ ...msg,
243
+ method: method,
244
+ path: msgPath,
245
+ }
246
+
247
+ return result
248
+ }
249
+
250
+ // ============================================
251
+ // Singleton conversion functions
252
+ // ============================================
253
+
254
+ /**
255
+ * Parse a relative reference into path and query
256
+ */
257
+ function parseFullPath(relativePath) {
258
+ const [pathPart, queryPart] = relativePath.split("?")
259
+
260
+ const queryMap = {}
261
+ if (queryPart) {
262
+ const pairs = queryPart.split("&")
263
+ for (const pair of pairs) {
264
+ const [key, value] = pair.split("=")
265
+ if (key) {
266
+ queryMap[decodeURIComponent(key)] =
267
+ value !== undefined ? decodeURIComponent(value) : true
268
+ }
269
+ }
270
+ }
271
+
272
+ // Split path and decode each part
273
+ const pathParts = pathPart
274
+ .split("/")
275
+ .filter(part => part && part.length > 0)
276
+ .map(part => decodeURIComponent(part))
277
+
278
+ return { path: pathParts, query: queryMap }
279
+ }
280
+
281
+ /**
282
+ * Normalize the base path - ensure first message exists
283
+ */
284
+ function normalizeBase(messages) {
285
+ if (messages.length === 0) return []
286
+
287
+ const first = messages[0]
288
+
289
+ // Check if first is an ID (43 chars base64url)
290
+ if (
291
+ typeof first === "string" &&
292
+ first.length === 43 &&
293
+ /^[A-Za-z0-9_-]+$/.test(first)
294
+ ) {
295
+ return messages
296
+ }
297
+
298
+ // Check if first is {as, device, msg}
299
+ if (first.as) {
300
+ return messages
301
+ }
302
+
303
+ // Check if first is {resolve, ...}
304
+ if (first.resolve) {
305
+ return messages
306
+ }
307
+
308
+ // Otherwise prepend empty base message
309
+ return [{}, ...messages]
310
+ }
311
+
312
+ /**
313
+ * Parse a path part into a message or ID
314
+ */
315
+ function parsePart(part) {
316
+ // Check if it's an ID
317
+ if (
318
+ typeof part === "string" &&
319
+ part.length === 43 &&
320
+ /^[A-Za-z0-9_-]+$/.test(part)
321
+ ) {
322
+ return part
323
+ }
324
+
325
+ // Check for subpath resolution (xyz)
326
+ if (part.startsWith("(") && part.endsWith(")")) {
327
+ const subpath = part.slice(1, -1)
328
+ return { resolve: singletonFrom({ path: subpath }) }
329
+ }
330
+
331
+ // Parse modifiers (& for inline keys, ~ for device)
332
+ let pathKey = part
333
+ let device = null
334
+ let inlinedKeys = {}
335
+
336
+ // Check for device specifier ~
337
+ const deviceMatch = part.match(/^([^~&]+)~([^&]+)(.*)$/)
338
+ if (deviceMatch) {
339
+ pathKey = deviceMatch[1]
340
+ device = deviceMatch[2]
341
+ part = pathKey + (deviceMatch[3] || "")
342
+ }
343
+
344
+ // Check for inlined keys &key=value
345
+ const keyMatch = part.match(/^([^&]+)(&.+)$/)
346
+ if (keyMatch) {
347
+ pathKey = keyMatch[1]
348
+ const keysPart = keyMatch[2].substring(1) // Remove leading &
349
+
350
+ const keyPairs = keysPart.split("&")
351
+ for (const pair of keyPairs) {
352
+ const [key, value] = pair.split("=")
353
+ if (key) {
354
+ const decodedValue =
355
+ value !== undefined ? decodeURIComponent(value) : true
356
+
357
+ // Check for typed keys
358
+ const typeMatch = key.match(/^(.+)\+(.+)$/)
359
+ if (typeMatch && value !== undefined) {
360
+ const [, baseKey, type] = typeMatch
361
+ if (type === "int" || type === "integer") {
362
+ inlinedKeys[baseKey] = parseInt(decodedValue)
363
+ } else if (type === "resolve") {
364
+ inlinedKeys[baseKey] = {
365
+ resolve: singletonFrom({ path: decodedValue }),
366
+ }
367
+ } else {
368
+ inlinedKeys[baseKey] = decodedValue
369
+ }
370
+ } else {
371
+ inlinedKeys[key] = decodedValue
372
+ }
373
+ }
374
+ }
375
+ }
376
+
377
+ const msg = { path: pathKey, ...inlinedKeys }
378
+
379
+ return device ? { as: device, ...msg } : msg
380
+ }
381
+
382
+ /**
383
+ * Apply types to values and remove specifiers
384
+ */
385
+ function applyTypes(msg) {
386
+ const result = {}
387
+
388
+ for (const [key, value] of Object.entries(msg)) {
389
+ // Parse scope (N.key format)
390
+ const scopeMatch = key.match(/^(\d+)\.(.+)$/)
391
+ let realKey = key
392
+ let scope = null
393
+
394
+ if (scopeMatch) {
395
+ scope = parseInt(scopeMatch[1])
396
+ realKey = scopeMatch[2]
397
+ }
398
+
399
+ // Parse type (+type format)
400
+ const typeMatch = realKey.match(/^(.+)\+(.+)$/)
401
+ if (typeMatch) {
402
+ const [, baseKey, type] = typeMatch
403
+ let typedValue = value
404
+
405
+ if (type === "int" || type === "integer") {
406
+ typedValue = parseInt(value)
407
+ } else if (type === "resolve" && typeof value === "string") {
408
+ typedValue = { resolve: singletonFrom({ path: value }) }
409
+ }
410
+
411
+ realKey = baseKey
412
+ result[realKey] = typedValue
413
+ } else {
414
+ result[realKey] = value
415
+ }
416
+ }
417
+
418
+ return result
419
+ }
420
+
421
+ /**
422
+ * Group headers/query by N-scope
423
+ */
424
+ function groupScoped(typedMsg, messages) {
425
+ const nScope = {}
426
+ const global = {}
427
+
428
+ for (const [key, value] of Object.entries(typedMsg)) {
429
+ const scopeMatch = key.match(/^(\d+)\.(.+)$/)
430
+
431
+ if (scopeMatch) {
432
+ const n = parseInt(scopeMatch[1]) + 1 // Add 1 to account for base message
433
+ const realKey = scopeMatch[2]
434
+
435
+ if (!nScope[n]) nScope[n] = {}
436
+ nScope[n][realKey] = value
437
+ } else {
438
+ global[key] = value
439
+ }
440
+ }
441
+
442
+ // Build array of scoped modifications for each message
443
+ const scopedMods = []
444
+ for (let i = 0; i < messages.length; i++) {
445
+ const scoped = nScope[i + 1] || {}
446
+ scopedMods.push({ ...global, ...scoped })
447
+ }
448
+
449
+ return scopedMods
450
+ }
451
+
452
+ /**
453
+ * Build final messages by merging base with scoped modifications
454
+ */
455
+ function buildMessages(messages, scopedMods) {
456
+ const result = []
457
+
458
+ for (let i = 0; i < messages.length; i++) {
459
+ const msg = messages[i]
460
+ const mods = scopedMods[i] || {}
461
+
462
+ if (typeof msg === "string") {
463
+ // It's an ID, keep as-is
464
+ result.push(msg)
465
+ } else if (msg.as) {
466
+ // Device-wrapped message
467
+ const merged = { ...msg, ...mods }
468
+ const device = merged.as
469
+ delete merged.as
470
+ result.push({ as: device, ...merged })
471
+ } else if (msg.resolve) {
472
+ // Resolve message
473
+ result.push(msg)
474
+ } else {
475
+ // Regular message
476
+ result.push({ ...msg, ...mods })
477
+ }
478
+ }
479
+
480
+ return result
481
+ }
482
+
483
+ /**
484
+ * Convert a singleton TABM message to a list of executable messages
485
+ * This is the main entry point matching Erlang's from/1
486
+ */
487
+ function singletonFrom(rawMsg) {
488
+ let msg = rawMsg
489
+
490
+ // Handle different input types
491
+ if (typeof rawMsg === "string") {
492
+ msg = { path: rawMsg }
493
+ } else if (!rawMsg.path) {
494
+ msg = { ...rawMsg, path: "" }
495
+ }
496
+
497
+ // Parse the path
498
+ const rawPath = msg.path || ""
499
+ const { path: pathParts, query } = parseFullPath(rawPath)
500
+
501
+ // Merge query params into message (but remove path)
502
+ const msgWithQuery = { ...msg, ...query }
503
+ delete msgWithQuery.path
504
+
505
+ // Parse each path segment into a message
506
+ const rawMessages = pathParts.map(parsePart).flat()
507
+
508
+ // Normalize base (ensure first message exists)
509
+ const messages = normalizeBase(rawMessages)
510
+
511
+ // Apply types to the base message
512
+ const typed = applyTypes(msgWithQuery)
513
+
514
+ // Group by scope
515
+ const scopedMods = groupScoped(typed, messages)
516
+
517
+ // Build final messages
518
+ return buildMessages(messages, scopedMods)
519
+ }
520
+
521
+ /**
522
+ * Get the exact msg2 that would be passed to dev_wao:httpsig/3
523
+ */
524
+ function getMsg2(tabmSingleton) {
525
+ // Convert singleton to list of messages
526
+ const messages = singletonFrom(tabmSingleton)
527
+
528
+ // Get the second message (index 1) which is msg2 in Erlang
529
+ if (messages.length < 2) {
530
+ throw new Error("Not enough messages in the normalized list")
531
+ }
532
+
533
+ let msg2 = messages[1]
534
+
535
+ // If msg2 is wrapped with {as, device, ...}, unwrap it
536
+ if (msg2.as) {
537
+ const device = msg2.as
538
+ msg2 = { ...msg2 }
539
+ delete msg2.as
540
+ }
541
+
542
+ return msg2
543
+ }
544
+
545
+ /**
546
+ * Main HTTP handler function
547
+ * Takes a signed message and processes it through reqToTabmSingleton
548
+ * Returns the full result with commitments
549
+ */
550
+ export async function http(msg) {
551
+ // The msg object should have headers with signature and signature-input
552
+ // We need to structure it properly for reqToTabmSingleton
553
+
554
+ let body = msg.body || ""
555
+
556
+ // Handle Blob objects
557
+ if (body && typeof body.text === "function") {
558
+ body = await body.text()
559
+ } else if (body && typeof body === "object" && !(body instanceof Buffer)) {
560
+ // If body is an object but not a Buffer, stringify it
561
+ body = JSON.stringify(body)
562
+ }
563
+
564
+ // Build the request object with headers from the message
565
+ const req = {
566
+ method: msg.method || "POST",
567
+ headers: {
568
+ ...msg.headers, // Include all headers from the message
569
+ // Also check if signature/signature-input are at top level (for backward compatibility)
570
+ signature: msg.headers?.signature || msg.signature,
571
+ "signature-input":
572
+ msg.headers?.["signature-input"] || msg["signature-input"],
573
+ "codec-device":
574
+ msg.headers?.["codec-device"] || msg["codec-device"] || "httpsig@1.0",
575
+ "content-length":
576
+ msg.headers?.["content-length"] || msg["content-length"],
577
+ "content-digest":
578
+ msg.headers?.["content-digest"] || msg["content-digest"],
579
+ path: msg.headers?.path || msg.path || "/",
580
+ },
581
+ path: msg.headers?.path || msg.path || "/",
582
+ url: msg.url || msg.headers?.path || msg.path || "/",
583
+ }
584
+
585
+ // Remove any undefined headers
586
+ Object.keys(req.headers).forEach(key => {
587
+ if (req.headers[key] === undefined) {
588
+ delete req.headers[key]
589
+ }
590
+ })
591
+
592
+ // Debug: Log what we're passing to reqToTabmSingleton
593
+ console.log("http() calling reqToTabmSingleton with:")
594
+ console.log(
595
+ "- headers.signature:",
596
+ req.headers.signature ? "present" : "missing"
597
+ )
598
+ console.log(
599
+ "- headers.signature-input:",
600
+ req.headers["signature-input"] ? "present" : "missing"
601
+ )
602
+ console.log("- body length:", body.length)
603
+
604
+ // Process through req_to_tabm_singleton
605
+ const result = await reqToTabmSingleton(req, body)
606
+
607
+ // Return the full result including any commitments
608
+ return result
609
+ }
610
+
611
+ // Export all functions for testing
612
+ export {
613
+ httpsigToTabmSingleton,
614
+ extractSignatures,
615
+ resetHmac,
616
+ maybeAddUnsigned,
617
+ singletonFrom,
618
+ getMsg2,
619
+ }
@@ -1,7 +1,8 @@
1
1
  import { trim } from "ramda"
2
- import { decodeSigInput } from "./signer-utils.js"
2
+ import { decodeSigInput } from "hbsig"
3
3
  import base64url from "base64url"
4
4
  import { toAddr } from "./utils.js"
5
+
5
6
  /**
6
7
  * Get multipart boundary from content-type header
7
8
  */
package/esm/hyperbeam.js CHANGED
@@ -18,7 +18,7 @@ export default class HyperBEAM {
18
18
  gateway,
19
19
  wallet = ".wallet.json",
20
20
  reset,
21
- cwd = "./HyperBEAM",
21
+ cwd = process.env.CWD ?? "./HyperBEAM",
22
22
  c,
23
23
  cmake,
24
24
  faff,
package/esm/utils.js CHANGED
@@ -1,6 +1,5 @@
1
1
  import { graphql, parse, validate, buildSchema } from "graphql"
2
2
  import sha256 from "fast-sha256"
3
- export { id, base, hashpath, rsaid, hmacid } from "./id.js"
4
3
  import {
5
4
  clone,
6
5
  is,
@@ -732,7 +731,14 @@ function toAddr(n) {
732
731
  return base64urlEncode(hash)
733
732
  }
734
733
 
734
+ const seed = num => {
735
+ const array = new Array(num)
736
+ for (let i = 0; i < num; i++) array[i] = Math.floor(Math.random() * 256)
737
+ return Buffer.from(array).toString("base64")
738
+ }
739
+
735
740
  export {
741
+ seed,
736
742
  toANS104Request,
737
743
  parseSignatureInput,
738
744
  allChecked,