oydid 0.8.2 → 0.9.1
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.
- checksums.yaml +4 -4
- data/VERSION +1 -1
- data/lib/oydid/basic.rb +18 -0
- data/lib/oydid.rb +143 -13
- data/spec/output/main/sample0_read.doc +1 -1
- data/spec/oydid_spec.rb +274 -0
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 9c74b5cd9e592efe1514c9110cff25a131ddc0a45727e035d48656bc0c20d2f8
|
|
4
|
+
data.tar.gz: bd936eb03e018e15ae334907c7087629f48f17c2cc54bdeebb8a81542e2f2af9
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 8fddcdf2ce2328ba9329263b1de2826cec3c3d4947c7cd5e599972798cffb10887e8bf2205d208961494a7296a818c90a4fea679f2b9b1c01109645c8fde59bf
|
|
7
|
+
data.tar.gz: fdaab3ac52a14b4313db2d7d52058a41bdd6b72b2ca580441f5620a246d73d2fdf753a0530aef76d44520d8347573852aa6e3ae27d173371d54b09eeb86c874c
|
data/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
0.
|
|
1
|
+
0.9.1
|
data/lib/oydid/basic.rb
CHANGED
|
@@ -1216,6 +1216,24 @@ class Oydid
|
|
|
1216
1216
|
end
|
|
1217
1217
|
end
|
|
1218
1218
|
|
|
1219
|
+
# The identifier without its location suffix.
|
|
1220
|
+
#
|
|
1221
|
+
# A did:oyd can carry "@<location>" (raw or percent-encoded) to say where the
|
|
1222
|
+
# document is hosted. That is a resolution hint, not part of the identity:
|
|
1223
|
+
# the same document can be mirrored at any number of locations, so the set of
|
|
1224
|
+
# location-bound variants is open and cannot be enumerated. Everything that
|
|
1225
|
+
# states identity - canonicalId and equivalentId in the didDocumentMetadata -
|
|
1226
|
+
# therefore uses this location-free form. The location-bound variant stays in
|
|
1227
|
+
# alsoKnownAs.
|
|
1228
|
+
#
|
|
1229
|
+
# Note this must NOT be applied to the id of the DID document itself: DID Core
|
|
1230
|
+
# requires that "the value of the id property in the retrieved DID document
|
|
1231
|
+
# must always match the DID being resolved", so a location-bound DID that was
|
|
1232
|
+
# requested is echoed as requested (see Oydid.document_id).
|
|
1233
|
+
def self.strip_location(id)
|
|
1234
|
+
id.to_s.split(LOCATION_PREFIX).first.split(CGI.escape LOCATION_PREFIX).first rescue id.to_s
|
|
1235
|
+
end
|
|
1236
|
+
|
|
1219
1237
|
def self.get_location(id)
|
|
1220
1238
|
if id.include?(LOCATION_PREFIX)
|
|
1221
1239
|
id_split = id.split(LOCATION_PREFIX)
|
data/lib/oydid.rb
CHANGED
|
@@ -68,8 +68,15 @@ class Oydid
|
|
|
68
68
|
end
|
|
69
69
|
|
|
70
70
|
# setup
|
|
71
|
+
#
|
|
72
|
+
# did_requested is the identifier the caller asked for. dag_update
|
|
73
|
+
# overwrites "did" with every version it walks through, so by the time a
|
|
74
|
+
# DID document is rendered the requested identifier would be lost - and
|
|
75
|
+
# after an update that is exactly the one a relying party holds, e.g.
|
|
76
|
+
# printed on a product's data carrier.
|
|
71
77
|
currentDID = {
|
|
72
78
|
"did": did,
|
|
79
|
+
"did_requested": did,
|
|
73
80
|
"doc": "",
|
|
74
81
|
"log": [],
|
|
75
82
|
"doc_log_id": nil,
|
|
@@ -1426,6 +1433,130 @@ class Oydid
|
|
|
1426
1433
|
wd
|
|
1427
1434
|
end
|
|
1428
1435
|
|
|
1436
|
+
# Identifiers of every published version of a DID, oldest first.
|
|
1437
|
+
#
|
|
1438
|
+
# A did:oyd is the hash over its own DID document, so every update mints a
|
|
1439
|
+
# new identifier while the earlier ones stay resolvable through the log.
|
|
1440
|
+
# The version reached by traversing the log is the canonical identifier,
|
|
1441
|
+
# every other version identifier is logically equivalent to it - which is
|
|
1442
|
+
# what DID Core expresses as didDocumentMetadata canonicalId/equivalentId.
|
|
1443
|
+
#
|
|
1444
|
+
# This lives in the gem rather than in the resolving controllers because
|
|
1445
|
+
# there are three of them (repository resolve and resolve_full, plus the
|
|
1446
|
+
# uniresolver plugin) and they had drifted into computing this list with
|
|
1447
|
+
# different rules.
|
|
1448
|
+
#
|
|
1449
|
+
# Both values are location-free: the "@<location>" suffix says where a
|
|
1450
|
+
# document is hosted, not who it is, and the same document can be mirrored at
|
|
1451
|
+
# any number of locations - so the set of location-bound variants is open and
|
|
1452
|
+
# equivalentId could not state it correctly anyway. The raw values these are
|
|
1453
|
+
# built from do carry a location (dag_update sets did_info["did"] from the log
|
|
1454
|
+
# entry, and log[]["doc"] is written with one), which is why strip_location is
|
|
1455
|
+
# applied here rather than assumed. The location-bound variant stays in
|
|
1456
|
+
# alsoKnownAs; document_id is deliberately left alone (see strip_location).
|
|
1457
|
+
#
|
|
1458
|
+
# Returns [canonicalId, equivalentIds]; equivalentIds never contains the
|
|
1459
|
+
# identifier the DID document itself carries as `id` (see document_id) and is
|
|
1460
|
+
# empty - not a set with the DID itself in it - while there is only one
|
|
1461
|
+
# version.
|
|
1462
|
+
# keep_location = true reproduces the pre-0.9.1 values, location suffix and
|
|
1463
|
+
# all. Only w3c uses it, to keep alsoKnownAs listing the location-bound
|
|
1464
|
+
# variant of a DID - dropping that would take the location out of the
|
|
1465
|
+
# document altogether, and alsoKnownAs is where it belongs.
|
|
1466
|
+
#
|
|
1467
|
+
# Deliberately a positional argument: callers pass did_info as a braceless
|
|
1468
|
+
# hash literal (version_ids("did" => ..., "log" => ...)), and a method with
|
|
1469
|
+
# a keyword parameter swallows that hash as keywords instead - every such
|
|
1470
|
+
# call site would raise ArgumentError.
|
|
1471
|
+
def self.version_ids(did_info, keep_location = false)
|
|
1472
|
+
normalize = lambda do |id|
|
|
1473
|
+
id = keep_location ? id.to_s : strip_location(id.to_s)
|
|
1474
|
+
id = percent_encode(id)
|
|
1475
|
+
id = "did:oyd:" + id if !id.start_with?("did:oyd:")
|
|
1476
|
+
id
|
|
1477
|
+
end
|
|
1478
|
+
canonical = normalize.call(did_info["did"])
|
|
1479
|
+
own = document_id(did_info)
|
|
1480
|
+
equivalentIds = []
|
|
1481
|
+
did_info["log"].each do |log|
|
|
1482
|
+
if log["op"] == 2 || log["op"] == 3
|
|
1483
|
+
eid = normalize.call(log["doc"])
|
|
1484
|
+
if eid != own && !equivalentIds.include?(eid)
|
|
1485
|
+
equivalentIds << eid
|
|
1486
|
+
end
|
|
1487
|
+
end
|
|
1488
|
+
end unless did_info["log"].nil?
|
|
1489
|
+
[canonical, equivalentIds]
|
|
1490
|
+
end
|
|
1491
|
+
|
|
1492
|
+
# created / updated / versionId of the resolved document version, as DID Core
|
|
1493
|
+
# 7.1.3 defines them. Returned as a hash with string keys, ready to be merged
|
|
1494
|
+
# into didDocumentMetadata; a property the log cannot answer is absent rather
|
|
1495
|
+
# than null - in particular `updated`, which the spec requires to be "omitted
|
|
1496
|
+
# if an Update operation has never been performed on the DID document".
|
|
1497
|
+
#
|
|
1498
|
+
# The resolved version is did_info["did"] - dag_update walks the log to the
|
|
1499
|
+
# newest document and leaves its identifier there. Deriving both versionId and
|
|
1500
|
+
# the `updated` entry from it avoids guessing which log entry is the newest,
|
|
1501
|
+
# which timestamps alone cannot decide (they are client-supplied and two
|
|
1502
|
+
# entries can share a second).
|
|
1503
|
+
#
|
|
1504
|
+
# versionId is the bare document hash, without the "did:oyd:" prefix and
|
|
1505
|
+
# without a location: it is the method-specific identifier of that version, so
|
|
1506
|
+
# "did:oyd:" + versionId is the versioned DID.
|
|
1507
|
+
def self.version_metadata(did_info)
|
|
1508
|
+
as_datetime = lambda do |ts|
|
|
1509
|
+
# XML Datetime normalised to UTC, no sub-second precision (7.1.3)
|
|
1510
|
+
Time.at(ts.to_i).utc.strftime("%Y-%m-%dT%H:%M:%SZ") rescue nil
|
|
1511
|
+
end
|
|
1512
|
+
version_of = lambda do |id|
|
|
1513
|
+
strip_location(id.to_s).delete_prefix("did:oyd:")
|
|
1514
|
+
end
|
|
1515
|
+
|
|
1516
|
+
meta = {}
|
|
1517
|
+
resolved = version_of.call(did_info["did"])
|
|
1518
|
+
meta["versionId"] = resolved if resolved != ""
|
|
1519
|
+
return meta if did_info["log"].nil?
|
|
1520
|
+
|
|
1521
|
+
created_entry = did_info["log"].find { |el| el["op"].to_i == 2 }
|
|
1522
|
+
if !created_entry.nil? && !created_entry["ts"].nil?
|
|
1523
|
+
created = as_datetime.call(created_entry["ts"])
|
|
1524
|
+
meta["created"] = created if !created.nil?
|
|
1525
|
+
end
|
|
1526
|
+
|
|
1527
|
+
updated_entry = did_info["log"].find do |el|
|
|
1528
|
+
el["op"].to_i == 3 && version_of.call(el["doc"]) == resolved
|
|
1529
|
+
end
|
|
1530
|
+
if !updated_entry.nil? && !updated_entry["ts"].nil?
|
|
1531
|
+
updated = as_datetime.call(updated_entry["ts"])
|
|
1532
|
+
meta["updated"] = updated if !updated.nil?
|
|
1533
|
+
end
|
|
1534
|
+
|
|
1535
|
+
meta
|
|
1536
|
+
end
|
|
1537
|
+
|
|
1538
|
+
# The identifier a resolved DID document carries as `id`.
|
|
1539
|
+
#
|
|
1540
|
+
# A did:oyd is the hash over its own document, so an update mints a new one
|
|
1541
|
+
# and the resolution walks the log to the newest version. Answering with that
|
|
1542
|
+
# newest identifier means a relying party never sees the DID it asked for -
|
|
1543
|
+
# the one on the data carrier, in the credential, in the database. So the
|
|
1544
|
+
# requested identifier is what goes into the document; which version it is
|
|
1545
|
+
# remains readable from didDocumentMetadata canonicalId.
|
|
1546
|
+
#
|
|
1547
|
+
# Callers that build a did_info by hand - the registrar endpoints do, for a
|
|
1548
|
+
# DID that was just created or updated - carry no did_requested and keep
|
|
1549
|
+
# getting the identifier they passed in.
|
|
1550
|
+
def self.document_id(did_info)
|
|
1551
|
+
did = did_info["did_requested"].to_s
|
|
1552
|
+
did = did_info["did"].to_s if did == ""
|
|
1553
|
+
did = percent_encode(did)
|
|
1554
|
+
if !did.start_with?("did:oyd:")
|
|
1555
|
+
did = "did:oyd:" + did
|
|
1556
|
+
end
|
|
1557
|
+
did
|
|
1558
|
+
end
|
|
1559
|
+
|
|
1429
1560
|
def self.w3c(did_info, options)
|
|
1430
1561
|
# check if doc is already W3C DID
|
|
1431
1562
|
is_already_w3c_did = (did_info.transform_keys(&:to_s)["doc"]["doc"].has_key?("@context") &&
|
|
@@ -1434,10 +1565,7 @@ class Oydid
|
|
|
1434
1565
|
if is_already_w3c_did
|
|
1435
1566
|
return did_info.transform_keys(&:to_s)["doc"]["doc"]
|
|
1436
1567
|
end
|
|
1437
|
-
did =
|
|
1438
|
-
if !did.start_with?("did:oyd:")
|
|
1439
|
-
did = "did:oyd:" + did
|
|
1440
|
-
end
|
|
1568
|
+
did = document_id(did_info)
|
|
1441
1569
|
|
|
1442
1570
|
didDoc = did_info.dup.transform_keys(&:to_s)["doc"]
|
|
1443
1571
|
pubDocKey = didDoc["key"].split(":")[0] rescue ""
|
|
@@ -1568,15 +1696,17 @@ class Oydid
|
|
|
1568
1696
|
end
|
|
1569
1697
|
end
|
|
1570
1698
|
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1699
|
+
# alsoKnownAs is a statement about the DID *subject*, and DID Core calls
|
|
1700
|
+
# it best practice not to read it as equivalence unless the relationship
|
|
1701
|
+
# is reciprocated - which it cannot be here, because all versions resolve
|
|
1702
|
+
# to the same document. It stays for backwards compatibility; the
|
|
1703
|
+
# authoritative statement is didDocumentMetadata canonicalId/equivalentId,
|
|
1704
|
+
# built from the same list - except that this one keeps the location
|
|
1705
|
+
# suffix. alsoKnownAs therefore carries two kinds of statement: other
|
|
1706
|
+
# versions of the DID, and the location-bound variant of one. It must not
|
|
1707
|
+
# be read as a list of locations; the method specification says so
|
|
1708
|
+
# explicitly.
|
|
1709
|
+
equivalentIds = version_ids(did_info, true).last
|
|
1580
1710
|
if equivalentIds.length > 0
|
|
1581
1711
|
wd["alsoKnownAs"] = equivalentIds
|
|
1582
1712
|
end
|
|
@@ -1 +1 @@
|
|
|
1
|
-
[{"did":"zQmaBZTghndXTgxNwfbdpVLWdFf6faYE4oeuN2zzXdQt1kh","doc":{"doc":{"simple":"example"},"key":"z6MusYB5iT5krCHYsZ76EzBaTdRwGKsaBhMcSbrXaPJgkuRQ:z6Mv7EYihbAat6Wq7GsjNsjcxt58dZT8fmsRjQGTkYamYrjB","log":"zQmVwMvovLy5KNYHHVHQ1wv8J7y9L6UPE8eyU4tzypFWtYe"},"log":[{"ts":1641224736,"op":2,"doc":"zQmaBZTghndXTgxNwfbdpVLWdFf6faYE4oeuN2zzXdQt1kh","sig":"z3Kb5qeReCqr3ftxpf2i5UypUwrzrVkyspMtaDcb6e9YdHVSptcAFgvwbgk3qWqspTcGiKDYKXZZh8g6XyM2WPmNp","previous":[]},{"ts":1641224736,"op":0,"doc":"zQmT8SG7a238bF7wdV7LdrEAQpimqhKGor7CQsjtCYdZdTS","sig":"z63hu8LseptBrvB2kEDwhPP35sBj7JDDJsEDW85cjRkrjjac9ZV3HxPW9NVKewHcQYwrVLVsnDCcm1RjbEARE5rJU","previous":[]}],"doc_log_id":0,"termination_log_id":1,"error":0,"message":"","verification":"identifier: zQmaBZTghndXTgxNwfbdpVLWdFf6faYE4oeuN2zzXdQt1kh\n✅ is hash of DID Document:\n{\n \"doc\": {\n \"simple\": \"example\"\n },\n \"key\": \"z6MusYB5iT5krCHYsZ76EzBaTdRwGKsaBhMcSbrXaPJgkuRQ:z6Mv7EYihbAat6Wq7GsjNsjcxt58dZT8fmsRjQGTkYamYrjB\",\n \"log\": \"zQmVwMvovLy5KNYHHVHQ1wv8J7y9L6UPE8eyU4tzypFWtYe\"\n}\n(Details: https://ownyourdata.github.io/oydid/#calculate_hash)\n\n'log' reference in DID Document: zQmVwMvovLy5KNYHHVHQ1wv8J7y9L6UPE8eyU4tzypFWtYe\n✅ is hash of TERMINATE log record:\n{\n \"ts\": 1641224736,\n \"op\": 0,\n \"doc\": \"zQmT8SG7a238bF7wdV7LdrEAQpimqhKGor7CQsjtCYdZdTS\",\n \"sig\": \"z63hu8LseptBrvB2kEDwhPP35sBj7JDDJsEDW85cjRkrjjac9ZV3HxPW9NVKewHcQYwrVLVsnDCcm1RjbEARE5rJU\",\n \"previous\": []\n}\n(Details: https://ownyourdata.github.io/oydid/#calculate_hash)\n\nRevocation reference in log record: zQmT8SG7a238bF7wdV7LdrEAQpimqhKGor7CQsjtCYdZdTS\n✅ cannot find revocation record searching at\n- https://oydid.ownyourdata.eu\n(Details: https://ownyourdata.github.io/oydid/#retrieve_log)\n\n","full_log":[{"ts":1641224736,"op":2,"doc":"zQmaBZTghndXTgxNwfbdpVLWdFf6faYE4oeuN2zzXdQt1kh","sig":"z3Kb5qeReCqr3ftxpf2i5UypUwrzrVkyspMtaDcb6e9YdHVSptcAFgvwbgk3qWqspTcGiKDYKXZZh8g6XyM2WPmNp","previous":[]},{"ts":1641224736,"op":0,"doc":"zQmT8SG7a238bF7wdV7LdrEAQpimqhKGor7CQsjtCYdZdTS","sig":"z63hu8LseptBrvB2kEDwhPP35sBj7JDDJsEDW85cjRkrjjac9ZV3HxPW9NVKewHcQYwrVLVsnDCcm1RjbEARE5rJU","previous":[]}]},""]
|
|
1
|
+
[{"did":"zQmaBZTghndXTgxNwfbdpVLWdFf6faYE4oeuN2zzXdQt1kh","did_requested":"did:oyd:zQmaBZTghndXTgxNwfbdpVLWdFf6faYE4oeuN2zzXdQt1kh","doc":{"doc":{"simple":"example"},"key":"z6MusYB5iT5krCHYsZ76EzBaTdRwGKsaBhMcSbrXaPJgkuRQ:z6Mv7EYihbAat6Wq7GsjNsjcxt58dZT8fmsRjQGTkYamYrjB","log":"zQmVwMvovLy5KNYHHVHQ1wv8J7y9L6UPE8eyU4tzypFWtYe"},"log":[{"ts":1641224736,"op":2,"doc":"zQmaBZTghndXTgxNwfbdpVLWdFf6faYE4oeuN2zzXdQt1kh","sig":"z3Kb5qeReCqr3ftxpf2i5UypUwrzrVkyspMtaDcb6e9YdHVSptcAFgvwbgk3qWqspTcGiKDYKXZZh8g6XyM2WPmNp","previous":[]},{"ts":1641224736,"op":0,"doc":"zQmT8SG7a238bF7wdV7LdrEAQpimqhKGor7CQsjtCYdZdTS","sig":"z63hu8LseptBrvB2kEDwhPP35sBj7JDDJsEDW85cjRkrjjac9ZV3HxPW9NVKewHcQYwrVLVsnDCcm1RjbEARE5rJU","previous":[]}],"doc_log_id":0,"termination_log_id":1,"error":0,"message":"","verification":"identifier: zQmaBZTghndXTgxNwfbdpVLWdFf6faYE4oeuN2zzXdQt1kh\n✅ is hash of DID Document:\n{\n \"doc\": {\n \"simple\": \"example\"\n },\n \"key\": \"z6MusYB5iT5krCHYsZ76EzBaTdRwGKsaBhMcSbrXaPJgkuRQ:z6Mv7EYihbAat6Wq7GsjNsjcxt58dZT8fmsRjQGTkYamYrjB\",\n \"log\": \"zQmVwMvovLy5KNYHHVHQ1wv8J7y9L6UPE8eyU4tzypFWtYe\"\n}\n(Details: https://ownyourdata.github.io/oydid/#calculate_hash)\n\n'log' reference in DID Document: zQmVwMvovLy5KNYHHVHQ1wv8J7y9L6UPE8eyU4tzypFWtYe\n✅ is hash of TERMINATE log record:\n{\n \"ts\": 1641224736,\n \"op\": 0,\n \"doc\": \"zQmT8SG7a238bF7wdV7LdrEAQpimqhKGor7CQsjtCYdZdTS\",\n \"sig\": \"z63hu8LseptBrvB2kEDwhPP35sBj7JDDJsEDW85cjRkrjjac9ZV3HxPW9NVKewHcQYwrVLVsnDCcm1RjbEARE5rJU\",\n \"previous\": []\n}\n(Details: https://ownyourdata.github.io/oydid/#calculate_hash)\n\nRevocation reference in log record: zQmT8SG7a238bF7wdV7LdrEAQpimqhKGor7CQsjtCYdZdTS\n✅ cannot find revocation record searching at\n- https://oydid.ownyourdata.eu\n(Details: https://ownyourdata.github.io/oydid/#retrieve_log)\n\n","full_log":[{"ts":1641224736,"op":2,"doc":"zQmaBZTghndXTgxNwfbdpVLWdFf6faYE4oeuN2zzXdQt1kh","sig":"z3Kb5qeReCqr3ftxpf2i5UypUwrzrVkyspMtaDcb6e9YdHVSptcAFgvwbgk3qWqspTcGiKDYKXZZh8g6XyM2WPmNp","previous":[]},{"ts":1641224736,"op":0,"doc":"zQmT8SG7a238bF7wdV7LdrEAQpimqhKGor7CQsjtCYdZdTS","sig":"z63hu8LseptBrvB2kEDwhPP35sBj7JDDJsEDW85cjRkrjjac9ZV3HxPW9NVKewHcQYwrVLVsnDCcm1RjbEARE5rJU","previous":[]}]},""]
|
data/spec/oydid_spec.rb
CHANGED
|
@@ -584,4 +584,278 @@ describe "OYDID handling" do
|
|
|
584
584
|
end
|
|
585
585
|
end
|
|
586
586
|
|
|
587
|
+
# Identifiers of the published versions of a DID. Every update mints a new
|
|
588
|
+
# one, so a resolver has to be able to say which is current and which are
|
|
589
|
+
# earlier - didDocumentMetadata canonicalId and equivalentId.
|
|
590
|
+
describe "version_ids" do
|
|
591
|
+
let(:first_did) { "zQmSE1hzumtZ7AoK1qhHf4t5kiKsujMsJSHqoXtWrdd7K7W" }
|
|
592
|
+
let(:second_did) { "zQmfEb3KgYZjZUPLTHPmFPdcV6peF5itB5NmJ9N6gaxxE8K" }
|
|
593
|
+
|
|
594
|
+
it "reports a freshly created DID as canonical without equivalents" do
|
|
595
|
+
canonical, equivalent = Oydid.version_ids(
|
|
596
|
+
"did" => "did:oyd:" + first_did,
|
|
597
|
+
"log" => [{ "op" => 2, "doc" => first_did },
|
|
598
|
+
{ "op" => 0, "doc" => "terminate" }])
|
|
599
|
+
expect(canonical).to eq "did:oyd:" + first_did
|
|
600
|
+
expect(equivalent).to eq []
|
|
601
|
+
end
|
|
602
|
+
|
|
603
|
+
it "names the current version canonical and the previous one equivalent" do
|
|
604
|
+
canonical, equivalent = Oydid.version_ids(
|
|
605
|
+
"did" => "did:oyd:" + second_did,
|
|
606
|
+
"log" => [{ "op" => 1, "doc" => "revoke" },
|
|
607
|
+
{ "op" => 2, "doc" => first_did },
|
|
608
|
+
{ "op" => 3, "doc" => second_did },
|
|
609
|
+
{ "op" => 0, "doc" => "terminate" }])
|
|
610
|
+
expect(canonical).to eq "did:oyd:" + second_did
|
|
611
|
+
expect(equivalent).to eq ["did:oyd:" + first_did]
|
|
612
|
+
end
|
|
613
|
+
|
|
614
|
+
it "keeps every earlier version, oldest first" do
|
|
615
|
+
canonical, equivalent = Oydid.version_ids(
|
|
616
|
+
"did" => "did:oyd:zC",
|
|
617
|
+
"log" => [{ "op" => 2, "doc" => "zA" },
|
|
618
|
+
{ "op" => 3, "doc" => "zB" },
|
|
619
|
+
{ "op" => 3, "doc" => "zC" }])
|
|
620
|
+
expect(canonical).to eq "did:oyd:zC"
|
|
621
|
+
expect(equivalent).to eq ["did:oyd:zA", "did:oyd:zB"]
|
|
622
|
+
end
|
|
623
|
+
|
|
624
|
+
it "adds the method prefix when the resolved DID carries none" do
|
|
625
|
+
canonical, = Oydid.version_ids("did" => second_did, "log" => [])
|
|
626
|
+
expect(canonical).to eq "did:oyd:" + second_did
|
|
627
|
+
end
|
|
628
|
+
|
|
629
|
+
# The identifier a relying party was handed is the one it asked for, which
|
|
630
|
+
# after an update is an earlier version than the document being served. Every
|
|
631
|
+
# other version is then equivalent to it - not just the current one. This is
|
|
632
|
+
# the case the HTTP request specs could not reach before update_did existed.
|
|
633
|
+
it "lists every other version when an earlier one was requested" do
|
|
634
|
+
canonical, equivalent = Oydid.version_ids(
|
|
635
|
+
"did" => "did:oyd:zC",
|
|
636
|
+
"did_requested" => "did:oyd:zA",
|
|
637
|
+
"log" => [{ "op" => 2, "doc" => "zA" },
|
|
638
|
+
{ "op" => 3, "doc" => "zB" },
|
|
639
|
+
{ "op" => 3, "doc" => "zC" }])
|
|
640
|
+
expect(canonical).to eq "did:oyd:zC"
|
|
641
|
+
expect(equivalent).to eq ["did:oyd:zB", "did:oyd:zC"]
|
|
642
|
+
end
|
|
643
|
+
|
|
644
|
+
it "lists the versions on both sides when a middle one was requested" do
|
|
645
|
+
canonical, equivalent = Oydid.version_ids(
|
|
646
|
+
"did" => "did:oyd:zC",
|
|
647
|
+
"did_requested" => "did:oyd:zB",
|
|
648
|
+
"log" => [{ "op" => 2, "doc" => "zA" },
|
|
649
|
+
{ "op" => 3, "doc" => "zB" },
|
|
650
|
+
{ "op" => 3, "doc" => "zC" }])
|
|
651
|
+
expect(canonical).to eq "did:oyd:zC"
|
|
652
|
+
expect(equivalent).to eq ["did:oyd:zA", "did:oyd:zC"]
|
|
653
|
+
end
|
|
654
|
+
|
|
655
|
+
# canonicalId and equivalentId state identity, and "@<location>" states where
|
|
656
|
+
# a document is hosted. The same document can be mirrored at any number of
|
|
657
|
+
# locations, so the set of location-bound variants is open and equivalentId
|
|
658
|
+
# could not state it correctly - both are therefore location-free.
|
|
659
|
+
it "strips the location suffix from canonicalId and equivalentId" do
|
|
660
|
+
canonical, equivalent = Oydid.version_ids(
|
|
661
|
+
"did" => "did:oyd:" + second_did + "@https://example.org",
|
|
662
|
+
"did_requested" => "did:oyd:" + second_did,
|
|
663
|
+
"log" => [{ "op" => 2, "doc" => first_did + "@https://example.org" },
|
|
664
|
+
{ "op" => 3, "doc" => second_did + "@https://example.org" }])
|
|
665
|
+
expect(canonical).to eq "did:oyd:" + second_did
|
|
666
|
+
expect(equivalent).to eq ["did:oyd:" + first_did]
|
|
667
|
+
end
|
|
668
|
+
|
|
669
|
+
# A location-bound DID that was asked for stays the id of the document - DID
|
|
670
|
+
# Core: "the value of the id property in the retrieved DID document must
|
|
671
|
+
# always match the DID being resolved". Its location-free form is then a
|
|
672
|
+
# genuinely different string for the same subject, so it belongs in
|
|
673
|
+
# equivalentId alongside the earlier version.
|
|
674
|
+
it "lists the location-free form of the current version when a location-bound DID was requested" do
|
|
675
|
+
canonical, equivalent = Oydid.version_ids(
|
|
676
|
+
"did" => "did:oyd:" + second_did + "@https://example.org",
|
|
677
|
+
"did_requested" => "did:oyd:" + second_did + "@https://example.org",
|
|
678
|
+
"log" => [{ "op" => 2, "doc" => first_did + "@https://example.org" },
|
|
679
|
+
{ "op" => 3, "doc" => second_did + "@https://example.org" }])
|
|
680
|
+
expect(canonical).to eq "did:oyd:" + second_did
|
|
681
|
+
expect(equivalent).to eq ["did:oyd:" + first_did, "did:oyd:" + second_did]
|
|
682
|
+
end
|
|
683
|
+
|
|
684
|
+
# this used to list the DID as its own equivalent, in location-bound form,
|
|
685
|
+
# and hand out that same string as canonicalId
|
|
686
|
+
it "reports no equivalents for a never updated DID served from a location" do
|
|
687
|
+
canonical, equivalent = Oydid.version_ids(
|
|
688
|
+
"did" => "did:oyd:" + first_did + "@https://example.org",
|
|
689
|
+
"did_requested" => "did:oyd:" + first_did,
|
|
690
|
+
"log" => [{ "op" => 2, "doc" => first_did + "@https://example.org" },
|
|
691
|
+
{ "op" => 0, "doc" => "terminate" }])
|
|
692
|
+
expect(canonical).to eq "did:oyd:" + first_did
|
|
693
|
+
expect(equivalent).to eq []
|
|
694
|
+
end
|
|
695
|
+
|
|
696
|
+
it "keeps the location suffix for the alsoKnownAs list" do
|
|
697
|
+
canonical, equivalent = Oydid.version_ids(
|
|
698
|
+
{ "did" => "did:oyd:" + first_did + "@https://example.org",
|
|
699
|
+
"did_requested" => "did:oyd:" + first_did,
|
|
700
|
+
"log" => [{ "op" => 2, "doc" => first_did + "@https://example.org" }] },
|
|
701
|
+
true)
|
|
702
|
+
expect(canonical).to eq "did:oyd:" + first_did + "%40example.org"
|
|
703
|
+
expect(equivalent).to eq ["did:oyd:" + first_did + "%40example.org"]
|
|
704
|
+
end
|
|
705
|
+
|
|
706
|
+
it "tolerates a missing log" do
|
|
707
|
+
canonical, equivalent = Oydid.version_ids("did" => "did:oyd:" + second_did)
|
|
708
|
+
expect(canonical).to eq "did:oyd:" + second_did
|
|
709
|
+
expect(equivalent).to eq []
|
|
710
|
+
end
|
|
711
|
+
|
|
712
|
+
# w3c builds alsoKnownAs from the same list. The two used to be computed
|
|
713
|
+
# separately in three places and drifted apart; this guards the merge.
|
|
714
|
+
#
|
|
715
|
+
# The canonical DID has to be one this suite stubs: w3c looks up delegation
|
|
716
|
+
# keys for it, and WebMock::NetConnectNotAllowedError descends from Exception
|
|
717
|
+
# rather than StandardError, so the inline `rescue []` around
|
|
718
|
+
# getDelegatedPubKeysFromDID does not catch it. Only the canonical DID is
|
|
719
|
+
# fetched - log entries are read, not resolved.
|
|
720
|
+
it "agrees with the alsoKnownAs the DID document carries" do
|
|
721
|
+
stubbed_did = "zQmaBZTghndXTgxNwfbdpVLWdFf6faYE4oeuN2zzXdQt1kh"
|
|
722
|
+
did_info = {
|
|
723
|
+
"did" => "did:oyd:" + stubbed_did,
|
|
724
|
+
"doc" => { "doc" => { "hello" => "world" },
|
|
725
|
+
"key" => "z6MktULudTtAsAhRegYPiZ6631RV3viv12qd4GQF8z1xB22S:" \
|
|
726
|
+
"z6MkqGC3nWZhYieEVTVDKW5v588CiGfsDSmRVG9ZwwWTvLSK" },
|
|
727
|
+
"log" => [{ "op" => 2, "doc" => first_did },
|
|
728
|
+
{ "op" => 3, "doc" => stubbed_did }]
|
|
729
|
+
}
|
|
730
|
+
wd = Oydid.w3c(Marshal.load(Marshal.dump(did_info)), {})
|
|
731
|
+
expect(wd["alsoKnownAs"]).to eq ["did:oyd:" + first_did]
|
|
732
|
+
expect(wd["alsoKnownAs"]).to eq Oydid.version_ids(did_info, true).last
|
|
733
|
+
end
|
|
734
|
+
|
|
735
|
+
# The two lists deliberately part ways on the location: identity statements
|
|
736
|
+
# drop it, alsoKnownAs keeps it - otherwise the location would disappear from
|
|
737
|
+
# the DID document altogether. Guards the live output for a never updated DID.
|
|
738
|
+
it "keeps the location-bound variant in alsoKnownAs while canonicalId drops it" do
|
|
739
|
+
stubbed_did = "zQmaBZTghndXTgxNwfbdpVLWdFf6faYE4oeuN2zzXdQt1kh"
|
|
740
|
+
did_info = {
|
|
741
|
+
"did" => "did:oyd:" + stubbed_did + "@https://example.org",
|
|
742
|
+
"did_requested" => "did:oyd:" + stubbed_did,
|
|
743
|
+
"doc" => { "doc" => { "hello" => "world" },
|
|
744
|
+
"key" => "z6MktULudTtAsAhRegYPiZ6631RV3viv12qd4GQF8z1xB22S:" \
|
|
745
|
+
"z6MkqGC3nWZhYieEVTVDKW5v588CiGfsDSmRVG9ZwwWTvLSK" },
|
|
746
|
+
"log" => [{ "op" => 2, "doc" => stubbed_did + "@https://example.org" }]
|
|
747
|
+
}
|
|
748
|
+
wd = Oydid.w3c(Marshal.load(Marshal.dump(did_info)), {})
|
|
749
|
+
expect(wd["id"]).to eq "did:oyd:" + stubbed_did
|
|
750
|
+
expect(wd["alsoKnownAs"]).to eq ["did:oyd:" + stubbed_did + "%40example.org"]
|
|
751
|
+
canonical, equivalent = Oydid.version_ids(did_info)
|
|
752
|
+
expect(canonical).to eq "did:oyd:" + stubbed_did
|
|
753
|
+
expect(equivalent).to eq []
|
|
754
|
+
end
|
|
755
|
+
end
|
|
756
|
+
|
|
757
|
+
# created / updated / versionId of the resolved version (DID Core 7.1.3).
|
|
758
|
+
# Without `updated` a consumer cannot tell how old the document in its hands is.
|
|
759
|
+
describe "version_metadata" do
|
|
760
|
+
let(:first_did) { "zQmSE1hzumtZ7AoK1qhHf4t5kiKsujMsJSHqoXtWrdd7K7W" }
|
|
761
|
+
let(:second_did) { "zQmfEb3KgYZjZUPLTHPmFPdcV6peF5itB5NmJ9N6gaxxE8K" }
|
|
762
|
+
|
|
763
|
+
# "The updated property is omitted if an Update operation has never been
|
|
764
|
+
# performed on the DID document." - 7.1.3
|
|
765
|
+
it "reports created and versionId, and omits updated, for a new DID" do
|
|
766
|
+
meta = Oydid.version_metadata(
|
|
767
|
+
"did" => "did:oyd:" + first_did + "@https://example.org",
|
|
768
|
+
"log" => [{ "op" => 2, "ts" => 1641224940, "doc" => first_did + "@https://example.org" },
|
|
769
|
+
{ "op" => 0, "ts" => 1641224940, "doc" => "terminate" }])
|
|
770
|
+
expect(meta["created"]).to eq "2022-01-03T15:49:00Z"
|
|
771
|
+
expect(meta["versionId"]).to eq first_did
|
|
772
|
+
expect(meta).not_to have_key("updated")
|
|
773
|
+
end
|
|
774
|
+
|
|
775
|
+
it "reports the timestamp of the update that produced the resolved version" do
|
|
776
|
+
meta = Oydid.version_metadata(
|
|
777
|
+
"did" => "did:oyd:" + second_did,
|
|
778
|
+
"log" => [{ "op" => 2, "ts" => 1641224940, "doc" => first_did },
|
|
779
|
+
{ "op" => 3, "ts" => 1641225032, "doc" => second_did },
|
|
780
|
+
{ "op" => 0, "ts" => 1641225032, "doc" => "terminate" }])
|
|
781
|
+
expect(meta["created"]).to eq "2022-01-03T15:49:00Z"
|
|
782
|
+
expect(meta["updated"]).to eq "2022-01-03T15:50:32Z"
|
|
783
|
+
expect(meta["versionId"]).to eq second_did
|
|
784
|
+
end
|
|
785
|
+
|
|
786
|
+
# the entry is picked by the version it produced, not by its timestamp:
|
|
787
|
+
# timestamps come from the client and two entries can share a second
|
|
788
|
+
it "ignores updates that did not produce the resolved version" do
|
|
789
|
+
meta = Oydid.version_metadata(
|
|
790
|
+
"did" => "did:oyd:zB",
|
|
791
|
+
"log" => [{ "op" => 2, "ts" => 1, "doc" => "zA" },
|
|
792
|
+
{ "op" => 3, "ts" => 2, "doc" => "zB" },
|
|
793
|
+
{ "op" => 3, "ts" => 3, "doc" => "zC" }])
|
|
794
|
+
expect(meta["versionId"]).to eq "zB"
|
|
795
|
+
expect(meta["updated"]).to eq "1970-01-01T00:00:02Z"
|
|
796
|
+
end
|
|
797
|
+
|
|
798
|
+
it "leaves out what the log cannot answer" do
|
|
799
|
+
expect(Oydid.version_metadata("did" => "did:oyd:" + first_did))
|
|
800
|
+
.to eq({ "versionId" => first_did })
|
|
801
|
+
expect(Oydid.version_metadata("did" => "did:oyd:" + first_did,
|
|
802
|
+
"log" => [{ "op" => 2, "doc" => first_did }]))
|
|
803
|
+
.to eq({ "versionId" => first_did })
|
|
804
|
+
end
|
|
805
|
+
end
|
|
806
|
+
|
|
807
|
+
# Which identifier a resolved DID document announces as its own. An update
|
|
808
|
+
# mints a new did:oyd, but the identifier a relying party holds is the one it
|
|
809
|
+
# asked for - that is the one the document has to carry.
|
|
810
|
+
describe "document_id" do
|
|
811
|
+
let(:first_did) { "zQmSE1hzumtZ7AoK1qhHf4t5kiKsujMsJSHqoXtWrdd7K7W" }
|
|
812
|
+
let(:second_did) { "zQmfEb3KgYZjZUPLTHPmFPdcV6peF5itB5NmJ9N6gaxxE8K" }
|
|
813
|
+
# a DID this suite stubs - w3c looks up delegation keys for whatever it
|
|
814
|
+
# emits as id, see the note on the alsoKnownAs example above
|
|
815
|
+
let(:stubbed_did) { "zQmaBZTghndXTgxNwfbdpVLWdFf6faYE4oeuN2zzXdQt1kh" }
|
|
816
|
+
|
|
817
|
+
it "answers with the requested DID when the resolver kept it" do
|
|
818
|
+
expect(Oydid.document_id("did" => "did:oyd:" + second_did,
|
|
819
|
+
"did_requested" => "did:oyd:" + first_did))
|
|
820
|
+
.to eq "did:oyd:" + first_did
|
|
821
|
+
end
|
|
822
|
+
|
|
823
|
+
it "falls back to the resolved DID for a did_info built by hand" do
|
|
824
|
+
expect(Oydid.document_id("did" => "did:oyd:" + second_did))
|
|
825
|
+
.to eq "did:oyd:" + second_did
|
|
826
|
+
end
|
|
827
|
+
|
|
828
|
+
it "adds the method prefix and percent-encodes a location suffix" do
|
|
829
|
+
expect(Oydid.document_id("did" => "", "did_requested" => first_did + "@https://example.org"))
|
|
830
|
+
.to eq "did:oyd:" + first_did + "%40example.org"
|
|
831
|
+
end
|
|
832
|
+
|
|
833
|
+
# the point of the exercise: a DID printed on a data carrier keeps naming
|
|
834
|
+
# itself after an update, while the current version stays readable
|
|
835
|
+
it "keeps the requested DID as id and moves the current one to alsoKnownAs" do
|
|
836
|
+
did_info = {
|
|
837
|
+
"did" => "did:oyd:" + second_did,
|
|
838
|
+
"did_requested" => "did:oyd:" + stubbed_did,
|
|
839
|
+
"doc" => { "doc" => { "hello" => "world" },
|
|
840
|
+
"key" => "z6MktULudTtAsAhRegYPiZ6631RV3viv12qd4GQF8z1xB22S:" \
|
|
841
|
+
"z6MkqGC3nWZhYieEVTVDKW5v588CiGfsDSmRVG9ZwwWTvLSK" },
|
|
842
|
+
"log" => [{ "op" => 2, "doc" => stubbed_did },
|
|
843
|
+
{ "op" => 3, "doc" => second_did }]
|
|
844
|
+
}
|
|
845
|
+
wd = Oydid.w3c(Marshal.load(Marshal.dump(did_info)), {})
|
|
846
|
+
expect(wd["id"]).to eq "did:oyd:" + stubbed_did
|
|
847
|
+
expect(wd["alsoKnownAs"]).to eq ["did:oyd:" + second_did]
|
|
848
|
+
# the entries of verificationMethod are built as hash literals with
|
|
849
|
+
# "id": - Ruby makes that a symbol key, the surrounding keys are strings
|
|
850
|
+
expect(wd["verificationMethod"].first[:id]).to start_with("did:oyd:" + stubbed_did + "#")
|
|
851
|
+
expect(wd["verificationMethod"].first[:controller]).to eq "did:oyd:" + stubbed_did
|
|
852
|
+
|
|
853
|
+
# canonicalId still names the version the log resolves to, and the
|
|
854
|
+
# equivalent list is everything the document does not call itself
|
|
855
|
+
canonical, equivalent = Oydid.version_ids(did_info)
|
|
856
|
+
expect(canonical).to eq "did:oyd:" + second_did
|
|
857
|
+
expect(equivalent).to eq ["did:oyd:" + second_did]
|
|
858
|
+
end
|
|
859
|
+
end
|
|
860
|
+
|
|
587
861
|
end
|
metadata
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: oydid
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.9.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Christoph Fabianek
|
|
8
8
|
bindir: bin
|
|
9
9
|
cert_chain: []
|
|
10
|
-
date: 2026-08-
|
|
10
|
+
date: 2026-08-26 00:00:00.000000000 Z
|
|
11
11
|
dependencies:
|
|
12
12
|
- !ruby/object:Gem::Dependency
|
|
13
13
|
name: simple_dag
|