oydid 0.9.0 → 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 +83 -10
- data/spec/oydid_spec.rb +142 -3
- 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.9.
|
|
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
|
@@ -1446,19 +1446,42 @@ class Oydid
|
|
|
1446
1446
|
# uniresolver plugin) and they had drifted into computing this list with
|
|
1447
1447
|
# different rules.
|
|
1448
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
|
+
#
|
|
1449
1458
|
# Returns [canonicalId, equivalentIds]; equivalentIds never contains the
|
|
1450
|
-
# identifier the DID document itself carries as `id` (see document_id)
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
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"])
|
|
1456
1479
|
own = document_id(did_info)
|
|
1457
1480
|
equivalentIds = []
|
|
1458
1481
|
did_info["log"].each do |log|
|
|
1459
1482
|
if log["op"] == 2 || log["op"] == 3
|
|
1460
|
-
eid =
|
|
1461
|
-
if eid != own
|
|
1483
|
+
eid = normalize.call(log["doc"])
|
|
1484
|
+
if eid != own && !equivalentIds.include?(eid)
|
|
1462
1485
|
equivalentIds << eid
|
|
1463
1486
|
end
|
|
1464
1487
|
end
|
|
@@ -1466,6 +1489,52 @@ class Oydid
|
|
|
1466
1489
|
[canonical, equivalentIds]
|
|
1467
1490
|
end
|
|
1468
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
|
+
|
|
1469
1538
|
# The identifier a resolved DID document carries as `id`.
|
|
1470
1539
|
#
|
|
1471
1540
|
# A did:oyd is the hash over its own document, so an update mints a new one
|
|
@@ -1632,8 +1701,12 @@ class Oydid
|
|
|
1632
1701
|
# is reciprocated - which it cannot be here, because all versions resolve
|
|
1633
1702
|
# to the same document. It stays for backwards compatibility; the
|
|
1634
1703
|
# authoritative statement is didDocumentMetadata canonicalId/equivalentId,
|
|
1635
|
-
# built from the same list
|
|
1636
|
-
|
|
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
|
|
1637
1710
|
if equivalentIds.length > 0
|
|
1638
1711
|
wd["alsoKnownAs"] = equivalentIds
|
|
1639
1712
|
end
|
data/spec/oydid_spec.rb
CHANGED
|
@@ -626,12 +626,80 @@ describe "OYDID handling" do
|
|
|
626
626
|
expect(canonical).to eq "did:oyd:" + second_did
|
|
627
627
|
end
|
|
628
628
|
|
|
629
|
-
|
|
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
|
|
630
675
|
canonical, equivalent = Oydid.version_ids(
|
|
631
676
|
"did" => "did:oyd:" + second_did + "@https://example.org",
|
|
677
|
+
"did_requested" => "did:oyd:" + second_did + "@https://example.org",
|
|
632
678
|
"log" => [{ "op" => 2, "doc" => first_did + "@https://example.org" },
|
|
633
679
|
{ "op" => 3, "doc" => second_did + "@https://example.org" }])
|
|
634
|
-
expect(canonical).to eq "did:oyd:" + second_did
|
|
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"
|
|
635
703
|
expect(equivalent).to eq ["did:oyd:" + first_did + "%40example.org"]
|
|
636
704
|
end
|
|
637
705
|
|
|
@@ -661,7 +729,78 @@ describe "OYDID handling" do
|
|
|
661
729
|
}
|
|
662
730
|
wd = Oydid.w3c(Marshal.load(Marshal.dump(did_info)), {})
|
|
663
731
|
expect(wd["alsoKnownAs"]).to eq ["did:oyd:" + first_did]
|
|
664
|
-
expect(wd["alsoKnownAs"]).to eq Oydid.version_ids(did_info).last
|
|
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 })
|
|
665
804
|
end
|
|
666
805
|
end
|
|
667
806
|
|
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.9.
|
|
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
|