@reventlessdev/reventless-seed 1.0.0-alpha.19 → 1.0.0-alpha.20

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 (54) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/lib/bs/.compiler.log +2 -2
  3. package/lib/bs/compiler-info.json +1 -1
  4. package/lib/bs/src/Seed-ReventlessSeed.cmi +0 -0
  5. package/lib/bs/src/Seed-ReventlessSeed.cmt +0 -0
  6. package/lib/bs/src/Seed_Client-ReventlessSeed.cmi +0 -0
  7. package/lib/bs/src/Seed_Client-ReventlessSeed.cmj +0 -0
  8. package/lib/bs/src/Seed_Client-ReventlessSeed.cmt +0 -0
  9. package/lib/bs/src/Seed_Client.ast +0 -0
  10. package/lib/bs/src/Seed_Client.res +108 -1
  11. package/lib/bs/src/Seed_Client.res.mjs +103 -1
  12. package/lib/bs/src/Seed_Connect-ReventlessSeed.cmi +0 -0
  13. package/lib/bs/src/Seed_Connect-ReventlessSeed.cmt +0 -0
  14. package/lib/bs/src/Seed_Connect.ast +0 -0
  15. package/lib/bs/src/Seed_Connect.res +7 -0
  16. package/lib/bs/src/Seed_Connect.res.mjs +4 -0
  17. package/lib/bs/src/Seed_Runner-ReventlessSeed.cmi +0 -0
  18. package/lib/bs/src/Seed_Runner-ReventlessSeed.cmt +0 -0
  19. package/lib/bs/src/Seed_Upload-ReventlessSeed.cmi +0 -0
  20. package/lib/bs/src/Seed_Upload-ReventlessSeed.cmt +0 -0
  21. package/lib/bs/tests/Seed_ClientTest-ReventlessSeed.cmi +0 -0
  22. package/lib/bs/tests/Seed_ClientTest-ReventlessSeed.cmj +0 -0
  23. package/lib/bs/tests/Seed_ClientTest-ReventlessSeed.cmt +0 -0
  24. package/lib/bs/tests/Seed_ClientTest.ast +0 -0
  25. package/lib/bs/tests/Seed_ClientTest.res +98 -0
  26. package/lib/bs/tests/Seed_ClientTest.res.mjs +103 -0
  27. package/lib/ocaml/.compiler.log +2 -2
  28. package/lib/ocaml/Seed-ReventlessSeed.cmi +0 -0
  29. package/lib/ocaml/Seed-ReventlessSeed.cmt +0 -0
  30. package/lib/ocaml/Seed_Client-ReventlessSeed.cmi +0 -0
  31. package/lib/ocaml/Seed_Client-ReventlessSeed.cmj +0 -0
  32. package/lib/ocaml/Seed_Client-ReventlessSeed.cmt +0 -0
  33. package/lib/ocaml/Seed_Client.ast +0 -0
  34. package/lib/ocaml/Seed_Client.res +108 -1
  35. package/lib/ocaml/Seed_ClientTest-ReventlessSeed.cmi +0 -0
  36. package/lib/ocaml/Seed_ClientTest-ReventlessSeed.cmj +0 -0
  37. package/lib/ocaml/Seed_ClientTest-ReventlessSeed.cmt +0 -0
  38. package/lib/ocaml/Seed_ClientTest.ast +0 -0
  39. package/lib/ocaml/Seed_ClientTest.res +98 -0
  40. package/lib/ocaml/Seed_Connect-ReventlessSeed.cmi +0 -0
  41. package/lib/ocaml/Seed_Connect-ReventlessSeed.cmt +0 -0
  42. package/lib/ocaml/Seed_Connect.ast +0 -0
  43. package/lib/ocaml/Seed_Connect.res +7 -0
  44. package/lib/ocaml/Seed_Runner-ReventlessSeed.cmi +0 -0
  45. package/lib/ocaml/Seed_Runner-ReventlessSeed.cmt +0 -0
  46. package/lib/ocaml/Seed_Upload-ReventlessSeed.cmi +0 -0
  47. package/lib/ocaml/Seed_Upload-ReventlessSeed.cmt +0 -0
  48. package/package.json +2 -2
  49. package/src/Seed_Client.res +108 -1
  50. package/src/Seed_Client.res.mjs +103 -1
  51. package/src/Seed_Connect.res +7 -0
  52. package/src/Seed_Connect.res.mjs +4 -0
  53. package/tests/Seed_ClientTest.res +98 -0
  54. package/tests/Seed_ClientTest.res.mjs +103 -0
@@ -57,3 +57,101 @@ describe("Seed_Client.isTransient:", () => {
57
57
  expect(Seed_Client.isTransient(JSON.Encode.null))->toBe(false)
58
58
  })
59
59
  })
60
+
61
+ // An account's listed groups are not what authorizes it — the token's are. Both
62
+ // platforms narrow a token to the single role the caller last chose, so a seed
63
+ // can log in as an account the user list shows as `[Admin, Shopper]` and present
64
+ // a token carrying `Shopper` alone. The refusal that follows names the field and
65
+ // not the caller, and the account list cannot explain it either. These pin the
66
+ // reading-back that turns that into a sentence.
67
+
68
+ let b64url = payload =>
69
+ payload
70
+ ->NodeBuffer.fromStringUtf8
71
+ ->NodeBuffer.toStringBase64Url
72
+
73
+ let jwt = claims =>
74
+ `header.${b64url(JSON.stringify(JSON.Encode.object(Dict.fromArray(claims))))}.signature`
75
+
76
+ let localToken = claims =>
77
+ `${b64url(JSON.stringify(JSON.Encode.object(Dict.fromArray(claims))))}.signature`
78
+
79
+ let strings = values => JSON.Encode.array(values->Array.map(JSON.Encode.string))
80
+
81
+ let clientWith = token => {
82
+ let c = Seed_Client.make(~config={endpoint: "http://example.invalid/graphql"})
83
+ c->Seed_Client.useToken(token)
84
+ c
85
+ }
86
+
87
+ describe("Seed_Client.effectiveGroups:", () => {
88
+ // A Cognito id token carries its payload in the second segment, a local dev
89
+ // token in the first. Neither platform should have to announce which it is.
90
+ testSync("reads the payload wherever the provider put it", () => {
91
+ expect(
92
+ clientWith(jwt([("cognito:groups", strings(["Admin", "Shopper"]))]))->Seed_Client.effectiveGroups,
93
+ )->toEqual(Some(["Admin", "Shopper"]))
94
+ expect(
95
+ clientWith(localToken([("groups", strings(["Merchandiser"]))]))->Seed_Client.effectiveGroups,
96
+ )->toEqual(Some(["Merchandiser"]))
97
+ })
98
+
99
+ // An opaque bearer is not an error — it just means this can add nothing, and
100
+ // the failure reports what it always did.
101
+ testSync("says nothing about a token it cannot read", () => {
102
+ expect(clientWith("not-a-token")->Seed_Client.effectiveGroups)->toEqual(None)
103
+ expect(
104
+ Seed_Client.make(~config={endpoint: "http://example.invalid/graphql"})
105
+ ->Seed_Client.effectiveGroups,
106
+ )->toEqual(None)
107
+ })
108
+ })
109
+
110
+ describe("Seed_Client.identitySummary:", () => {
111
+ // The case that cost the debugging: eligible in the user list, refused by the
112
+ // token. Naming what it was narrowed FROM is what points at the role switch.
113
+ testSync("names the membership a narrowed token gave up", () =>
114
+ expect(
115
+ clientWith(
116
+ jwt([
117
+ ("cognito:groups", strings(["Shopper"])),
118
+ ("availableRoles", JSON.Encode.string("Admin,Shopper")),
119
+ ]),
120
+ )->Seed_Client.identitySummary,
121
+ )->toEqual(Some("Shopper — narrowed to this role from Admin, Shopper"))
122
+ )
123
+
124
+ // An ordinary login is not narrowed and has nothing to explain.
125
+ testSync("stays quiet about an unnarrowed token", () =>
126
+ expect(
127
+ clientWith(jwt([("cognito:groups", strings(["Admin", "Shopper"]))]))
128
+ ->Seed_Client.identitySummary,
129
+ )->toEqual(Some("Admin, Shopper"))
130
+ )
131
+
132
+ // A token granting nothing reads as a sentence rather than an empty string.
133
+ testSync("says so when the token carries no groups at all", () =>
134
+ expect(
135
+ clientWith(jwt([("cognito:groups", strings([]))]))->Seed_Client.identitySummary,
136
+ )->toEqual(Some("no groups"))
137
+ )
138
+ })
139
+
140
+ describe("Seed_Client.isDenied:", () => {
141
+ // AppSync types it; the local server carries it in the message.
142
+ testSync("recognises a refusal in either platform's vocabulary", () => {
143
+ expect(Seed_Client.isDenied(errors([errorOfType("Unauthorized")])))->toBe(true)
144
+ expect(
145
+ Seed_Client.isDenied(
146
+ errors([Dict.fromArray([("message", JSON.Encode.string("Not Authorized to access X"))])]),
147
+ ),
148
+ )->toBe(true)
149
+ })
150
+
151
+ // Attaching an identity to an unrelated failure would point the reader at the
152
+ // wrong thing.
153
+ testSync("leaves every other failure alone", () => {
154
+ expect(Seed_Client.isDenied(errors([errorOfType("ValidationException")])))->toBe(false)
155
+ expect(Seed_Client.isDenied(errors([])))->toBe(false)
156
+ })
157
+ })
@@ -57,8 +57,111 @@ globalThis.describe("Seed_Client.isTransient:", () => {
57
57
  });
58
58
  });
59
59
 
60
+ function b64url(payload) {
61
+ return Buffer.from(payload, "utf8").toString("base64url");
62
+ }
63
+
64
+ function jwt(claims) {
65
+ let payload = JSON.stringify(Object.fromEntries(claims));
66
+ return `header.` + Buffer.from(payload, "utf8").toString("base64url") + `.signature`;
67
+ }
68
+
69
+ function localToken(claims) {
70
+ let payload = JSON.stringify(Object.fromEntries(claims));
71
+ return Buffer.from(payload, "utf8").toString("base64url") + `.signature`;
72
+ }
73
+
74
+ function strings(values) {
75
+ return values.map(prim => prim);
76
+ }
77
+
78
+ function clientWith(token) {
79
+ let c = Seed_Client$ReventlessSeed.make({
80
+ endpoint: "http://example.invalid/graphql"
81
+ });
82
+ Seed_Client$ReventlessSeed.useToken(c, token);
83
+ return c;
84
+ }
85
+
86
+ globalThis.describe("Seed_Client.effectiveGroups:", () => {
87
+ globalThis.test("reads the payload wherever the provider put it", () => {
88
+ globalThis.expect(Seed_Client$ReventlessSeed.effectiveGroups(clientWith(jwt([[
89
+ "cognito:groups",
90
+ [
91
+ "Admin",
92
+ "Shopper"
93
+ ].map(prim => prim)
94
+ ]])))).toEqual([
95
+ "Admin",
96
+ "Shopper"
97
+ ]);
98
+ globalThis.expect(Seed_Client$ReventlessSeed.effectiveGroups(clientWith(localToken([[
99
+ "groups",
100
+ ["Merchandiser"].map(prim => prim)
101
+ ]])))).toEqual(["Merchandiser"]);
102
+ });
103
+ globalThis.test("says nothing about a token it cannot read", () => {
104
+ globalThis.expect(Seed_Client$ReventlessSeed.effectiveGroups(clientWith("not-a-token"))).toEqual(undefined);
105
+ globalThis.expect(Seed_Client$ReventlessSeed.effectiveGroups(Seed_Client$ReventlessSeed.make({
106
+ endpoint: "http://example.invalid/graphql"
107
+ }))).toEqual(undefined);
108
+ });
109
+ });
110
+
111
+ globalThis.describe("Seed_Client.identitySummary:", () => {
112
+ globalThis.test("names the membership a narrowed token gave up", () => {
113
+ globalThis.expect(Seed_Client$ReventlessSeed.identitySummary(clientWith(jwt([
114
+ [
115
+ "cognito:groups",
116
+ ["Shopper"].map(prim => prim)
117
+ ],
118
+ [
119
+ "availableRoles",
120
+ "Admin,Shopper"
121
+ ]
122
+ ])))).toEqual("Shopper — narrowed to this role from Admin, Shopper");
123
+ });
124
+ globalThis.test("stays quiet about an unnarrowed token", () => {
125
+ globalThis.expect(Seed_Client$ReventlessSeed.identitySummary(clientWith(jwt([[
126
+ "cognito:groups",
127
+ [
128
+ "Admin",
129
+ "Shopper"
130
+ ].map(prim => prim)
131
+ ]])))).toEqual("Admin, Shopper");
132
+ });
133
+ globalThis.test("says so when the token carries no groups at all", () => {
134
+ globalThis.expect(Seed_Client$ReventlessSeed.identitySummary(clientWith(jwt([[
135
+ "cognito:groups",
136
+ [].map(prim => prim)
137
+ ]])))).toEqual("no groups");
138
+ });
139
+ });
140
+
141
+ globalThis.describe("Seed_Client.isDenied:", () => {
142
+ globalThis.test("recognises a refusal in either platform's vocabulary", () => {
143
+ let entries = [errorOfType("Unauthorized")];
144
+ globalThis.expect(Seed_Client$ReventlessSeed.isDenied(entries.map(prim => prim))).toBe(true);
145
+ let entries$1 = [Object.fromEntries([[
146
+ "message",
147
+ "Not Authorized to access X"
148
+ ]])];
149
+ globalThis.expect(Seed_Client$ReventlessSeed.isDenied(entries$1.map(prim => prim))).toBe(true);
150
+ });
151
+ globalThis.test("leaves every other failure alone", () => {
152
+ let entries = [errorOfType("ValidationException")];
153
+ globalThis.expect(Seed_Client$ReventlessSeed.isDenied(entries.map(prim => prim))).toBe(false);
154
+ globalThis.expect(Seed_Client$ReventlessSeed.isDenied([].map(prim => prim))).toBe(false);
155
+ });
156
+ });
157
+
60
158
  export {
61
159
  errors,
62
160
  errorOfType,
161
+ b64url,
162
+ jwt,
163
+ localToken,
164
+ strings,
165
+ clientWith,
63
166
  }
64
167
  /* Not a pure module */