@_mustachio/openauth 0.12.0 → 0.13.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 (95) hide show
  1. package/dist/esm/domain/scim/discovery.js +206 -0
  2. package/dist/esm/domain/scim/filter.js +177 -0
  3. package/dist/esm/domain/scim/handle.js +338 -0
  4. package/dist/esm/domain/scim/patch.js +568 -0
  5. package/dist/esm/domain/scim/resource.js +256 -0
  6. package/dist/esm/http/handlers/scim.js +59 -0
  7. package/dist/esm/http/middleware/tenant.js +25 -0
  8. package/dist/esm/http/router.js +3 -0
  9. package/dist/esm/index.js +1 -0
  10. package/dist/esm/methods/saml-sp/acs.js +44 -20
  11. package/dist/esm/methods/saml-sp/attributes.js +2 -0
  12. package/dist/esm/methods/saml-sp/authnrequest.js +2 -2
  13. package/dist/esm/methods/saml-sp/factory.js +12 -1
  14. package/dist/esm/methods/saml-sp/metadata.js +5 -3
  15. package/dist/esm/methods/saml-sp/saml-instance.js +13 -3
  16. package/dist/esm/methods/saml-sp/slo-initiate.js +2 -2
  17. package/dist/esm/methods/saml-sp/sls.js +2 -2
  18. package/dist/esm/ports/scim-directory.js +0 -0
  19. package/dist/esm/types/error.js +5 -0
  20. package/dist/esm/types/scim.js +0 -0
  21. package/dist/types/domain/scim/discovery.d.ts +11 -0
  22. package/dist/types/domain/scim/discovery.d.ts.map +1 -0
  23. package/dist/types/domain/scim/filter.d.ts +51 -0
  24. package/dist/types/domain/scim/filter.d.ts.map +1 -0
  25. package/dist/types/domain/scim/handle.d.ts +35 -0
  26. package/dist/types/domain/scim/handle.d.ts.map +1 -0
  27. package/dist/types/domain/scim/patch.d.ts +73 -0
  28. package/dist/types/domain/scim/patch.d.ts.map +1 -0
  29. package/dist/types/domain/scim/resource.d.ts +56 -0
  30. package/dist/types/domain/scim/resource.d.ts.map +1 -0
  31. package/dist/types/http/context.d.ts +3 -0
  32. package/dist/types/http/context.d.ts.map +1 -1
  33. package/dist/types/http/handlers/method-route.d.ts.map +1 -1
  34. package/dist/types/http/handlers/scim.d.ts +3 -0
  35. package/dist/types/http/handlers/scim.d.ts.map +1 -0
  36. package/dist/types/http/middleware/tenant.d.ts.map +1 -1
  37. package/dist/types/http/router.d.ts.map +1 -1
  38. package/dist/types/index.d.ts +2 -0
  39. package/dist/types/index.d.ts.map +1 -1
  40. package/dist/types/methods/saml-sp/acs.d.ts.map +1 -1
  41. package/dist/types/methods/saml-sp/attributes.d.ts +5 -0
  42. package/dist/types/methods/saml-sp/attributes.d.ts.map +1 -1
  43. package/dist/types/methods/saml-sp/authnrequest.d.ts.map +1 -1
  44. package/dist/types/methods/saml-sp/factory.d.ts +217 -1
  45. package/dist/types/methods/saml-sp/factory.d.ts.map +1 -1
  46. package/dist/types/methods/saml-sp/metadata.d.ts +8 -0
  47. package/dist/types/methods/saml-sp/metadata.d.ts.map +1 -1
  48. package/dist/types/methods/saml-sp/parse-idp-metadata.d.ts.map +1 -1
  49. package/dist/types/methods/saml-sp/saml-instance.d.ts +12 -0
  50. package/dist/types/methods/saml-sp/saml-instance.d.ts.map +1 -1
  51. package/dist/types/methods/saml-sp/slo-initiate.d.ts.map +1 -1
  52. package/dist/types/methods/saml-sp/sls.d.ts.map +1 -1
  53. package/dist/types/methods/saml-sp/types.d.ts +95 -0
  54. package/dist/types/methods/saml-sp/types.d.ts.map +1 -1
  55. package/dist/types/ports/scim-directory.d.ts +104 -0
  56. package/dist/types/ports/scim-directory.d.ts.map +1 -0
  57. package/dist/types/ports/session-store.d.ts.map +1 -1
  58. package/dist/types/types/error.d.ts +5 -0
  59. package/dist/types/types/error.d.ts.map +1 -1
  60. package/dist/types/types/idp.d.ts +12 -0
  61. package/dist/types/types/idp.d.ts.map +1 -1
  62. package/dist/types/types/scim.d.ts +269 -0
  63. package/dist/types/types/scim.d.ts.map +1 -0
  64. package/dist/types/types/tenant.d.ts +10 -0
  65. package/dist/types/types/tenant.d.ts.map +1 -1
  66. package/package.json +1 -1
  67. package/src/domain/scim/discovery.ts +262 -0
  68. package/src/domain/scim/filter.ts +295 -0
  69. package/src/domain/scim/handle.ts +597 -0
  70. package/src/domain/scim/patch.ts +807 -0
  71. package/src/domain/scim/resource.ts +358 -0
  72. package/src/http/context.ts +3 -0
  73. package/src/http/handlers/authorize.ts +0 -1
  74. package/src/http/handlers/method-route.ts +1 -4
  75. package/src/http/handlers/scim.ts +107 -0
  76. package/src/http/middleware/tenant.ts +47 -1
  77. package/src/http/router.ts +9 -0
  78. package/src/index.ts +20 -0
  79. package/src/methods/saml-sp/acs.ts +99 -49
  80. package/src/methods/saml-sp/attributes.ts +12 -3
  81. package/src/methods/saml-sp/authnrequest.ts +3 -2
  82. package/src/methods/saml-sp/factory.ts +24 -3
  83. package/src/methods/saml-sp/metadata.ts +19 -8
  84. package/src/methods/saml-sp/parse-idp-metadata.ts +3 -9
  85. package/src/methods/saml-sp/saml-instance.ts +42 -5
  86. package/src/methods/saml-sp/slo-initiate.ts +6 -3
  87. package/src/methods/saml-sp/sls.ts +3 -2
  88. package/src/methods/saml-sp/types.ts +95 -0
  89. package/src/ports/CONSISTENCY.md +28 -25
  90. package/src/ports/scim-directory.ts +166 -0
  91. package/src/ports/session-store.ts +1 -5
  92. package/src/types/error.ts +11 -0
  93. package/src/types/idp.ts +12 -0
  94. package/src/types/scim.ts +280 -0
  95. package/src/types/tenant.ts +10 -0
@@ -0,0 +1,206 @@
1
+ // src/domain/scim/discovery.ts
2
+ import {
3
+ SCIM_ENTERPRISE_SCHEMA,
4
+ SCIM_GROUP_SCHEMA,
5
+ SCIM_LIST_SCHEMA,
6
+ SCIM_USER_SCHEMA
7
+ } from "./resource";
8
+ function serviceProviderConfig(baseUrl, maxResults) {
9
+ return {
10
+ schemas: ["urn:ietf:params:scim:schemas:core:2.0:ServiceProviderConfig"],
11
+ documentationUri: "https://datatracker.ietf.org/doc/html/rfc7644",
12
+ patch: { supported: true },
13
+ bulk: { supported: false, maxOperations: 0, maxPayloadSize: 0 },
14
+ filter: { supported: true, maxResults },
15
+ changePassword: { supported: false },
16
+ sort: { supported: false },
17
+ etag: { supported: false },
18
+ authenticationSchemes: [
19
+ {
20
+ type: "oauthbearertoken",
21
+ name: "OAuth Bearer Token",
22
+ description: "Authentication via the bearer token issued for this SCIM connection",
23
+ specUri: "https://datatracker.ietf.org/doc/html/rfc6750",
24
+ primary: true
25
+ }
26
+ ],
27
+ meta: {
28
+ resourceType: "ServiceProviderConfig",
29
+ location: `${baseUrl}/ServiceProviderConfig`
30
+ }
31
+ };
32
+ }
33
+ function resourceTypes(baseUrl, groups) {
34
+ const user = {
35
+ schemas: ["urn:ietf:params:scim:schemas:core:2.0:ResourceType"],
36
+ id: "User",
37
+ name: "User",
38
+ endpoint: "/Users",
39
+ description: "User Account",
40
+ schema: SCIM_USER_SCHEMA,
41
+ schemaExtensions: [{ schema: SCIM_ENTERPRISE_SCHEMA, required: false }],
42
+ meta: {
43
+ resourceType: "ResourceType",
44
+ location: `${baseUrl}/ResourceTypes/User`
45
+ }
46
+ };
47
+ const group = {
48
+ schemas: ["urn:ietf:params:scim:schemas:core:2.0:ResourceType"],
49
+ id: "Group",
50
+ name: "Group",
51
+ endpoint: "/Groups",
52
+ description: "Group",
53
+ schema: SCIM_GROUP_SCHEMA,
54
+ meta: {
55
+ resourceType: "ResourceType",
56
+ location: `${baseUrl}/ResourceTypes/Group`
57
+ }
58
+ };
59
+ const resources = groups ? [user, group] : [user];
60
+ return {
61
+ schemas: [SCIM_LIST_SCHEMA],
62
+ totalResults: resources.length,
63
+ startIndex: 1,
64
+ itemsPerPage: resources.length,
65
+ Resources: resources
66
+ };
67
+ }
68
+ var str = (name, extra = {}) => ({
69
+ name,
70
+ type: "string",
71
+ multiValued: false,
72
+ required: false,
73
+ caseExact: false,
74
+ mutability: "readWrite",
75
+ returned: "default",
76
+ uniqueness: "none",
77
+ ...extra
78
+ });
79
+ var multiValueSub = [
80
+ str("value"),
81
+ str("type"),
82
+ {
83
+ name: "primary",
84
+ type: "boolean",
85
+ multiValued: false,
86
+ required: false,
87
+ mutability: "readWrite",
88
+ returned: "default"
89
+ }
90
+ ];
91
+ function schemas(baseUrl, groups) {
92
+ const userSchema = {
93
+ id: SCIM_USER_SCHEMA,
94
+ name: "User",
95
+ description: "User Account",
96
+ attributes: [
97
+ str("userName", { required: true, uniqueness: "server" }),
98
+ {
99
+ name: "name",
100
+ type: "complex",
101
+ multiValued: false,
102
+ required: false,
103
+ mutability: "readWrite",
104
+ returned: "default",
105
+ subAttributes: [
106
+ str("formatted"),
107
+ str("familyName"),
108
+ str("givenName"),
109
+ str("middleName"),
110
+ str("honorificPrefix"),
111
+ str("honorificSuffix")
112
+ ]
113
+ },
114
+ str("displayName"),
115
+ {
116
+ name: "active",
117
+ type: "boolean",
118
+ multiValued: false,
119
+ required: false,
120
+ mutability: "readWrite",
121
+ returned: "default"
122
+ },
123
+ {
124
+ name: "emails",
125
+ type: "complex",
126
+ multiValued: true,
127
+ required: false,
128
+ mutability: "readWrite",
129
+ returned: "default",
130
+ subAttributes: multiValueSub
131
+ },
132
+ {
133
+ name: "phoneNumbers",
134
+ type: "complex",
135
+ multiValued: true,
136
+ required: false,
137
+ mutability: "readWrite",
138
+ returned: "default",
139
+ subAttributes: multiValueSub
140
+ }
141
+ ],
142
+ meta: {
143
+ resourceType: "Schema",
144
+ location: `${baseUrl}/Schemas/${SCIM_USER_SCHEMA}`
145
+ }
146
+ };
147
+ const enterpriseSchema = {
148
+ id: SCIM_ENTERPRISE_SCHEMA,
149
+ name: "EnterpriseUser",
150
+ description: "Enterprise User Extension",
151
+ attributes: [
152
+ str("employeeNumber"),
153
+ str("costCenter"),
154
+ str("organization"),
155
+ str("division"),
156
+ str("department"),
157
+ {
158
+ name: "manager",
159
+ type: "complex",
160
+ multiValued: false,
161
+ required: false,
162
+ mutability: "readWrite",
163
+ returned: "default",
164
+ subAttributes: [str("value"), str("displayName")]
165
+ }
166
+ ],
167
+ meta: {
168
+ resourceType: "Schema",
169
+ location: `${baseUrl}/Schemas/${SCIM_ENTERPRISE_SCHEMA}`
170
+ }
171
+ };
172
+ const groupSchema = {
173
+ id: SCIM_GROUP_SCHEMA,
174
+ name: "Group",
175
+ description: "Group",
176
+ attributes: [
177
+ str("displayName", { required: true }),
178
+ {
179
+ name: "members",
180
+ type: "complex",
181
+ multiValued: true,
182
+ required: false,
183
+ mutability: "readWrite",
184
+ returned: "default",
185
+ subAttributes: [str("value"), str("display"), str("type")]
186
+ }
187
+ ],
188
+ meta: {
189
+ resourceType: "Schema",
190
+ location: `${baseUrl}/Schemas/${SCIM_GROUP_SCHEMA}`
191
+ }
192
+ };
193
+ const resources = groups ? [userSchema, enterpriseSchema, groupSchema] : [userSchema, enterpriseSchema];
194
+ return {
195
+ schemas: [SCIM_LIST_SCHEMA],
196
+ totalResults: resources.length,
197
+ startIndex: 1,
198
+ itemsPerPage: resources.length,
199
+ Resources: resources
200
+ };
201
+ }
202
+ export {
203
+ resourceTypes,
204
+ schemas,
205
+ serviceProviderConfig
206
+ };
@@ -0,0 +1,177 @@
1
+ // src/domain/scim/filter.ts
2
+ import { err, ok } from "../../types/result";
3
+ var SUPPORTED_FILTER_HELP = 'supported filters are: userName eq "…", externalId eq "…", id eq "…", ' + 'active eq true|false, emails[type eq "…"].value eq "…", and two of ' + 'those joined by "and"';
4
+ var ATTRIBUTES = {
5
+ id: "id",
6
+ username: "userName",
7
+ externalid: "externalId",
8
+ active: "active",
9
+ "emails.value": "emails.value",
10
+ displayname: "displayName"
11
+ };
12
+ var USER_FILTER_ATTRIBUTES = new Set(["id", "userName", "externalId", "active", "emails.value"]);
13
+ var GROUP_FILTER_ATTRIBUTES = new Set(["id", "displayName", "externalId"]);
14
+ function normalizeComplexPath(raw) {
15
+ const m = /^([A-Za-z]+)\[[^\]]*\]\.([A-Za-z]+)$/.exec(raw.trim());
16
+ if (!m)
17
+ return raw.trim();
18
+ return `${m[1]}.${m[2]}`;
19
+ }
20
+ function findOperator(term) {
21
+ let depth = 0;
22
+ let inQuotes = false;
23
+ for (let i = 0;i < term.length; i++) {
24
+ const ch = term[i];
25
+ if (ch === '"' && term[i - 1] !== "\\") {
26
+ inQuotes = !inQuotes;
27
+ continue;
28
+ }
29
+ if (inQuotes)
30
+ continue;
31
+ if (ch === "[")
32
+ depth++;
33
+ else if (ch === "]")
34
+ depth--;
35
+ else if (depth === 0 && ch === " ") {
36
+ const m = /^\s+([A-Za-z]{2,3})(?:\s+|$)/.exec(term.slice(i));
37
+ if (m) {
38
+ return {
39
+ attr: term.slice(0, i),
40
+ op: m[1],
41
+ value: term.slice(i + m[0].length)
42
+ };
43
+ }
44
+ }
45
+ }
46
+ return null;
47
+ }
48
+ function parseTerm(raw, allowed) {
49
+ const term = raw.trim();
50
+ const found = findOperator(term);
51
+ if (!found) {
52
+ return err({
53
+ detail: `could not parse filter term "${term}". ${SUPPORTED_FILTER_HELP}`
54
+ });
55
+ }
56
+ const { attr: rawAttr, op: rawOp, value: rawValue } = found;
57
+ if (rawOp.toLowerCase() !== "eq") {
58
+ return err({
59
+ detail: `operator "${rawOp}" is not supported; only "eq" is. ` + SUPPORTED_FILTER_HELP
60
+ });
61
+ }
62
+ const attrKey = normalizeComplexPath(rawAttr).toLowerCase();
63
+ const attribute = ATTRIBUTES[attrKey];
64
+ if (!attribute || !allowed.has(attribute)) {
65
+ return err({
66
+ detail: `attribute "${rawAttr.trim()}" is not filterable. ` + SUPPORTED_FILTER_HELP
67
+ });
68
+ }
69
+ const value = rawValue.trim();
70
+ if (attribute === "active") {
71
+ const lowered = value.toLowerCase();
72
+ if (lowered !== "true" && lowered !== "false") {
73
+ return err({
74
+ detail: `"active" must be compared to true or false, got ${value}`
75
+ });
76
+ }
77
+ return ok({ op: "eq", attribute, value: lowered === "true" });
78
+ }
79
+ if (value.length < 2 || !value.startsWith('"') || !value.endsWith('"')) {
80
+ return err({
81
+ detail: `value for "${attribute}" must be a quoted string, got ${value}`
82
+ });
83
+ }
84
+ const unquoted = value.slice(1, -1).replace(/\\"/g, '"').replace(/\\\\/g, "\\");
85
+ if (unquoted.length === 0) {
86
+ return err({ detail: `value for "${attribute}" must not be empty` });
87
+ }
88
+ return ok({ op: "eq", attribute, value: unquoted });
89
+ }
90
+ function parseScimFilter(raw, allowed = USER_FILTER_ATTRIBUTES) {
91
+ if (raw === null || raw === undefined || raw.trim().length === 0) {
92
+ return ok(undefined);
93
+ }
94
+ const input = raw.trim();
95
+ const masked = maskQuoted(input);
96
+ if (masked.includes("(") || masked.includes(")")) {
97
+ return err({
98
+ detail: `grouped expressions are not supported. ${SUPPORTED_FILTER_HELP}`
99
+ });
100
+ }
101
+ const parts = splitTopLevelAnd(input);
102
+ if (parts.length > 2) {
103
+ return err({
104
+ detail: `at most two terms joined by "and" are supported. ` + SUPPORTED_FILTER_HELP
105
+ });
106
+ }
107
+ if (/\sor\s/i.test(masked)) {
108
+ return err({
109
+ detail: `"or" is not supported. ${SUPPORTED_FILTER_HELP}`
110
+ });
111
+ }
112
+ if (/(^|\s)not\s/i.test(masked)) {
113
+ return err({
114
+ detail: `"not" is not supported. ${SUPPORTED_FILTER_HELP}`
115
+ });
116
+ }
117
+ const first = parseTerm(parts[0], allowed);
118
+ if (!first.ok)
119
+ return first;
120
+ if (parts.length === 1)
121
+ return ok(first.value);
122
+ const second = parseTerm(parts[1], allowed);
123
+ if (!second.ok)
124
+ return second;
125
+ return ok({ op: "and", left: first.value, right: second.value });
126
+ }
127
+ function maskQuoted(input) {
128
+ let out = "";
129
+ let inQuotes = false;
130
+ for (let i = 0;i < input.length; i++) {
131
+ const ch = input[i];
132
+ if (ch === '"' && input[i - 1] !== "\\") {
133
+ inQuotes = !inQuotes;
134
+ out += ch;
135
+ continue;
136
+ }
137
+ out += inQuotes ? " " : ch;
138
+ }
139
+ return out;
140
+ }
141
+ function splitTopLevelAnd(input) {
142
+ const parts = [];
143
+ let depth = 0;
144
+ let inQuotes = false;
145
+ let start = 0;
146
+ for (let i = 0;i < input.length; i++) {
147
+ const ch = input[i];
148
+ if (ch === '"' && input[i - 1] !== "\\") {
149
+ inQuotes = !inQuotes;
150
+ continue;
151
+ }
152
+ if (inQuotes)
153
+ continue;
154
+ if (ch === "[")
155
+ depth++;
156
+ else if (ch === "]")
157
+ depth--;
158
+ else if (depth === 0 && (ch === "a" || ch === "A")) {
159
+ const window = input.slice(i, i + 3);
160
+ const before = input[i - 1];
161
+ const after = input[i + 3];
162
+ if (window.toLowerCase() === "and" && (before === " " || before === undefined) && (after === " " || after === undefined)) {
163
+ parts.push(input.slice(start, i));
164
+ start = i + 3;
165
+ i += 2;
166
+ }
167
+ }
168
+ }
169
+ parts.push(input.slice(start));
170
+ return parts.map((p) => p.trim()).filter((p) => p.length > 0);
171
+ }
172
+ export {
173
+ GROUP_FILTER_ATTRIBUTES,
174
+ SUPPORTED_FILTER_HELP,
175
+ USER_FILTER_ATTRIBUTES,
176
+ parseScimFilter
177
+ };