@pvh-afl/core 1.1.21 → 1.1.22
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/dist/auth/auth.module.d.ts +58 -0
- package/dist/auth/auth.module.d.ts.map +1 -0
- package/dist/auth/auth.module.js +87 -0
- package/dist/auth/auth.module.js.map +1 -0
- package/dist/auth/auth.types.d.ts +122 -0
- package/dist/auth/auth.types.d.ts.map +1 -0
- package/dist/auth/auth.types.js +41 -0
- package/dist/auth/auth.types.js.map +1 -0
- package/dist/auth/current-user.decorator.d.ts +17 -0
- package/dist/auth/current-user.decorator.d.ts.map +1 -0
- package/dist/auth/current-user.decorator.js +22 -0
- package/dist/auth/current-user.decorator.js.map +1 -0
- package/dist/auth/index.d.ts +10 -0
- package/dist/auth/index.d.ts.map +1 -0
- package/dist/auth/index.js +19 -0
- package/dist/auth/index.js.map +1 -0
- package/dist/auth/jwt-auth.guard.d.ts +21 -0
- package/dist/auth/jwt-auth.guard.d.ts.map +1 -0
- package/dist/auth/jwt-auth.guard.js +84 -0
- package/dist/auth/jwt-auth.guard.js.map +1 -0
- package/dist/auth/jwt.service.d.ts +93 -0
- package/dist/auth/jwt.service.d.ts.map +1 -0
- package/dist/auth/jwt.service.js +185 -0
- package/dist/auth/jwt.service.js.map +1 -0
- package/dist/auth/optional-jwt-auth.guard.d.ts +34 -0
- package/dist/auth/optional-jwt-auth.guard.d.ts.map +1 -0
- package/dist/auth/optional-jwt-auth.guard.js +55 -0
- package/dist/auth/optional-jwt-auth.guard.js.map +1 -0
- package/dist/auth/ownership.d.ts +65 -0
- package/dist/auth/ownership.d.ts.map +1 -0
- package/dist/auth/ownership.js +117 -0
- package/dist/auth/ownership.js.map +1 -0
- package/dist/auth/utils.d.ts +53 -0
- package/dist/auth/utils.d.ts.map +1 -0
- package/dist/auth/utils.js +86 -0
- package/dist/auth/utils.js.map +1 -0
- package/dist/integrations/commerce/commerce.types.d.ts +4 -1
- package/dist/integrations/commerce/commerce.types.d.ts.map +1 -1
- package/dist/integrations/commerce/shopify.service.d.ts +2 -2
- package/dist/integrations/commerce/shopify.service.d.ts.map +1 -1
- package/dist/integrations/commerce/shopify.service.js +22 -4
- package/dist/integrations/commerce/shopify.service.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.AuthTokenService = exports.TokenWrongTypeError = exports.TokenInvalidError = exports.TokenExpiredError = void 0;
|
|
4
|
+
const tslib_1 = require("tslib");
|
|
5
|
+
const common_1 = require("@nestjs/common");
|
|
6
|
+
const jwt_1 = require("@nestjs/jwt");
|
|
7
|
+
const auth_types_1 = require("./auth.types");
|
|
8
|
+
const utils_1 = require("./utils");
|
|
9
|
+
/** Thrown when a token is well-formed and correctly signed but has expired. */
|
|
10
|
+
class TokenExpiredError extends Error {
|
|
11
|
+
}
|
|
12
|
+
exports.TokenExpiredError = TokenExpiredError;
|
|
13
|
+
/** Thrown when a token fails any other check. */
|
|
14
|
+
class TokenInvalidError extends Error {
|
|
15
|
+
}
|
|
16
|
+
exports.TokenInvalidError = TokenInvalidError;
|
|
17
|
+
/** Thrown when a refresh token is used as an access token, or the reverse. */
|
|
18
|
+
class TokenWrongTypeError extends Error {
|
|
19
|
+
}
|
|
20
|
+
exports.TokenWrongTypeError = TokenWrongTypeError;
|
|
21
|
+
const DEFAULT_TTL_SECONDS = 1800; // 30 minutes
|
|
22
|
+
const DEFAULT_REFRESH_TTL_SECONDS = 604800; // 7 days
|
|
23
|
+
/**
|
|
24
|
+
* Signs and verifies the authentication tokens this service issues.
|
|
25
|
+
*
|
|
26
|
+
* Two kinds, distinguished by a `typ` claim that is checked on every verify:
|
|
27
|
+
*
|
|
28
|
+
* - **access** (30 min) — sent on every protected request.
|
|
29
|
+
* - **refresh** (7 days) — sent only to `/auth/refresh`, to mint a new access
|
|
30
|
+
* token without going back to Shopify.
|
|
31
|
+
*
|
|
32
|
+
* The refresh token carries the same identity claims as an access token, which is
|
|
33
|
+
* what lets renewal be a purely local operation: no Shopify call, so renewal
|
|
34
|
+
* costs nothing against Shopify's rate limit.
|
|
35
|
+
*
|
|
36
|
+
* Fixed to RS256, and the algorithm is pinned on verify rather than read from the
|
|
37
|
+
* token. That is deliberate: the JWT spec allows `alg: none`, and a verifier that
|
|
38
|
+
* trusts the token's own header can also be tricked into validating an RS256
|
|
39
|
+
* token as HS256 using the public key as the HMAC secret. Both attacks fail
|
|
40
|
+
* against an explicit allow-list of one algorithm.
|
|
41
|
+
*/
|
|
42
|
+
let AuthTokenService = class AuthTokenService {
|
|
43
|
+
jwt;
|
|
44
|
+
options;
|
|
45
|
+
ttlSeconds;
|
|
46
|
+
refreshTtlSeconds;
|
|
47
|
+
constructor(jwt, options) {
|
|
48
|
+
this.jwt = jwt;
|
|
49
|
+
this.options = options;
|
|
50
|
+
this.ttlSeconds = options.ttlSeconds ?? DEFAULT_TTL_SECONDS;
|
|
51
|
+
this.refreshTtlSeconds =
|
|
52
|
+
options.refreshTtlSeconds ?? DEFAULT_REFRESH_TTL_SECONDS;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Issue an access and refresh token for a customer whose identity Shopify has
|
|
56
|
+
* already confirmed.
|
|
57
|
+
*
|
|
58
|
+
* @param customerId - Shopify customer GID, taken from Shopify's response.
|
|
59
|
+
* @param phone - The customer's phone number as Shopify holds it, if any.
|
|
60
|
+
* Stored as a hash so a `mobile` value in a later request can be checked
|
|
61
|
+
* against it without the number appearing in the token.
|
|
62
|
+
* @param maxExpiresAt - Optional upper bound on both expiries, normally an
|
|
63
|
+
* upstream credential's own expiry, so neither token outlives it.
|
|
64
|
+
*/
|
|
65
|
+
async issuePair(customerId, phone, maxExpiresAt) {
|
|
66
|
+
const [access, refresh] = await Promise.all([
|
|
67
|
+
this.sign(auth_types_1.TokenType.ACCESS, customerId, phone, this.ttlSeconds, maxExpiresAt),
|
|
68
|
+
this.sign(auth_types_1.TokenType.REFRESH, customerId, phone, this.refreshTtlSeconds, maxExpiresAt),
|
|
69
|
+
]);
|
|
70
|
+
return {
|
|
71
|
+
accessToken: access.token,
|
|
72
|
+
accessExpiresAt: access.expiresAt,
|
|
73
|
+
refreshToken: refresh.token,
|
|
74
|
+
refreshExpiresAt: refresh.expiresAt,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Issue a replacement access token from a verified refresh token.
|
|
79
|
+
*
|
|
80
|
+
* The refresh token itself is **not** replaced: its expiry is the absolute end
|
|
81
|
+
* of the session (see `refreshTtlSeconds`). The caller keeps using the same one
|
|
82
|
+
* until it expires, then logs in again.
|
|
83
|
+
*/
|
|
84
|
+
async issueAccessToken(user, maxExpiresAt) {
|
|
85
|
+
return this.signFromHash(auth_types_1.TokenType.ACCESS, user.customerId, user.phoneHash, this.ttlSeconds, maxExpiresAt);
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Verify an access token and return the caller it identifies.
|
|
89
|
+
*
|
|
90
|
+
* @throws {TokenExpiredError} past `exp` — the client should refresh and retry.
|
|
91
|
+
* @throws {TokenWrongTypeError} a refresh token was sent instead.
|
|
92
|
+
* @throws {TokenInvalidError} anything else.
|
|
93
|
+
*/
|
|
94
|
+
async verifyAccess(token) {
|
|
95
|
+
return this.verify(token, auth_types_1.TokenType.ACCESS);
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Verify a refresh token and return the caller it identifies.
|
|
99
|
+
*
|
|
100
|
+
* @throws {TokenExpiredError} the session has reached its absolute end; the
|
|
101
|
+
* customer has to log in again.
|
|
102
|
+
* @throws {TokenWrongTypeError} an access token was sent instead.
|
|
103
|
+
* @throws {TokenInvalidError} anything else.
|
|
104
|
+
*/
|
|
105
|
+
async verifyRefresh(token) {
|
|
106
|
+
return this.verify(token, auth_types_1.TokenType.REFRESH);
|
|
107
|
+
}
|
|
108
|
+
async verify(token, expected) {
|
|
109
|
+
let payload;
|
|
110
|
+
try {
|
|
111
|
+
payload = await this.jwt.verifyAsync(token, {
|
|
112
|
+
// Pinned here, never taken from the token header.
|
|
113
|
+
algorithms: ['RS256'],
|
|
114
|
+
publicKey: this.options.publicKey,
|
|
115
|
+
issuer: this.options.issuer,
|
|
116
|
+
audience: this.options.audience,
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
catch (error) {
|
|
120
|
+
// jsonwebtoken signals expiry with this name; treat it separately so the
|
|
121
|
+
// client knows to refresh rather than to send the user back to login.
|
|
122
|
+
if (error instanceof Error && error.name === 'TokenExpiredError') {
|
|
123
|
+
throw new TokenExpiredError('Token has expired');
|
|
124
|
+
}
|
|
125
|
+
throw new TokenInvalidError(error instanceof Error ? error.message : 'Token verification failed');
|
|
126
|
+
}
|
|
127
|
+
// Checked before anything else about the payload: a valid signature on the
|
|
128
|
+
// wrong kind of token is still the wrong token.
|
|
129
|
+
if (payload.typ !== expected) {
|
|
130
|
+
throw new TokenWrongTypeError(`Expected a ${expected} token but received ${payload.typ ?? 'none'}`);
|
|
131
|
+
}
|
|
132
|
+
// `sub` carries the identity, so an absent or non-string value means the
|
|
133
|
+
// token cannot authenticate anyone even though its signature is valid.
|
|
134
|
+
if (typeof payload.sub !== 'string' || payload.sub.trim() === '') {
|
|
135
|
+
throw new TokenInvalidError('Token has no subject');
|
|
136
|
+
}
|
|
137
|
+
return {
|
|
138
|
+
customerId: payload.sub,
|
|
139
|
+
phoneHash: typeof payload.phone_hash === 'string' ? payload.phone_hash : undefined,
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
sign(typ, customerId, phone, ttlSeconds, maxExpiresAt) {
|
|
143
|
+
return this.signFromHash(typ, customerId, phone ? (0, utils_1.hashPhone)(phone) : undefined, ttlSeconds, maxExpiresAt);
|
|
144
|
+
}
|
|
145
|
+
async signFromHash(typ, customerId, phoneHash, ttlSeconds, maxExpiresAt) {
|
|
146
|
+
const nowSeconds = Math.floor(Date.now() / 1000);
|
|
147
|
+
const exp = this.resolveExpiry(nowSeconds, ttlSeconds, maxExpiresAt);
|
|
148
|
+
const payload = {
|
|
149
|
+
iss: this.options.issuer,
|
|
150
|
+
aud: this.options.audience,
|
|
151
|
+
typ,
|
|
152
|
+
sub: customerId,
|
|
153
|
+
...(phoneHash ? { phone_hash: phoneHash } : {}),
|
|
154
|
+
iat: nowSeconds,
|
|
155
|
+
exp,
|
|
156
|
+
};
|
|
157
|
+
const token = await this.jwt.signAsync(payload, {
|
|
158
|
+
algorithm: 'RS256',
|
|
159
|
+
privateKey: this.options.privateKey,
|
|
160
|
+
// iss/aud/iat/exp are already in the payload; signing them again via
|
|
161
|
+
// options would make jsonwebtoken reject the duplicate keys.
|
|
162
|
+
});
|
|
163
|
+
return { token, expiresAt: new Date(exp * 1000) };
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* Work out the `exp` claim: the given lifetime, shortened if an upstream
|
|
167
|
+
* credential expires sooner.
|
|
168
|
+
*/
|
|
169
|
+
resolveExpiry(nowSeconds, ttlSeconds, maxExpiresAt) {
|
|
170
|
+
const ownExpiry = nowSeconds + ttlSeconds;
|
|
171
|
+
if (!maxExpiresAt)
|
|
172
|
+
return ownExpiry;
|
|
173
|
+
const upstream = new Date(maxExpiresAt).getTime();
|
|
174
|
+
if (Number.isNaN(upstream))
|
|
175
|
+
return ownExpiry;
|
|
176
|
+
return Math.min(ownExpiry, Math.floor(upstream / 1000));
|
|
177
|
+
}
|
|
178
|
+
};
|
|
179
|
+
exports.AuthTokenService = AuthTokenService;
|
|
180
|
+
exports.AuthTokenService = AuthTokenService = tslib_1.__decorate([
|
|
181
|
+
(0, common_1.Injectable)(),
|
|
182
|
+
tslib_1.__param(1, (0, common_1.Inject)(auth_types_1.AUTH_OPTIONS)),
|
|
183
|
+
tslib_1.__metadata("design:paramtypes", [jwt_1.JwtService, Object])
|
|
184
|
+
], AuthTokenService);
|
|
185
|
+
//# sourceMappingURL=jwt.service.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"jwt.service.js","sourceRoot":"","sources":["../../src/auth/jwt.service.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { CanActivate, ExecutionContext } from '@nestjs/common';
|
|
2
|
+
import { AuthTokenService } from './jwt.service';
|
|
3
|
+
/**
|
|
4
|
+
* Attaches the caller when a token is supplied, and lets the request through when
|
|
5
|
+
* none is.
|
|
6
|
+
*
|
|
7
|
+
* For routes that legitimately serve both guests and signed-in customers — the
|
|
8
|
+
* wishlist, where a shopper builds a list before they have an account. Requiring a
|
|
9
|
+
* token there would break guest wishlists; ignoring tokens entirely would leave no
|
|
10
|
+
* way to tell the two apart.
|
|
11
|
+
*
|
|
12
|
+
* "Optional" means optional to *send*. A token that is present but unusable is still
|
|
13
|
+
* rejected, with the same error code {@link JwtAuthGuard} would give:
|
|
14
|
+
*
|
|
15
|
+
* - no token → treated as a guest, request proceeds with no `user`
|
|
16
|
+
* - valid token → caller attached to the request
|
|
17
|
+
* - expired or invalid token → 401, exactly as on a protected route
|
|
18
|
+
*
|
|
19
|
+
* Degrading a broken token to "guest" instead would hide two real problems: a client
|
|
20
|
+
* with an expired token would silently receive guest data rather than being told to
|
|
21
|
+
* refresh, and a genuine bug in token handling would look like ordinary guest
|
|
22
|
+
* traffic.
|
|
23
|
+
*
|
|
24
|
+
* Because a guest reaches the handler with no `user`, this must be paired with
|
|
25
|
+
* `assertOwnsClaimedCustomer` — otherwise a caller could simply name any customer
|
|
26
|
+
* and be served their data. The guard establishes *whether* there is a caller; that
|
|
27
|
+
* assertion enforces what an unproven claim is allowed to do.
|
|
28
|
+
*/
|
|
29
|
+
export declare class OptionalJwtAuthGuard implements CanActivate {
|
|
30
|
+
private readonly strict;
|
|
31
|
+
constructor(tokens: AuthTokenService);
|
|
32
|
+
canActivate(context: ExecutionContext): Promise<boolean>;
|
|
33
|
+
}
|
|
34
|
+
//# sourceMappingURL=optional-jwt-auth.guard.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"optional-jwt-auth.guard.d.ts","sourceRoot":"","sources":["../../src/auth/optional-jwt-auth.guard.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,gBAAgB,EAAc,MAAM,gBAAgB,CAAC;AAE3E,OAAO,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AAGjD;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,qBACa,oBAAqB,YAAW,WAAW;IACtD,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAe;gBAE1B,MAAM,EAAE,gBAAgB;IAM9B,WAAW,CAAC,OAAO,EAAE,gBAAgB,GAAG,OAAO,CAAC,OAAO,CAAC;CAS/D"}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.OptionalJwtAuthGuard = void 0;
|
|
4
|
+
const tslib_1 = require("tslib");
|
|
5
|
+
const common_1 = require("@nestjs/common");
|
|
6
|
+
const jwt_auth_guard_1 = require("./jwt-auth.guard");
|
|
7
|
+
const jwt_service_1 = require("./jwt.service");
|
|
8
|
+
const utils_1 = require("./utils");
|
|
9
|
+
/**
|
|
10
|
+
* Attaches the caller when a token is supplied, and lets the request through when
|
|
11
|
+
* none is.
|
|
12
|
+
*
|
|
13
|
+
* For routes that legitimately serve both guests and signed-in customers — the
|
|
14
|
+
* wishlist, where a shopper builds a list before they have an account. Requiring a
|
|
15
|
+
* token there would break guest wishlists; ignoring tokens entirely would leave no
|
|
16
|
+
* way to tell the two apart.
|
|
17
|
+
*
|
|
18
|
+
* "Optional" means optional to *send*. A token that is present but unusable is still
|
|
19
|
+
* rejected, with the same error code {@link JwtAuthGuard} would give:
|
|
20
|
+
*
|
|
21
|
+
* - no token → treated as a guest, request proceeds with no `user`
|
|
22
|
+
* - valid token → caller attached to the request
|
|
23
|
+
* - expired or invalid token → 401, exactly as on a protected route
|
|
24
|
+
*
|
|
25
|
+
* Degrading a broken token to "guest" instead would hide two real problems: a client
|
|
26
|
+
* with an expired token would silently receive guest data rather than being told to
|
|
27
|
+
* refresh, and a genuine bug in token handling would look like ordinary guest
|
|
28
|
+
* traffic.
|
|
29
|
+
*
|
|
30
|
+
* Because a guest reaches the handler with no `user`, this must be paired with
|
|
31
|
+
* `assertOwnsClaimedCustomer` — otherwise a caller could simply name any customer
|
|
32
|
+
* and be served their data. The guard establishes *whether* there is a caller; that
|
|
33
|
+
* assertion enforces what an unproven claim is allowed to do.
|
|
34
|
+
*/
|
|
35
|
+
let OptionalJwtAuthGuard = class OptionalJwtAuthGuard {
|
|
36
|
+
strict;
|
|
37
|
+
constructor(tokens) {
|
|
38
|
+
// Delegate rather than duplicate, so the two guards cannot drift apart in how
|
|
39
|
+
// they verify a token or which error codes they return.
|
|
40
|
+
this.strict = new jwt_auth_guard_1.JwtAuthGuard(tokens);
|
|
41
|
+
}
|
|
42
|
+
async canActivate(context) {
|
|
43
|
+
const request = (0, utils_1.getRequest)(context);
|
|
44
|
+
if (!(0, utils_1.bearer)(request)) {
|
|
45
|
+
return true;
|
|
46
|
+
}
|
|
47
|
+
return this.strict.canActivate(context);
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
exports.OptionalJwtAuthGuard = OptionalJwtAuthGuard;
|
|
51
|
+
exports.OptionalJwtAuthGuard = OptionalJwtAuthGuard = tslib_1.__decorate([
|
|
52
|
+
(0, common_1.Injectable)(),
|
|
53
|
+
tslib_1.__metadata("design:paramtypes", [jwt_service_1.AuthTokenService])
|
|
54
|
+
], OptionalJwtAuthGuard);
|
|
55
|
+
//# sourceMappingURL=optional-jwt-auth.guard.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"optional-jwt-auth.guard.js","sourceRoot":"","sources":["../../src/auth/optional-jwt-auth.guard.ts"],"names":[],"mappings":";;;;AAAA,2CAA2E;AAC3E,qDAAgD;AAChD,+CAAiD;AACjD,mCAA6C;AAE7C;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAEI,IAAM,oBAAoB,GAA1B,MAAM,oBAAoB;IACd,MAAM,CAAe;IAEtC,YAAY,MAAwB;QAClC,8EAA8E;QAC9E,wDAAwD;QACxD,IAAI,CAAC,MAAM,GAAG,IAAI,6BAAY,CAAC,MAAM,CAAC,CAAC;IACzC,CAAC;IAED,KAAK,CAAC,WAAW,CAAC,OAAyB;QACzC,MAAM,OAAO,GAAG,IAAA,kBAAU,EAAC,OAAO,CAAC,CAAC;QAEpC,IAAI,CAAC,IAAA,cAAM,EAAC,OAAO,CAAC,EAAE,CAAC;YACrB,OAAO,IAAI,CAAC;QACd,CAAC;QAED,OAAO,IAAI,CAAC,MAAM,CAAC,WAAW,CAAC,OAAO,CAAC,CAAC;IAC1C,CAAC;CACF,CAAA;AAlBY,oDAAoB;+BAApB,oBAAoB;IADhC,IAAA,mBAAU,GAAE;6CAIS,8BAAgB;GAHzB,oBAAoB,CAkBhC"}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { AuthUser } from './auth.types';
|
|
2
|
+
/**
|
|
3
|
+
* Ownership assertions.
|
|
4
|
+
*
|
|
5
|
+
* `JwtAuthGuard` establishes *who* is calling. It cannot tell whether the
|
|
6
|
+
* records a request names belong to that caller — requests still carry
|
|
7
|
+
* `customerId`, `shopifyCustomerId` and `mobile` in their payloads, and a
|
|
8
|
+
* logged-in caller is free to put someone else's value there.
|
|
9
|
+
*
|
|
10
|
+
* These helpers close that gap: they compare a submitted value against the
|
|
11
|
+
* verified token and reject a mismatch. Call one at the top of every handler
|
|
12
|
+
* that accepts an identity value.
|
|
13
|
+
*
|
|
14
|
+
* A missing value is not a failure — many of these fields are optional, and
|
|
15
|
+
* "absent" means the handler has nothing to act on rather than an attempt to
|
|
16
|
+
* act on someone else. Validating that a required field is present stays the
|
|
17
|
+
* handler's own job.
|
|
18
|
+
*/
|
|
19
|
+
/**
|
|
20
|
+
* Assert that a submitted customer identifier is the caller's own.
|
|
21
|
+
*
|
|
22
|
+
* Accepts either form Shopify uses — `"7654321"` and
|
|
23
|
+
* `"gid://shopify/Customer/7654321"` refer to the same customer and both pass.
|
|
24
|
+
*
|
|
25
|
+
* @throws {ForbiddenException} when the value belongs to another customer.
|
|
26
|
+
*/
|
|
27
|
+
export declare function assertOwnsCustomer(user: AuthUser, submitted?: string | null): void;
|
|
28
|
+
/**
|
|
29
|
+
* For routes reachable by both guests and signed-in customers: if the request names a
|
|
30
|
+
* customer, require proof that the caller is that customer.
|
|
31
|
+
*
|
|
32
|
+
* Pairs with {@link OptionalJwtAuthGuard}, which leaves `user` undefined for a guest.
|
|
33
|
+
* The rule this encodes:
|
|
34
|
+
*
|
|
35
|
+
* - names no customer → allowed; the handler falls back to the guest `sessionId`
|
|
36
|
+
* - names a customer, no verified caller → 401, because the claim is unproven
|
|
37
|
+
* - names a customer that is not the caller's → 403
|
|
38
|
+
*
|
|
39
|
+
* Without this, an unauthenticated caller on an optional-auth route could name any
|
|
40
|
+
* customer and be served their data — exactly the hole the guard alone leaves open.
|
|
41
|
+
*
|
|
42
|
+
* The two failure codes differ because the client's next move differs: 401 means
|
|
43
|
+
* "authenticate and try again", 403 means "this is not yours, stop".
|
|
44
|
+
*
|
|
45
|
+
* @throws {UnauthorizedException} a customer is named but the caller is not
|
|
46
|
+
* authenticated.
|
|
47
|
+
* @throws {ForbiddenException} the named customer is somebody else.
|
|
48
|
+
*/
|
|
49
|
+
export declare function assertOwnsClaimedCustomer(user: AuthUser | undefined, submitted?: string | null): void;
|
|
50
|
+
/**
|
|
51
|
+
* Assert that a submitted mobile number is the caller's own.
|
|
52
|
+
*
|
|
53
|
+
* Compares hashes, so the raw number never has to be held anywhere. Numbers are
|
|
54
|
+
* normalised first, so `"9876543210"`, `"+91 98765 43210"` and `"919876543210"`
|
|
55
|
+
* all match the same customer.
|
|
56
|
+
*
|
|
57
|
+
* A caller whose Shopify record has no phone number cannot pass this check —
|
|
58
|
+
* there is nothing to compare against, so any submitted number is rejected
|
|
59
|
+
* rather than waved through.
|
|
60
|
+
*
|
|
61
|
+
* @throws {ForbiddenException} when the number belongs to another customer, or
|
|
62
|
+
* when the caller has no phone number on record.
|
|
63
|
+
*/
|
|
64
|
+
export declare function assertOwnsPhone(user: AuthUser, submitted?: string | null): void;
|
|
65
|
+
//# sourceMappingURL=ownership.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ownership.d.ts","sourceRoot":"","sources":["../../src/auth/ownership.ts"],"names":[],"mappings":"AAEA,OAAO,EAAiB,QAAQ,EAAE,MAAM,cAAc,CAAC;AAGvD;;;;;;;;;;;;;;;;GAgBG;AAEH;;;;;;;GAOG;AACH,wBAAgB,kBAAkB,CAChC,IAAI,EAAE,QAAQ,EACd,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,GACxB,IAAI,CAaN;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,yBAAyB,CACvC,IAAI,EAAE,QAAQ,GAAG,SAAS,EAC1B,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,GACxB,IAAI,CAaN;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,QAAQ,EAAE,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,GAAG,IAAI,CAwB/E"}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.assertOwnsCustomer = assertOwnsCustomer;
|
|
4
|
+
exports.assertOwnsClaimedCustomer = assertOwnsClaimedCustomer;
|
|
5
|
+
exports.assertOwnsPhone = assertOwnsPhone;
|
|
6
|
+
const common_1 = require("@nestjs/common");
|
|
7
|
+
const logger_1 = require("../common/logger");
|
|
8
|
+
const auth_types_1 = require("./auth.types");
|
|
9
|
+
const utils_1 = require("./utils");
|
|
10
|
+
/**
|
|
11
|
+
* Ownership assertions.
|
|
12
|
+
*
|
|
13
|
+
* `JwtAuthGuard` establishes *who* is calling. It cannot tell whether the
|
|
14
|
+
* records a request names belong to that caller — requests still carry
|
|
15
|
+
* `customerId`, `shopifyCustomerId` and `mobile` in their payloads, and a
|
|
16
|
+
* logged-in caller is free to put someone else's value there.
|
|
17
|
+
*
|
|
18
|
+
* These helpers close that gap: they compare a submitted value against the
|
|
19
|
+
* verified token and reject a mismatch. Call one at the top of every handler
|
|
20
|
+
* that accepts an identity value.
|
|
21
|
+
*
|
|
22
|
+
* A missing value is not a failure — many of these fields are optional, and
|
|
23
|
+
* "absent" means the handler has nothing to act on rather than an attempt to
|
|
24
|
+
* act on someone else. Validating that a required field is present stays the
|
|
25
|
+
* handler's own job.
|
|
26
|
+
*/
|
|
27
|
+
/**
|
|
28
|
+
* Assert that a submitted customer identifier is the caller's own.
|
|
29
|
+
*
|
|
30
|
+
* Accepts either form Shopify uses — `"7654321"` and
|
|
31
|
+
* `"gid://shopify/Customer/7654321"` refer to the same customer and both pass.
|
|
32
|
+
*
|
|
33
|
+
* @throws {ForbiddenException} when the value belongs to another customer.
|
|
34
|
+
*/
|
|
35
|
+
function assertOwnsCustomer(user, submitted) {
|
|
36
|
+
if (submitted == null || submitted === '')
|
|
37
|
+
return;
|
|
38
|
+
if ((0, utils_1.normalizeGid)(submitted) !== (0, utils_1.normalizeGid)(user.customerId)) {
|
|
39
|
+
logger_1.logger.warn('Ownership check failed on customer id', {
|
|
40
|
+
callerCustomerId: user.customerId,
|
|
41
|
+
});
|
|
42
|
+
throw new common_1.ForbiddenException({
|
|
43
|
+
code: auth_types_1.AuthErrorCode.FORBIDDEN,
|
|
44
|
+
message: 'Not permitted',
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* For routes reachable by both guests and signed-in customers: if the request names a
|
|
50
|
+
* customer, require proof that the caller is that customer.
|
|
51
|
+
*
|
|
52
|
+
* Pairs with {@link OptionalJwtAuthGuard}, which leaves `user` undefined for a guest.
|
|
53
|
+
* The rule this encodes:
|
|
54
|
+
*
|
|
55
|
+
* - names no customer → allowed; the handler falls back to the guest `sessionId`
|
|
56
|
+
* - names a customer, no verified caller → 401, because the claim is unproven
|
|
57
|
+
* - names a customer that is not the caller's → 403
|
|
58
|
+
*
|
|
59
|
+
* Without this, an unauthenticated caller on an optional-auth route could name any
|
|
60
|
+
* customer and be served their data — exactly the hole the guard alone leaves open.
|
|
61
|
+
*
|
|
62
|
+
* The two failure codes differ because the client's next move differs: 401 means
|
|
63
|
+
* "authenticate and try again", 403 means "this is not yours, stop".
|
|
64
|
+
*
|
|
65
|
+
* @throws {UnauthorizedException} a customer is named but the caller is not
|
|
66
|
+
* authenticated.
|
|
67
|
+
* @throws {ForbiddenException} the named customer is somebody else.
|
|
68
|
+
*/
|
|
69
|
+
function assertOwnsClaimedCustomer(user, submitted) {
|
|
70
|
+
if (submitted == null || submitted === '')
|
|
71
|
+
return;
|
|
72
|
+
if (!user) {
|
|
73
|
+
logger_1.logger.warn('Unauthenticated request named a customer id');
|
|
74
|
+
throw new common_1.UnauthorizedException({
|
|
75
|
+
code: auth_types_1.AuthErrorCode.TOKEN_MISSING,
|
|
76
|
+
message: 'Authentication required to act on a customer account',
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
assertOwnsCustomer(user, submitted);
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Assert that a submitted mobile number is the caller's own.
|
|
83
|
+
*
|
|
84
|
+
* Compares hashes, so the raw number never has to be held anywhere. Numbers are
|
|
85
|
+
* normalised first, so `"9876543210"`, `"+91 98765 43210"` and `"919876543210"`
|
|
86
|
+
* all match the same customer.
|
|
87
|
+
*
|
|
88
|
+
* A caller whose Shopify record has no phone number cannot pass this check —
|
|
89
|
+
* there is nothing to compare against, so any submitted number is rejected
|
|
90
|
+
* rather than waved through.
|
|
91
|
+
*
|
|
92
|
+
* @throws {ForbiddenException} when the number belongs to another customer, or
|
|
93
|
+
* when the caller has no phone number on record.
|
|
94
|
+
*/
|
|
95
|
+
function assertOwnsPhone(user, submitted) {
|
|
96
|
+
if (submitted == null || submitted === '')
|
|
97
|
+
return;
|
|
98
|
+
if (!user.phoneHash) {
|
|
99
|
+
logger_1.logger.warn('Ownership check failed: caller has no phone on record', {
|
|
100
|
+
callerCustomerId: user.customerId,
|
|
101
|
+
});
|
|
102
|
+
throw new common_1.ForbiddenException({
|
|
103
|
+
code: auth_types_1.AuthErrorCode.FORBIDDEN,
|
|
104
|
+
message: 'Not permitted',
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
if ((0, utils_1.hashPhone)(submitted) !== user.phoneHash) {
|
|
108
|
+
logger_1.logger.warn('Ownership check failed on mobile number', {
|
|
109
|
+
callerCustomerId: user.customerId,
|
|
110
|
+
});
|
|
111
|
+
throw new common_1.ForbiddenException({
|
|
112
|
+
code: auth_types_1.AuthErrorCode.FORBIDDEN,
|
|
113
|
+
message: 'Not permitted',
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
//# sourceMappingURL=ownership.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ownership.js","sourceRoot":"","sources":["../../src/auth/ownership.ts"],"names":[],"mappings":";;AA+BA,gDAgBC;AAuBD,8DAgBC;AAgBD,0CAwBC;AA9HD,2CAA2E;AAC3E,6CAA0C;AAC1C,6CAAuD;AACvD,mCAAkD;AAElD;;;;;;;;;;;;;;;;GAgBG;AAEH;;;;;;;GAOG;AACH,SAAgB,kBAAkB,CAChC,IAAc,EACd,SAAyB;IAEzB,IAAI,SAAS,IAAI,IAAI,IAAI,SAAS,KAAK,EAAE;QAAE,OAAO;IAElD,IAAI,IAAA,oBAAY,EAAC,SAAS,CAAC,KAAK,IAAA,oBAAY,EAAC,IAAI,CAAC,UAAU,CAAC,EAAE,CAAC;QAC9D,eAAM,CAAC,IAAI,CAAC,uCAAuC,EAAE;YACnD,gBAAgB,EAAE,IAAI,CAAC,UAAU;SAClC,CAAC,CAAC;QAEH,MAAM,IAAI,2BAAkB,CAAC;YAC3B,IAAI,EAAE,0BAAa,CAAC,SAAS;YAC7B,OAAO,EAAE,eAAe;SACzB,CAAC,CAAC;IACL,CAAC;AACH,CAAC;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,SAAgB,yBAAyB,CACvC,IAA0B,EAC1B,SAAyB;IAEzB,IAAI,SAAS,IAAI,IAAI,IAAI,SAAS,KAAK,EAAE;QAAE,OAAO;IAElD,IAAI,CAAC,IAAI,EAAE,CAAC;QACV,eAAM,CAAC,IAAI,CAAC,6CAA6C,CAAC,CAAC;QAE3D,MAAM,IAAI,8BAAqB,CAAC;YAC9B,IAAI,EAAE,0BAAa,CAAC,aAAa;YACjC,OAAO,EAAE,sDAAsD;SAChE,CAAC,CAAC;IACL,CAAC;IAED,kBAAkB,CAAC,IAAI,EAAE,SAAS,CAAC,CAAC;AACtC,CAAC;AAED;;;;;;;;;;;;;GAaG;AACH,SAAgB,eAAe,CAAC,IAAc,EAAE,SAAyB;IACvE,IAAI,SAAS,IAAI,IAAI,IAAI,SAAS,KAAK,EAAE;QAAE,OAAO;IAElD,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,CAAC;QACpB,eAAM,CAAC,IAAI,CAAC,uDAAuD,EAAE;YACnE,gBAAgB,EAAE,IAAI,CAAC,UAAU;SAClC,CAAC,CAAC;QAEH,MAAM,IAAI,2BAAkB,CAAC;YAC3B,IAAI,EAAE,0BAAa,CAAC,SAAS;YAC7B,OAAO,EAAE,eAAe;SACzB,CAAC,CAAC;IACL,CAAC;IAED,IAAI,IAAA,iBAAS,EAAC,SAAS,CAAC,KAAK,IAAI,CAAC,SAAS,EAAE,CAAC;QAC5C,eAAM,CAAC,IAAI,CAAC,yCAAyC,EAAE;YACrD,gBAAgB,EAAE,IAAI,CAAC,UAAU;SAClC,CAAC,CAAC;QAEH,MAAM,IAAI,2BAAkB,CAAC;YAC3B,IAAI,EAAE,0BAAa,CAAC,SAAS;YAC7B,OAAO,EAAE,eAAe;SACzB,CAAC,CAAC;IACL,CAAC;AACH,CAAC"}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { ExecutionContext } from '@nestjs/common';
|
|
2
|
+
/**
|
|
3
|
+
* Shape of the request object the guard reads from and writes to.
|
|
4
|
+
*/
|
|
5
|
+
export interface AuthRequest {
|
|
6
|
+
headers: Record<string, string | string[] | undefined>;
|
|
7
|
+
user?: unknown;
|
|
8
|
+
[key: string]: unknown;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Get the underlying request, whether the handler is a REST controller or a
|
|
12
|
+
* GraphQL resolver.
|
|
13
|
+
*
|
|
14
|
+
* This has to branch. In a GraphQL execution context the request is not where
|
|
15
|
+
* `switchToHttp()` looks, and in an HTTP context `GqlExecutionContext.create()`
|
|
16
|
+
* does not produce a usable one. Calling either unconditionally works for one
|
|
17
|
+
* transport and silently misbehaves on the other.
|
|
18
|
+
*/
|
|
19
|
+
export declare function getRequest(context: ExecutionContext): AuthRequest;
|
|
20
|
+
/**
|
|
21
|
+
* Extract a Bearer token from the `Authorization` header.
|
|
22
|
+
*
|
|
23
|
+
* Returns undefined when the header is absent or is not a Bearer credential —
|
|
24
|
+
* the caller decides whether that is an error.
|
|
25
|
+
*/
|
|
26
|
+
export declare function bearer(request: AuthRequest): string | undefined;
|
|
27
|
+
/**
|
|
28
|
+
* Reduce a Shopify customer identifier to its numeric part so the two forms
|
|
29
|
+
* compare equal.
|
|
30
|
+
*
|
|
31
|
+
* Callers send either `"7654321"` or `"gid://shopify/Customer/7654321"`, and
|
|
32
|
+
* both refer to the same customer. Comparing the raw strings would reject a
|
|
33
|
+
* legitimate request.
|
|
34
|
+
*/
|
|
35
|
+
export declare function normalizeGid(value: string): string;
|
|
36
|
+
/**
|
|
37
|
+
* Normalise an Indian mobile number to `91XXXXXXXXXX`.
|
|
38
|
+
*
|
|
39
|
+
* Mirrors the logic in the Shopify webhook handler so a number hashed at login
|
|
40
|
+
* matches the same number submitted in a request, whichever way it was typed:
|
|
41
|
+
* `"9876543210"`, `"+91 98765 43210"` and `"919876543210"` all converge.
|
|
42
|
+
*/
|
|
43
|
+
export declare function normalizePhone(phone: string): string;
|
|
44
|
+
/**
|
|
45
|
+
* SHA-256 as lowercase hex.
|
|
46
|
+
*/
|
|
47
|
+
export declare function sha256(value: string): string;
|
|
48
|
+
/**
|
|
49
|
+
* Hash a mobile number for storage in a token: normalise first, then hash, so
|
|
50
|
+
* the same number always produces the same digest.
|
|
51
|
+
*/
|
|
52
|
+
export declare function hashPhone(phone: string): string;
|
|
53
|
+
//# sourceMappingURL=utils.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../../src/auth/utils.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AAIlD;;GAEG;AACH,MAAM,WAAW,WAAW;IAC1B,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,SAAS,CAAC,CAAC;IACvD,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACxB;AAED;;;;;;;;GAQG;AACH,wBAAgB,UAAU,CAAC,OAAO,EAAE,gBAAgB,GAAG,WAAW,CAKjE;AAED;;;;;GAKG;AACH,wBAAgB,MAAM,CAAC,OAAO,EAAE,WAAW,GAAG,MAAM,GAAG,SAAS,CAY/D;AAED;;;;;;;GAOG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAIlD;AAED;;;;;;GAMG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAYpD;AAED;;GAEG;AACH,wBAAgB,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAE5C;AAED;;;GAGG;AACH,wBAAgB,SAAS,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAE/C"}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.getRequest = getRequest;
|
|
4
|
+
exports.bearer = bearer;
|
|
5
|
+
exports.normalizeGid = normalizeGid;
|
|
6
|
+
exports.normalizePhone = normalizePhone;
|
|
7
|
+
exports.sha256 = sha256;
|
|
8
|
+
exports.hashPhone = hashPhone;
|
|
9
|
+
const graphql_1 = require("@nestjs/graphql");
|
|
10
|
+
const crypto_1 = require("crypto");
|
|
11
|
+
/**
|
|
12
|
+
* Get the underlying request, whether the handler is a REST controller or a
|
|
13
|
+
* GraphQL resolver.
|
|
14
|
+
*
|
|
15
|
+
* This has to branch. In a GraphQL execution context the request is not where
|
|
16
|
+
* `switchToHttp()` looks, and in an HTTP context `GqlExecutionContext.create()`
|
|
17
|
+
* does not produce a usable one. Calling either unconditionally works for one
|
|
18
|
+
* transport and silently misbehaves on the other.
|
|
19
|
+
*/
|
|
20
|
+
function getRequest(context) {
|
|
21
|
+
if (context.getType() === 'graphql') {
|
|
22
|
+
return graphql_1.GqlExecutionContext.create(context).getContext().req;
|
|
23
|
+
}
|
|
24
|
+
return context.switchToHttp().getRequest();
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Extract a Bearer token from the `Authorization` header.
|
|
28
|
+
*
|
|
29
|
+
* Returns undefined when the header is absent or is not a Bearer credential —
|
|
30
|
+
* the caller decides whether that is an error.
|
|
31
|
+
*/
|
|
32
|
+
function bearer(request) {
|
|
33
|
+
const raw = request.headers?.authorization ?? request.headers?.Authorization;
|
|
34
|
+
const header = Array.isArray(raw) ? raw[0] : raw;
|
|
35
|
+
if (!header)
|
|
36
|
+
return undefined;
|
|
37
|
+
const [scheme, token] = header.split(' ');
|
|
38
|
+
if (!scheme || scheme.toLowerCase() !== 'bearer' || !token) {
|
|
39
|
+
return undefined;
|
|
40
|
+
}
|
|
41
|
+
return token.trim() || undefined;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Reduce a Shopify customer identifier to its numeric part so the two forms
|
|
45
|
+
* compare equal.
|
|
46
|
+
*
|
|
47
|
+
* Callers send either `"7654321"` or `"gid://shopify/Customer/7654321"`, and
|
|
48
|
+
* both refer to the same customer. Comparing the raw strings would reject a
|
|
49
|
+
* legitimate request.
|
|
50
|
+
*/
|
|
51
|
+
function normalizeGid(value) {
|
|
52
|
+
return String(value)
|
|
53
|
+
.trim()
|
|
54
|
+
.replace(/^gid:\/\/shopify\/Customer\//, '');
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Normalise an Indian mobile number to `91XXXXXXXXXX`.
|
|
58
|
+
*
|
|
59
|
+
* Mirrors the logic in the Shopify webhook handler so a number hashed at login
|
|
60
|
+
* matches the same number submitted in a request, whichever way it was typed:
|
|
61
|
+
* `"9876543210"`, `"+91 98765 43210"` and `"919876543210"` all converge.
|
|
62
|
+
*/
|
|
63
|
+
function normalizePhone(phone) {
|
|
64
|
+
const digits = String(phone).replace(/\D/g, '');
|
|
65
|
+
if (digits.startsWith('91') && digits.length === 12) {
|
|
66
|
+
return digits;
|
|
67
|
+
}
|
|
68
|
+
if (digits.length === 10) {
|
|
69
|
+
return `91${digits}`;
|
|
70
|
+
}
|
|
71
|
+
return digits;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* SHA-256 as lowercase hex.
|
|
75
|
+
*/
|
|
76
|
+
function sha256(value) {
|
|
77
|
+
return (0, crypto_1.createHash)('sha256').update(value, 'utf8').digest('hex');
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Hash a mobile number for storage in a token: normalise first, then hash, so
|
|
81
|
+
* the same number always produces the same digest.
|
|
82
|
+
*/
|
|
83
|
+
function hashPhone(phone) {
|
|
84
|
+
return sha256(normalizePhone(phone));
|
|
85
|
+
}
|
|
86
|
+
//# sourceMappingURL=utils.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"utils.js","sourceRoot":"","sources":["../../src/auth/utils.ts"],"names":[],"mappings":";;AAsBA,gCAKC;AAQD,wBAYC;AAUD,oCAIC;AASD,wCAYC;AAKD,wBAEC;AAMD,8BAEC;AAhGD,6CAAsD;AACtD,mCAAoC;AAWpC;;;;;;;;GAQG;AACH,SAAgB,UAAU,CAAC,OAAyB;IAClD,IAAI,OAAO,CAAC,OAAO,EAAa,KAAK,SAAS,EAAE,CAAC;QAC/C,OAAO,6BAAmB,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,UAAU,EAAE,CAAC,GAAkB,CAAC;IAC7E,CAAC;IACD,OAAO,OAAO,CAAC,YAAY,EAAE,CAAC,UAAU,EAAe,CAAC;AAC1D,CAAC;AAED;;;;;GAKG;AACH,SAAgB,MAAM,CAAC,OAAoB;IACzC,MAAM,GAAG,GAAG,OAAO,CAAC,OAAO,EAAE,aAAa,IAAI,OAAO,CAAC,OAAO,EAAE,aAAa,CAAC;IAC7E,MAAM,MAAM,GAAG,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC;IAEjD,IAAI,CAAC,MAAM;QAAE,OAAO,SAAS,CAAC;IAE9B,MAAM,CAAC,MAAM,EAAE,KAAK,CAAC,GAAG,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;IAC1C,IAAI,CAAC,MAAM,IAAI,MAAM,CAAC,WAAW,EAAE,KAAK,QAAQ,IAAI,CAAC,KAAK,EAAE,CAAC;QAC3D,OAAO,SAAS,CAAC;IACnB,CAAC;IAED,OAAO,KAAK,CAAC,IAAI,EAAE,IAAI,SAAS,CAAC;AACnC,CAAC;AAED;;;;;;;GAOG;AACH,SAAgB,YAAY,CAAC,KAAa;IACxC,OAAO,MAAM,CAAC,KAAK,CAAC;SACjB,IAAI,EAAE;SACN,OAAO,CAAC,8BAA8B,EAAE,EAAE,CAAC,CAAC;AACjD,CAAC;AAED;;;;;;GAMG;AACH,SAAgB,cAAc,CAAC,KAAa;IAC1C,MAAM,MAAM,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;IAEhD,IAAI,MAAM,CAAC,UAAU,CAAC,IAAI,CAAC,IAAI,MAAM,CAAC,MAAM,KAAK,EAAE,EAAE,CAAC;QACpD,OAAO,MAAM,CAAC;IAChB,CAAC;IAED,IAAI,MAAM,CAAC,MAAM,KAAK,EAAE,EAAE,CAAC;QACzB,OAAO,KAAK,MAAM,EAAE,CAAC;IACvB,CAAC;IAED,OAAO,MAAM,CAAC;AAChB,CAAC;AAED;;GAEG;AACH,SAAgB,MAAM,CAAC,KAAa;IAClC,OAAO,IAAA,mBAAU,EAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,KAAK,EAAE,MAAM,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;AAClE,CAAC;AAED;;;GAGG;AACH,SAAgB,SAAS,CAAC,KAAa;IACrC,OAAO,MAAM,CAAC,cAAc,CAAC,KAAK,CAAC,CAAC,CAAC;AACvC,CAAC"}
|
|
@@ -214,6 +214,9 @@ export interface CartAttributeInput {
|
|
|
214
214
|
key: string;
|
|
215
215
|
value: string;
|
|
216
216
|
}
|
|
217
|
+
export interface CartCreateInput {
|
|
218
|
+
attributes?: CartAttributeInput[];
|
|
219
|
+
}
|
|
217
220
|
export interface CommerceAppliedGiftCard {
|
|
218
221
|
id: string;
|
|
219
222
|
lastCharacters: string;
|
|
@@ -313,7 +316,7 @@ export interface ICommerceService {
|
|
|
313
316
|
getCollectionByHandle(handle: string, productsFirst?: number): Promise<IntegrationResponse<CommerceCollection | null>>;
|
|
314
317
|
getMenu(handle: string): Promise<IntegrationResponse<CommerceMenu | null>>;
|
|
315
318
|
getPage(handle: string): Promise<IntegrationResponse<CommercePage | null>>;
|
|
316
|
-
createCart(): Promise<IntegrationResponse<CommerceCart>>;
|
|
319
|
+
createCart(input?: CartCreateInput): Promise<IntegrationResponse<CommerceCart>>;
|
|
317
320
|
getCart(cartId: string): Promise<IntegrationResponse<CommerceCart | null>>;
|
|
318
321
|
addToCart(cartId: string, lines: CartLineInput[]): Promise<IntegrationResponse<CommerceCart>>;
|
|
319
322
|
updateCartLines(cartId: string, lines: CartLineUpdateInput[]): Promise<IntegrationResponse<CommerceCart>>;
|